Compare commits

..

1 Commits

Author SHA1 Message Date
Greg Johnston
134d0ba537 chore(nightly): update proc-macro span file name method name 2025-04-16 08:17:57 -04:00
346 changed files with 3109 additions and 10840 deletions

View File

@@ -29,15 +29,11 @@ Steps to reproduce the behavior:
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Next Steps**
- [ ] I will make a PR
- [ ] I would like to make a PR, but need help getting started
- [ ] I want someone else to take the time to fix this
- [ ] This is a low priority for me and is just shared for your information
**Additional context**
Add any other context about the problem here.

View File

@@ -1,19 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# Grouping all dependencies in one PR weekly
- package-ecosystem: cargo
directory: "/"
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 1
allow:
- dependency-type: "all"
groups:
rust-dependencies:
patterns:
- "*"

View File

@@ -17,23 +17,38 @@ env:
jobs:
autofix:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with: {toolchain: "nightly-2025-07-16", components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
with: {toolchain: nightly, components: "rustfmt, clippy", target: "wasm32-unknown-unknown", rustflags: ""}
- name: Install Glib
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Install cargo-all-features
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
- name: Install jq
run: sudo apt-get install jq
- name: Format the workspace
run: cargo fmt --all
- name: Clippy the workspace
run: cargo all-features clippy --allow-dirty --fix --lib --no-deps
- uses: autofix-ci/action@v1.3.2
- run: |
echo "Formatting the workspace"
cargo fmt --all
echo "Running Clippy against each member's features (default features included)"
for member in $(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | .name'); do
echo "Working on member $member":
echo -e "\tdefault-features/no-features:"
# this will also run on members with no features or default features
cargo clippy --allow-dirty --fix --lib --package "$member"
features=$(cargo metadata --no-deps --format-version 1 | jq -r ".packages[] | select(.name == \"$member\") | .features | keys[]")
for feature in $features; do
if [ "$feature" = "default" ]; then
continue
fi
echo -e "\tfeature $feature"
cargo clippy --allow-dirty --fix --lib --package "$member" --features "$feature"
done
done
- uses: autofix-ci/action@v1.3.1
if: ${{ always() }}
with:
fail-fast: false

View File

@@ -63,6 +63,6 @@ jobs:
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Semver Checks
uses: obi1kenobi/cargo-semver-checks-action@v2

View File

@@ -19,7 +19,7 @@ jobs:
matrix: ${{ steps.set-example-changed.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get example files that changed

View File

@@ -17,7 +17,7 @@ jobs:
EXCLUDED_EXAMPLES: cargo-make
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix

View File

@@ -13,7 +13,7 @@ jobs:
leptos_changed: ${{ steps.set-source-changed.outputs.leptos_changed }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get source files that changed

View File

@@ -13,7 +13,7 @@ jobs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Install jq
run: sudo apt-get install jq
- name: Set Matrix

View File

@@ -12,7 +12,7 @@ jobs:
contents: write # To push a branch
pull-requests: write # To create a PR from that branch
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install mdbook

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
toolchain: [stable, nightly-2025-07-16]
toolchain: [stable, nightly-2025-03-05]
erased_mode: [true, false]
steps:
- name: Free Disk Space
@@ -53,7 +53,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
@@ -72,14 +72,6 @@ jobs:
run: cargo binstall cargo-nextest --no-confirm
- name: Install cargo-all-features
run: cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
# Part of direct-minimal-versions check
- name: Install cargo-hack
if: contains(matrix.toolchain, 'nightly')
uses: taiki-e/install-action@cargo-hack
# Part of direct-minimal-versions check
- name: Install cargo-minimal-versions
if: contains(matrix.toolchain, 'nightly')
uses: taiki-e/install-action@cargo-minimal-versions
- name: Install Trunk
if: contains(inputs.directory, 'examples')
run: cargo binstall trunk --no-confirm
@@ -168,18 +160,6 @@ jobs:
run: |
cd '${{ inputs.directory }}'
cargo make --no-workspace --profile=github-actions ci
# check the direct-minimal-versions on release
COMMIT_MSG=$(git log -1 --pretty=format:'%s')
# Supports: v1.2.3, v1.2.3-alpha, v1.2.3-beta1, v1.2.3-rc.1, etc.
if [[ "$COMMIT_MSG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.?[0-9]+)?)?$ ]]; then
cargo make --no-workspace --profile=github-actions check-minimal-versions
fi
# Check if the counter_isomorphic can be built with leptos_debuginfo cfg flag in release mode
- name: ${{ inputs.cargo_make_task }} with --cfg=leptos_debuginfo
if: contains(inputs.directory, 'counter_isomorphic')
run: |
cd '${{ inputs.directory }}'
RUSTFLAGS="$RUSTFLAGS --cfg leptos_debuginfo" cargo leptos build --release
- name: Clean up ${{ inputs.directory }}
if: always()
run: |

1615
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,139 +40,41 @@ members = [
exclude = ["benchmarks", "examples", "projects"]
[workspace.package]
version = "0.8.0-rc1"
edition = "2021"
rust-version = "1.88"
rust-version = "1.76"
[workspace.dependencies]
# members
throw_error = { path = "./any_error/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.3.0" }
any_spawner = { path = "./any_spawner/", version = "0.3.0-rc1" }
const_str_slice_concat = { path = "./const_str_slice_concat", version = "0.1" }
either_of = { path = "./either_of/", version = "0.1.6" }
either_of = { path = "./either_of/", version = "0.1.5" }
hydration_context = { path = "./hydration_context", version = "0.3.0" }
leptos = { path = "./leptos", version = "0.8.9" }
leptos_config = { path = "./leptos_config", version = "0.8.7" }
leptos_dom = { path = "./leptos_dom", version = "0.8.6" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.5" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.5" }
leptos_macro = { path = "./leptos_macro", version = "0.8.8" }
leptos_router = { path = "./router", version = "0.8.7" }
leptos_router_macro = { path = "./router_macro", version = "0.8.5" }
leptos_server = { path = "./leptos_server", version = "0.8.5" }
leptos_meta = { path = "./meta", version = "0.8.5" }
itertools = "0.14.0"
leptos = { path = "./leptos", version = "0.8.0-rc1" }
leptos_config = { path = "./leptos_config", version = "0.8.0-rc1" }
leptos_dom = { path = "./leptos_dom", version = "0.8.0-rc1" }
leptos_hot_reload = { path = "./leptos_hot_reload", version = "0.8.0-rc1" }
leptos_integration_utils = { path = "./integrations/utils", version = "0.8.0-rc1" }
leptos_macro = { path = "./leptos_macro", version = "0.8.0-rc1" }
leptos_router = { path = "./router", version = "0.8.0-rc1" }
leptos_router_macro = { path = "./router_macro", version = "0.8.0-rc1" }
leptos_server = { path = "./leptos_server", version = "0.8.0-rc1" }
leptos_meta = { path = "./meta", version = "0.8.0-rc1" }
next_tuple = { path = "./next_tuple", version = "0.1.0" }
oco_ref = { path = "./oco", version = "0.2.1" }
oco_ref = { path = "./oco", version = "0.2.0" }
or_poisoned = { path = "./or_poisoned", version = "0.1.0" }
reactive_graph = { path = "./reactive_graph", version = "0.2.7" }
reactive_stores = { path = "./reactive_stores", version = "0.2.5" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.6" }
server_fn = { path = "./server_fn", version = "0.8.7" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.7" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.5" }
tachys = { path = "./tachys", version = "0.2.8" }
wasm_split_helpers = { path = "./wasm_split", version = "0.1.2" }
wasm_split_macros = { path = "./wasm_split_macros", version = "0.1.3" }
# members deps
async-once-cell = { default-features = false, version = "0.5.3" }
itertools = { default-features = false, version = "0.14.0" }
convert_case = { default-features = false, version = "0.8.0" }
serde_json = { default-features = false, version = "1.0.143" }
trybuild = { default-features = false, version = "1.0.110" }
typed-builder = { default-features = false, version = "0.21.2" }
thiserror = { default-features = false, version = "2.0.16" }
wasm-bindgen = { default-features = false, version = "0.2.100" }
indexmap = { default-features = false, version = "2.11.0" }
rstml = { default-features = false, version = "0.12.1" }
rustc_version = { default-features = false, version = "0.4.1" }
guardian = { default-features = false, version = "1.3.0" }
rustc-hash = { default-features = false, version = "2.1.1" }
actix-web = { default-features = false, version = "4.11.0" }
tracing = { default-features = false, version = "0.1.41" }
slotmap = { default-features = false, version = "1.0.7" }
futures = { default-features = false, version = "0.3.31" }
dashmap = { default-features = false, version = "6.1.0" }
pin-project-lite = { default-features = false, version = "0.2.16" }
send_wrapper = { default-features = false, version = "0.6.0" }
tokio-test = { default-features = false, version = "0.4.4" }
html-escape = { default-features = false, version = "0.2.13" }
proc-macro-error2 = { default-features = false, version = "2.0.1" }
const_format = { default-features = false, version = "0.2.34" }
gloo-net = { default-features = false, version = "0.6.0" }
url = { default-features = false, version = "2.5.4" }
tokio = { default-features = false, version = "1.47.1" }
base64 = { default-features = false, version = "0.22.1" }
cfg-if = { default-features = false, version = "1.0.3" }
wasm-bindgen-futures = { default-features = false, version = "0.4.50" }
tower = { default-features = false, version = "0.5.2" }
proc-macro2 = { default-features = false, version = "1.0.101" }
serde = { default-features = false, version = "1.0.219" }
parking_lot = { default-features = false, version = "0.12.4" }
axum = { default-features = false, version = "0.8.4" }
serde_qs = { default-features = false, version = "0.15.0" }
syn = { default-features = false, version = "2.0.106" }
xxhash-rust = { default-features = false, version = "0.8.15" }
paste = { default-features = false, version = "1.0.15" }
quote = { default-features = false, version = "1.0.40" }
web-sys = { default-features = false, version = "0.3.77" }
js-sys = { default-features = false, version = "0.3.77" }
rand = { default-features = false, version = "0.9.1" }
serde-lite = { default-features = false, version = "0.5.0" }
tokio-tungstenite = { default-features = false, version = "0.27.0" }
serial_test = { default-features = false, version = "3.2.0" }
erased = { default-features = false, version = "0.1.2" }
glib = { default-features = false, version = "0.20.12" }
async-trait = { default-features = false, version = "0.1.89" }
typed-builder-macro = { default-features = false, version = "0.21.0" }
linear-map = { default-features = false, version = "1.2.0" }
anyhow = { default-features = false, version = "1.0.99" }
walkdir = { default-features = false, version = "2.5.0" }
actix-ws = { default-features = false, version = "0.3.0" }
tower-http = { default-features = false, version = "0.6.4" }
prettyplease = { default-features = false, version = "0.2.37" }
inventory = { default-features = false, version = "0.3.21" }
config = { default-features = false, version = "0.15.14" }
camino = { default-features = false, version = "1.1.11" }
ciborium = { default-features = false, version = "0.2.2" }
multer = { default-features = false, version = "3.1.0" }
leptos-spin-macro = { default-features = false, version = "0.2.0" }
sledgehammer_utils = { default-features = false, version = "0.3.1" }
sledgehammer_bindgen = { default-features = false, version = "0.6.0" }
wasm-streams = { default-features = false, version = "0.4.2" }
rkyv = { default-features = false, version = "0.8.11" }
temp-env = { default-features = false, version = "0.3.6" }
uuid = { default-features = false, version = "1.18.0" }
bytes = { default-features = false, version = "1.10.1" }
http = { default-features = false, version = "1.3.1" }
regex = { default-features = false, version = "1.11.2" }
drain_filter_polyfill = { default-features = false, version = "0.1.3" }
tempfile = { default-features = false, version = "3.21.0" }
futures-lite = { default-features = false, version = "2.6.1" }
log = { default-features = false, version = "0.4.27" }
percent-encoding = { default-features = false, version = "2.3.2" }
async-executor = { default-features = false, version = "1.13.2" }
const-str = { default-features = false, version = "0.6.4" }
http-body-util = { default-features = false, version = "0.1.3" }
hyper = { default-features = false, version = "1.7.0" }
postcard = { default-features = false, version = "1.1.3" }
rmp-serde = { default-features = false, version = "1.3.0" }
reqwest = { default-features = false, version = "0.12.23" }
tower-layer = { default-features = false, version = "0.3.3" }
attribute-derive = { default-features = false, version = "0.10.3" }
insta = { default-features = false, version = "1.43.1" }
codee = { default-features = false, version = "0.3.0" }
actix-http = { default-features = false, version = "3.11.1" }
wasm-bindgen-test = { default-features = false, version = "0.3.50" }
rustversion = { default-features = false, version = "1.0.22" }
getrandom = { default-features = false, version = "0.3.3" }
actix-files = { default-features = false, version = "0.6.6" }
async-lock = { default-features = false, version = "3.4.1" }
base16 = { default-features = false, version = "0.2.1" }
digest = { default-features = false, version = "0.10.7" }
sha2 = { default-features = false, version = "0.10.8" }
subsecond = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
dioxus-cli-config = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
dioxus-devtools = { default-features = false, git = "https://github.com/dioxuslabs/dioxus" }
reactive_graph = { path = "./reactive_graph", version = "0.2.0-rc1" }
reactive_stores = { path = "./reactive_stores", version = "0.2.0-rc1" }
reactive_stores_macro = { path = "./reactive_stores_macro", version = "0.2.0-rc1" }
rustversion = "1"
serde_json = "1.0.0"
server_fn = { path = "./server_fn", version = "0.8.0-rc1" }
server_fn_macro = { path = "./server_fn_macro", version = "0.8.0-rc1" }
server_fn_macro_default = { path = "./server_fn/server_fn_macro_default", version = "0.8.0-rc1" }
tachys = { path = "./tachys", version = "0.2.0-rc1" }
trybuild = "1"
wasm-bindgen = { version = "0.2.100" }
[profile.release]
codegen-units = 1

View File

@@ -90,13 +90,35 @@ Here are some resources for learning more about Leptos:
- [API Documentation](https://docs.rs/leptos/latest/leptos/)
- [Common Bugs](https://github.com/leptos-rs/leptos/tree/main/docs/COMMON_BUGS.md) (and how to fix them!)
## `nightly` Note
Most of the examples assume youre using `nightly` version of Rust and the `nightly` feature of Leptos. To use `nightly` Rust, you can either set your toolchain globally or on per-project basis.
To set `nightly` as a default toolchain for all projects (and add the ability to compile Rust to WebAssembly, if you havent already):
```
rustup toolchain install nightly
rustup default nightly
rustup target add wasm32-unknown-unknown
```
If you'd like to use `nightly` only in your Leptos project however, add [`rust-toolchain.toml`](https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file) file with the following content:
```toml
[toolchain]
channel = "nightly"
targets = ["wasm32-unknown-unknown"]
```
The `nightly` feature enables the function call syntax for accessing and setting signals, as opposed to `.get()` and `.set()`. This leads to a consistent mental model in which accessing a reactive value of any kind (a signal, memo, or derived signal) is always represented as a function call. This is only possible with nightly Rust and the `nightly` feature.
## `cargo-leptos`
[`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos) is a build tool that's designed to make it easy to build apps that run on both the client and the server, with seamless integration. The best way to get started with a real Leptos project right now is to use `cargo-leptos` and our starter templates for [Actix](https://github.com/leptos-rs/start) or [Axum](https://github.com/leptos-rs/start-axum).
```bash
cargo install cargo-leptos
cargo leptos new --git https://github.com/leptos-rs/start-axum
cargo leptos new --git https://github.com/leptos-rs/start
cd [your project name]
cargo leptos watch
```
@@ -125,7 +147,7 @@ Yes, Im sure there are. You can see from the state of our issue tracker over
This may be the big one: “production ready” implies a certain orientation to a library: that you can basically use it, without any special knowledge of its internals or ability to contribute. Everyone has this at some level in their stack: for example I (@gbj) dont have the capacity or knowledge to contribute to something like `wasm-bindgen` at this point: I simply rely on it to work.
There are several people in the community using Leptos right now for many websites at work, who have also become significant contributors. There may be missing features that you need, and you may end up building them! But, if you're willing to contribute a few missing pieces along the way, the framework is most definitely usable for production applications, especially given the ecosystem of libraries that have sprung up around it.
There are several people in the community using Leptos right now for internal apps at work, who have also become significant contributors. I think this is the right level of production use for now. There may be missing features that you need, and you may end up building them! But for internal apps, if youre willing to build and contribute missing pieces along the way, the framework is definitely usable right now.
### Can I use this for native GUI?

View File

@@ -10,4 +10,4 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = { workspace = true, default-features = true }
pin-project-lite = "0.2.15"

View File

@@ -1,6 +1,6 @@
[package]
name = "any_spawner"
version = "0.3.0"
version = "0.3.0-rc1"
authors = ["Greg Johnston"]
license = "MIT"
readme = "../README.md"
@@ -9,25 +9,25 @@ description = "Spawn asynchronous tasks in an executor-independent way."
edition.workspace = true
[dependencies]
async-executor = { optional = true , workspace = true, default-features = true }
futures = { workspace = true, default-features = true }
glib = { optional = true , workspace = true, default-features = true }
thiserror = { workspace = true , default-features = true }
tokio = { optional = true, default-features = false, features = [
async-executor = { version = "1.13.1", optional = true }
futures = "0.3.31"
glib = { version = "0.20.6", optional = true }
thiserror = "2.0"
tokio = { version = "1.41", optional = true, default-features = false, features = [
"rt",
] , workspace = true }
tracing = { optional = true , workspace = true, default-features = true }
wasm-bindgen-futures = { optional = true , workspace = true, default-features = true }
] }
tracing = { version = "0.1.41", optional = true }
wasm-bindgen-futures = { version = "0.4.50", optional = true }
[dev-dependencies]
futures-lite = { default-features = false , workspace = true }
tokio = { default-features = false, features = [
futures-lite = { version = "2.6.0", default-features = false }
tokio = { version = "1.41", default-features = false, features = [
"rt",
"macros",
"time",
] , workspace = true }
wasm-bindgen-test = { workspace = true, default-features = true }
serial_test = { workspace = true, default-features = true }
] }
wasm-bindgen-test = { version = "0.3.50" }
serial_test = "3.2.0"
[features]
async-executor = ["dep:async-executor"]

View File

@@ -472,8 +472,9 @@ fn handle_uninitialized_spawn(_fut: PinnedFuture<()>) {
#[cfg(all(debug_assertions, not(feature = "tracing")))]
{
panic!(
"At {caller}, tried to spawn a Future with Executor::spawn() \
before a global executor was initialized."
"At {}, tried to spawn a Future with Executor::spawn() before a \
global executor was initialized.",
caller
);
}
// In release builds (without tracing), call the specific no-op function.
@@ -502,8 +503,9 @@ fn handle_uninitialized_spawn_local(_fut: PinnedLocalFuture<()>) {
#[cfg(all(debug_assertions, not(feature = "tracing")))]
{
panic!(
"At {caller}, tried to spawn a Future with \
Executor::spawn_local() before a global executor was initialized."
"At {}, tried to spawn a Future with Executor::spawn_local() \
before a global executor was initialized.",
caller
);
}
// In release builds (without tracing), call the specific no-op function (which usually panics).

View File

@@ -2,6 +2,8 @@
name = "benchmarks"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[dependencies]
l0410 = { package = "leptos", version = "0.4.10", features = [

View File

@@ -1,14 +0,0 @@
[tasks.check-minimal-versions]
condition = { channels = ["nightly"] }
command = "cargo"
args = [
"all-features",
"minimal-versions",
"check",
"--ignore-private",
"--detach-path-deps",
"--direct",
]
install_script = '''
cargo install --git https://github.com/sabify/cargo-all-features --branch arbitrary-command-support
'''

View File

@@ -1,8 +1,4 @@
extend = [
{ path = "./lint.toml" },
{ path = "./test.toml" },
{ path = "./check-minimal-versions.toml" },
]
extend = [{ path = "./lint.toml" }, { path = "./test.toml" }]
[env]
RUSTFLAGS = ""

View File

@@ -31,7 +31,7 @@ pub const fn const_concat(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable references in const fns
// no mutable refernces in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
@@ -59,7 +59,7 @@ pub const fn const_concat_with_prefix(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable references in const fns
// no mutable refernces in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;
@@ -116,7 +116,7 @@ pub const fn const_concat_with_separator(
let mut i = 0;
// have it iterate over bytes manually, because, again,
// no mutable references in const fns
// no mutable refernces in const fns
while i < x.len() {
buffer[position] = x[i];
position += 1;

View File

@@ -10,8 +10,8 @@ rust-version.workspace = true
edition.workspace = true
[dependencies]
pin-project-lite = { workspace = true, default-features = true }
paste = { workspace = true, default-features = true }
pin-project-lite = "0.2.16"
paste = "1.0.15"
[features]
default = ["no_std"]

View File

@@ -19,7 +19,7 @@ leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
thiserror = "1.0"
tokio = { version = "1.39", features = [
"rt-multi-thread",
"macros",

View File

@@ -1,11 +0,0 @@
extend = [
{ path = "./cargo-leptos.toml" },
{ path = "../cargo-make/webdriver.toml" },
]
[tasks.integration-test]
dependencies = [
"install-cargo-leptos",
"start-webdriver",
"cargo-leptos-e2e-split",
]

View File

@@ -11,10 +11,6 @@ args = ["--locked"]
command = "cargo"
args = ["leptos", "end-to-end"]
[tasks.cargo-leptos-e2e-split]
command = "cargo"
args = ["leptos", "end-to-end", "--split"]
[tasks.build]
clear = true
command = "cargo"

View File

@@ -2,6 +2,8 @@
name = "counter_isomorphic"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -21,6 +23,7 @@ leptos = { path = "../../leptos" }
leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.22"
once_cell = "1.19"
gloo-net = { version = "0.6.0" }
wasm-bindgen = "0.2.93"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -18,7 +18,7 @@ tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
http = { version = "1.1" }
thiserror = "2.0.12"
thiserror = "1.0"
wasm-bindgen = "0.2.93"
[features]

View File

@@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
log = "0.4.22"
console_log = "1.0"
console_error_panic_hook = "0.1.7"
thiserror = "2.0.12"
thiserror = "1.0"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-subscriber-wasm = "0.1.0"

View File

@@ -7,7 +7,7 @@ pub fn main() {
fmt()
.with_writer(
// To avoid trace events in the browser from showing their
// To avoide trace events in the browser from showing their
// JS backtrace, which is very annoying, in my opinion
MakeConsoleWriter::default()
.map_trace_level_to(tracing::Level::DEBUG),

View File

@@ -6,7 +6,7 @@ use leptos_router::{components::A, hooks::use_params_map};
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new_blocking(
let story = Resource::new(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -4,7 +4,7 @@ mod routes;
use leptos_meta::{provide_meta_context, Link, Meta, MetaTags, Stylesheet};
use leptos_router::{
components::{FlatRoutes, Route, Router, RoutingProgress},
Lazy, OptionalParamSegment, ParamSegment, StaticSegment,
OptionalParamSegment, ParamSegment, StaticSegment,
};
use routes::{nav::*, stories::*, story::*, users::*};
use std::time::Duration;
@@ -44,8 +44,8 @@ pub fn App() -> impl IntoView {
<Nav />
<main>
<FlatRoutes fallback=|| "Not found.">
<Route path=(StaticSegment("users"), ParamSegment("id")) view={Lazy::<UserRoute>::new()}/>
<Route path=(StaticSegment("stories"), ParamSegment("id")) view={Lazy::<StoryRoute>::new()}/>
<Route path=(StaticSegment("users"), ParamSegment("id")) view=User/>
<Route path=(StaticSegment("stories"), ParamSegment("id")) view=Story/>
<Route path=OptionalParamSegment("stories") view=Stories/>
</FlatRoutes>
</main>

View File

@@ -1,7 +1,7 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::get, Router};
use axum::Router;
use hackernews_axum::{shell, App};
use leptos::config::get_configuration;
use leptos_axum::{generate_route_list, LeptosRoutes};
@@ -13,15 +13,6 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route(
"/favicon.ico",
get(|| async {
(
[("content-type", "image/x-icon")],
include_bytes!("../public/favicon.ico"),
)
}),
)
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -1,38 +1,24 @@
use crate::api::{self, Story};
use crate::api;
use leptos::{either::Either, prelude::*};
use leptos_meta::Meta;
use leptos_router::{
components::A, hooks::use_params_map, lazy_route, LazyRoute,
};
use leptos_router::{components::A, hooks::use_params_map};
#[derive(Debug)]
pub struct StoryRoute {
story: Resource<Option<Story>>,
}
#[lazy_route]
impl LazyRoute for StoryRoute {
fn data() -> Self {
let params = use_params_map();
let story = Resource::new_blocking(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(&api::story(&format!(
"item/{id}"
)))
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<api::Story>(&api::story(&format!("item/{id}")))
.await
}
},
);
Self { story }
}
}
},
);
fn view(this: Self) -> AnyView {
let StoryRoute { story } = this;
Suspense(SuspenseProps::builder().fallback(|| "Loading...").children(ToChildren::to_children(move || Suspend::new(async move {
Suspense(SuspenseProps::builder().fallback(|| "Loading...").children(ToChildren::to_children(move || Suspend::new(async move {
match story.await.clone() {
None => Either::Left("Story not found."),
Some(story) => {
@@ -75,8 +61,7 @@ impl LazyRoute for StoryRoute {
})
}
}
}))).build()).into_any()
}
}))).build())
}
#[component]

View File

@@ -1,58 +1,46 @@
use crate::api::{self, User};
use leptos::{either::Either, prelude::*, server::Resource};
use leptos_router::{hooks::use_params_map, lazy_route, LazyRoute};
use leptos_router::hooks::use_params_map;
#[derive(Debug)]
pub struct UserRoute {
user: Resource<Option<User>>,
}
#[lazy_route]
impl LazyRoute for UserRoute {
fn data() -> Self {
let params = use_params_map();
let user = Resource::new(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
UserRoute { user }
}
fn view(this: Self) -> AnyView {
let UserRoute { user } = this;
view! {
<div class="user-view">
<Suspense fallback=|| view! { "Loading..." }>
{move || Suspend::new(async move { match user.await.clone() {
None => Either::Left(view! { <h1>"User not found."</h1> }),
Some(user) => Either::Right(view! {
<div>
<h1>"User: " {user.id.clone()}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
<li inner_html={user.about} class="about"></li>
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
})
}})}
</Suspense>
</div>
}.into_any()
#[component]
pub fn User() -> impl IntoView {
let params = use_params_map();
let user = Resource::new(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {
None
} else {
api::fetch_api::<User>(&api::user(&id)).await
}
},
);
view! {
<div class="user-view">
<Suspense fallback=|| view! { "Loading..." }>
{move || Suspend::new(async move { match user.await.clone() {
None => Either::Left(view! { <h1>"User not found."</h1> }),
Some(user) => Either::Right(view! {
<div>
<h1>"User: " {user.id.clone()}</h1>
<ul class="meta">
<li>
<span class="label">"Created: "</span> {user.created}
</li>
<li>
<span class="label">"Karma: "</span> {user.karma}
</li>
<li inner_html={user.about} class="about"></li>
</ul>
<p class="links">
<a href=format!("https://news.ycombinator.com/submitted?id={}", user.id)>"submissions"</a>
" | "
<a href=format!("https://news.ycombinator.com/threads?id={}", user.id)>"comments"</a>
</p>
</div>
})
}})}
</Suspense>
</div>
}
}

View File

@@ -26,17 +26,12 @@ pub async fn file_and_error_handler(
.map(|h| h.to_str().unwrap_or("none"))
.unwrap_or("none")
.to_string();
let static_result = get_static_file(uri.clone(), accept_encoding).await;
let res = get_static_file(uri.clone(), accept_encoding).await.unwrap();
match static_result {
Ok(res) => {
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
}
}
Err(e) => e.into_response(),
if res.status() == StatusCode::OK {
res.into_response()
} else {
(StatusCode::NOT_FOUND, "Not found.").into_response()
}
}

View File

@@ -1,7 +1,6 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::routing::get;
pub use axum::Router;
use hackernews_islands::*;
pub use leptos::config::get_configuration;
@@ -26,7 +25,6 @@ async fn main() {
// build our application with a route
let app = Router::new()
.route("/favicon.ico", get(fallback::file_and_error_handler))
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())

View File

@@ -47,7 +47,7 @@ pub fn Stories() -> impl IntoView {
let stories = Resource::new(
move || (page(), story_type()),
move |(page, story_type)| async move {
fetch_stories(story_type, page).await.ok()
fetch_stories(category(&story_type), page).await.ok()
},
);
let (pending, set_pending) = signal(false);

View File

@@ -13,7 +13,7 @@ pub async fn fetch_story(
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new_blocking(
let story = Resource::new(
move || params.read().get("id").unwrap_or_default(),
move |id| async move {
if id.is_empty() {

View File

@@ -7,7 +7,7 @@ use send_wrapper::SendWrapper;
#[component]
pub fn Story() -> impl IntoView {
let params = use_params_map();
let story = Resource::new_blocking(
let story = Resource::new(
move || params.read().get("id").unwrap_or_default(),
move |id| {
SendWrapper::new(async move {

View File

@@ -10,11 +10,7 @@ crate-type = ["cdylib", "rlib"]
console_error_panic_hook = "0.1.7"
futures = "0.3.30"
http = "1.1"
leptos = { path = "../../leptos", features = [
"tracing",
"islands",
"islands-router",
] }
leptos = { path = "../../leptos", features = ["tracing", "islands"] }
leptos_router = { path = "../../router" }
server_fn = { path = "../../server_fn", features = ["serde-lite"] }
leptos_axum = { path = "../../integrations/axum", features = [

View File

@@ -1,95 +0,0 @@
[package]
name = "lazy_routes"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = ["tracing"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
tokio = { version = "1.39", features = [
"rt-multi-thread",
"macros",
"time",
], optional = true }
wasm-bindgen = "0.2.92"
futures = "0.3.31"
serde_json = "1.0.140"
gloo-timers = { version = "0.3", features = ["futures"] }
[features]
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "regression"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Leptos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,8 +0,0 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-split-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "regression"

View File

@@ -1,8 +0,0 @@
# Regression Tests
This example functions as a catch-all for all current and future regression
test cases that typically happens at integration.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,18 +0,0 @@
[package]
name = "lazy_routes_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0"
async-trait = "0.1.81"
cucumber = "0.21.1"
fantoccini = "0.21.1"
pretty_assertions = "1.4"
serde_json = "1.0"
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time"] }
url = "2.5"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -1,20 +0,0 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"2",
"--fail-fast",
"${@}",
]

View File

@@ -1,30 +0,0 @@
# Lazy Routes
This example demonstrates how to split the WASM bundle that is sent to the client into multiple binaries, which can be lazy-loaded, either independently or in a way that's integrated into the router.
Without code splitting, the entire application is compiled to a monolithic WASM binary, the size of which grows in proportion to the complexity of the application. This means that the time to interactive (TTI) for any page is proportional to the size of the entire application, not only that page.
Code splitting allows you to lazy-load some functions, by splitting off the WASM binary code for certain functions into separate files, which can be downloaded as needed. This minimizes initial TTI for any page, and then amortizes the cost of loading the binary over the lifetime of the application session.
In many cases, this can be done with minimal or no cost.
Lazy loading can be used in two ways, each of which is shown in the example.
## `#[lazy]` macro
`#[lazy]` is an attribute macro that can be used to annotate an `async fn` in order to split its code out into a separate file that will be loaded on demand, when compiled with `cargo leptos --split`.
This has some limitations (for example, it must return concrete types) but can be used for most functions.
## `LazyRoute`
`LazyRoute` is a specialized application of `#[lazy]` that allows you to define an entire route/page of your application as being lazy-loaded.
Creating a lazy route requires you to split the route into two parts:
1. `data()`: A synchronous method that should be used to start loading any async data used by the page, for example by creating a `Resource`
2. `view()`: An async (because lazy-loaded) method that renders the view.
The purpose of splitting these into two parts is to avoid a “waterfall,” in which the browser first waits for a lazy-loaded WASM chunk that defines the page, _then_ makes a second request to the server to load the relevant data. Instead, a `LazyRoute` will begin loading resources created in the `data` method while lazy-loading the component body in the `view`, then render the route.
This means that in many cases, the data loading “hides” the cost of the lazy-loading; i.e., the page needs to wait for the data to load, so the fact that it is waiting concurrently for the lazy-loaded view means that the lazy loading does not cost anything additional in terms of page load time.

View File

@@ -1,33 +0,0 @@
@basic
Feature: Check that each page hydrates correctly
Scenario: Page A is rendered correctly.
Given I see the app
Then I see the page is View A
Scenario: Page A hydrates and allows navigating to page B.
Given I see the app
When I select the link B
Then I see the navigating indicator
When I wait for a second
Then I see the page is View B
Scenario: Page B is rendered correctly.
When I open the app at /b
Then I see the page is View B
Scenario: Page B hydrates and allows navigating to page C.
When I open the app at /b
When I select the link C
Then I see the navigating indicator
When I wait for a second
Then I see the page is View C
Scenario: Page C is rendered correctly.
When I open the app at /c
Then I see the page is View C
Scenario: Page C hydrates and allows navigating to page A.
When I open the app at /c
When I select the link A
Then I see the page is View A

View File

@@ -1,15 +0,0 @@
@duplicate_names
Feature: Lazy functions can share the same name
Scenario: Two functions with the same name both work.
Given I see the app
Then I see the page is View A
When I click the button First
When I wait for a second
Then I see the result is {"a":"First Value","b":1}
When I click the button Second
When I wait for a second
Then I see the result is {"a":"Second Value","b":2}
When I click the button Third
When I wait for a second
Then I see the result is Third value.

View File

@@ -1,9 +0,0 @@
@shared_chunks
Feature: Shared code splitting works correctly
Scenario: Two functions using same serde code both work.
Given I see the app
Then I see the page is View A
When I click the button First
When I wait for a second
Then I see the result is {"a":"First Value","b":1}

View File

@@ -1,30 +0,0 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
use std::{ffi::OsStr, fs::read_dir};
#[tokio::main]
async fn main() -> Result<()> {
// Normally the below is done, but it's now gotten to the point of
// having a sufficient number of tests where the resource contention
// of the concurrently running browsers will cause failures on CI.
// AppWorld::cucumber()
// .fail_on_skipped()
// .run_and_exit("./features")
// .await;
// Mitigate the issue by manually stepping through each feature,
// rather than letting cucumber glob them and dispatch all at once.
for entry in read_dir("./features")? {
let path = entry?.path();
if path.extension() == Some(OsStr::new("feature")) {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit(path)
.await;
}
}
Ok(())
}

View File

@@ -1,23 +0,0 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn click_link(client: &Client, text: &str) -> Result<()> {
let link = find::link_with_text(&client, &text).await?;
link.click().await?;
Ok(())
}
pub async fn click_button(client: &Client, id: &str) -> Result<()> {
let btn = find::element_by_id(&client, &id).await?;
btn.click().await?;
Ok(())
}

View File

@@ -1,29 +0,0 @@
use crate::fixtures::find;
use anyhow::{Ok, Result};
use fantoccini::Client;
use pretty_assertions::assert_eq;
pub async fn page_name_is(client: &Client, expected_text: &str) -> Result<()> {
let actual = find::text_at_id(client, "page").await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn result_is(client: &Client, expected_text: &str) -> Result<()> {
let actual = find::text_at_id(client, "result").await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn navigating_appears(client: &Client) -> Result<()> {
let actual = find::text_at_id(client, "navigating").await?;
assert_eq!(&actual, "Navigating...");
Ok(())
}
pub async fn element_exists(client: &Client, id: &str) -> Result<()> {
find::element_by_id(client, id)
.await
.expect(&format!("could not find element with id `{id}`"));
Ok(())
}

View File

@@ -1,23 +0,0 @@
use anyhow::{Ok, Result};
use fantoccini::{elements::Element, Client, Locator};
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
let element = element_by_id(client, id)
.await
.expect(format!("no such element with id `{}`", id).as_str());
let text = element.text().await?;
Ok(text)
}
pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
let link = client
.wait()
.for_element(Locator::LinkText(text))
.await
.expect(format!("Link not found by `{}`", text).as_str());
Ok(link)
}
pub async fn element_by_id(client: &Client, id: &str) -> Result<Element> {
Ok(client.wait().for_element(Locator::Id(id)).await?)
}

View File

@@ -1,4 +0,0 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -1,68 +0,0 @@
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{gherkin::Step, given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[when(regex = "^I open the app at (.*)$")]
async fn i_open_the_app_at(world: &mut AppWorld, url: String) -> Result<()> {
let client = &world.client;
action::goto_path(client, &url).await?;
Ok(())
}
#[when(regex = "^I select the link (.*)$")]
async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::click_link(client, &text).await?;
Ok(())
}
#[when(regex = "^I click the button (.*)$")]
async fn i_click_the_button(world: &mut AppWorld, id: String) -> Result<()> {
let client = &world.client;
action::click_button(client, &id).await?;
Ok(())
}
#[when(expr = "I select the following links")]
async fn i_select_the_following_links(
world: &mut AppWorld,
step: &Step,
) -> Result<()> {
let client = &world.client;
if let Some(table) = step.table.as_ref() {
for row in table.rows.iter() {
action::click_link(client, &row[0]).await?;
}
}
Ok(())
}
#[when("I wait for a second")]
async fn i_wait_for_a_second(world: &mut AppWorld) -> Result<()> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(())
}
#[given(regex = "^I (refresh|reload) the (browser|page)$")]
#[when(regex = "^I (refresh|reload) the (browser|page)$")]
async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
client.refresh().await?;
Ok(())
}

View File

@@ -1,31 +0,0 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = r"^I see the navigating indicator")]
async fn i_see_the_nav(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::navigating_appears(client).await?;
Ok(())
}
#[then(regex = r"^I see the page is (.*)$")]
async fn i_see_the_page_is(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
check::page_name_is(client, &text).await?;
Ok(())
}
#[then(regex = r"^I see the result is (.*)$")]
async fn i_see_the_result_is(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
check::result_is(client, &text).await?;
Ok(())
}
#[then(regex = r"^I see the navbar$")]
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::element_exists(client, "nav").await?;
Ok(())
}

View File

@@ -1,39 +0,0 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

View File

@@ -1,355 +0,0 @@
use leptos::{prelude::*, task::spawn_local};
use leptos_router::{
components::{Outlet, ParentRoute, Route, Router, Routes},
lazy_route, Lazy, LazyRoute, StaticSegment,
};
use serde::{Deserialize, Serialize};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
let count = RwSignal::new(0);
provide_context(count);
let (is_routing, set_is_routing) = signal(false);
view! {
<nav id="nav" style="width: 100%">
<a href="/">"A"</a> " | "
<a href="/b">"B"</a> " | "
<a href="/c">"C"</a> " | "
<a href="/d">"D"</a>
<span style="float: right" id="navigating">
{move || is_routing.get().then_some("Navigating...")}
</span>
</nav>
<Router set_is_routing>
<Routes fallback=|| "Not found.">
<Route path=StaticSegment("") view=ViewA/>
<Route path=StaticSegment("b") view=ViewB/>
<Route path=StaticSegment("c") view={Lazy::<ViewC>::new()}/>
// you can nest lazy routes, and there data and views will all load concurrently
<ParentRoute path=StaticSegment("d") view={Lazy::<ViewD>::new()}>
<Route path=StaticSegment("") view={Lazy::<ViewE>::new()}/>
</ParentRoute>
</Routes>
</Router>
}
}
// View A: A plain old synchronous route, just like they all currently work. The WASM binary code
// for this is shipped as part of the main bundle. Any data-loading code (like resources that run
// in the body of the component) will be shipped as part of the main bundle.
#[component]
pub fn ViewA() -> impl IntoView {
leptos::logging::log!("View A");
let result = RwSignal::new("Click a button to see the result".to_string());
view! {
<p id="page">"View A"</p>
<pre id="result">{result}</pre>
<button id="First" on:click=move |_| spawn_local(async move { result.set(first_value().await); })>"First"</button>
<button id="Second" on:click=move |_| spawn_local(async move { result.set(second_value().await); })>"Second"</button>
// test to make sure duplicate names in different scopes can be used
<button id="Third" on:click=move |_| {
#[lazy]
pub fn second_value() -> String {
"Third value.".to_string()
}
spawn_local(async move {
result.set(second_value().await);
});
}>"Third"</button>
}
}
// View B: lazy-loaded route with lazy-loaded data
#[derive(Debug, Clone, Deserialize)]
pub struct Comment {
#[serde(rename = "postId")]
post_id: usize,
id: usize,
name: String,
email: String,
body: String,
}
#[lazy]
fn deserialize_comments(data: &str) -> Vec<Comment> {
serde_json::from_str(data).unwrap()
}
#[component]
pub fn ViewB() -> impl IntoView {
let data = LocalResource::new(|| async move {
let preload = deserialize_comments("[]");
let (_, data) = futures::future::join(preload, async {
gloo_timers::future::TimeoutFuture::new(500).await;
r#"
[
{
"postId": 1,
"id": 1,
"name": "id labore ex et quam laborum",
"email": "Eliseo@gardner.biz",
"body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
},
{
"postId": 1,
"id": 2,
"name": "quo vero reiciendis velit similique earum",
"email": "Jayne_Kuhic@sydney.com",
"body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
},
{
"postId": 1,
"id": 3,
"name": "odio adipisci rerum aut animi",
"email": "Nikita@garfield.biz",
"body": "quia molestiae reprehenderit quasi aspernatur\naut expedita occaecati aliquam eveniet laudantium\nomnis quibusdam delectus saepe quia accusamus maiores nam est\ncum et ducimus et vero voluptates excepturi deleniti ratione"
}
]
"#
})
.await;
deserialize_comments(data).await
});
view! {
<p id="page">"View B"</p>
<Suspense fallback=|| view! { <p id="loading">"Loading..."</p> }>
<ul>
{move || Suspend::new(async move {
let items = data.await;
items.into_iter()
.map(|comment| view! {
<li id=format!("{}-{}", comment.post_id, comment.id)>
<strong>{comment.name}</strong> " (by " {comment.email} ")"<br/>
{comment.body}
</li>
})
.collect_view()
})}
</ul>
</Suspense>
}
.into_any()
}
#[derive(Debug, Clone, Deserialize)]
pub struct Album {
#[serde(rename = "userId")]
user_id: usize,
id: usize,
title: String,
}
// View C: a lazy view, and some data, loaded in parallel when we navigate to /c.
#[derive(Clone)]
pub struct ViewC {
data: LocalResource<Vec<Album>>,
}
// Lazy-loaded routes need to implement the LazyRoute trait. They define a "route data" struct,
// which is created with `::data()`, and then a separate view function which is lazily loaded.
//
// This is important because it allows us to concurrently 1) load the route data, and 2) lazily
// load the component, rather than creating a "waterfall" where we can't start loading the route
// data until we've received the view.
//
// The `#[lazy_route]` macro makes `view` into a lazy-loaded inner function, replacing `self` with
// `this`.
#[lazy_route]
impl LazyRoute for ViewC {
fn data() -> Self {
// the data method itself is synchronous: it typically creates things like Resources,
// which are created synchronously but spawn an async data-loading task
// if you want further code-splitting, however, you can create a lazy function to load the data!
#[lazy]
async fn lazy_data() -> Vec<Album> {
gloo_timers::future::TimeoutFuture::new(250).await;
vec![
Album {
user_id: 1,
id: 1,
title: "quidem molestiae enim".into(),
},
Album {
user_id: 1,
id: 2,
title: "sunt qui excepturi placeat culpa".into(),
},
Album {
user_id: 1,
id: 3,
title: "omnis laborum odio".into(),
},
]
}
Self {
data: LocalResource::new(lazy_data),
}
}
fn view(this: Self) -> AnyView {
let albums = move || {
Suspend::new(async move {
this.data
.await
.into_iter()
.map(|album| {
view! {
<li id=format!("{}-{}", album.user_id, album.id)>
{album.title}
</li>
}
})
.collect::<Vec<_>>()
})
};
view! {
<p id="page">"View C"</p>
<hr/>
<Suspense fallback=|| view! { <p id="loading">"Loading..."</p> }>
<ul>{albums}</ul>
</Suspense>
}
.into_any()
}
}
// When two functions have shared code, that shared code will be split out automatically
// into an additional file. For example, the shared serde code here will be split into a single file,
// and then loaded lazily once when the first of the two functions is called
#[lazy]
pub fn first_value() -> String {
#[derive(Serialize)]
struct FirstValue {
a: String,
b: i32,
}
serde_json::to_string(&FirstValue {
a: "First Value".into(),
b: 1,
})
.unwrap()
}
#[lazy]
pub fn second_value() -> String {
#[derive(Serialize)]
struct SecondValue {
a: String,
b: i32,
}
serde_json::to_string(&SecondValue {
a: "Second Value".into(),
b: 2,
})
.unwrap()
}
struct ViewD {
data: Resource<Result<Vec<i32>, ServerFnError>>,
}
#[lazy_route]
impl LazyRoute for ViewD {
fn data() -> Self {
Self {
data: Resource::new(|| (), |_| d_data()),
}
}
fn view(this: Self) -> AnyView {
let items = move || {
Suspend::new(async move {
this.data
.await
.unwrap_or_default()
.into_iter()
.map(|item| view! { <li>{item}</li> })
.collect::<Vec<_>>()
})
};
view! {
<p id="page">"View D"</p>
<hr/>
<Suspense fallback=|| view! { <p id="loading">"Loading..."</p> }>
<ul>{items}</ul>
</Suspense>
<Outlet/>
}
.into_any()
}
}
// Server functions can be made lazy by combining the two macros,
// with `#[server]` coming first, then `#[lazy]`
#[server]
#[lazy]
async fn d_data() -> Result<Vec<i32>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(vec![1, 1, 2, 3, 5, 8, 13])
}
struct ViewE {
data: Resource<Result<Vec<String>, ServerFnError>>,
}
#[lazy_route]
impl LazyRoute for ViewE {
fn data() -> Self {
Self {
data: Resource::new(|| (), |_| e_data()),
}
}
fn view(this: Self) -> AnyView {
let items = move || {
Suspend::new(async move {
this.data
.await
.unwrap_or_default()
.into_iter()
.map(|item| view! { <li>{item}</li> })
.collect::<Vec<_>>()
})
};
view! {
<p id="page">"View E"</p>
<hr/>
<Suspense fallback=|| view! { <p id="loading">"Loading..."</p> }>
<ul>{items}</ul>
</Suspense>
}
.into_any()
}
}
#[server]
async fn e_data() -> Result<Vec<String>, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(vec!["foo".into(), "bar".into(), "baz".into()])
}

View File

@@ -1,9 +0,0 @@
pub mod app;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_lazy(App);
}

View File

@@ -1,37 +0,0 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use lazy_routes::app::{shell, App};
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
println!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -1,3 +0,0 @@
body {
font-family: sans-serif;
}

View File

@@ -1,90 +0,0 @@
[package]
name = "regression"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
axum = { version = "0.8.1", optional = true }
console_error_panic_hook = "0.1.7"
console_log = "1.0"
leptos = { path = "../../leptos", features = ["tracing"] }
leptos_meta = { path = "../../meta" }
leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
tokio = { version = "1.39", features = [ "rt-multi-thread", "macros", "time" ], optional = true }
wasm-bindgen = "0.2.92"
[features]
hydrate = [
"leptos/hydrate",
]
ssr = [
"dep:axum",
"dep:tokio",
"leptos/ssr",
"leptos_meta/ssr",
"dep:leptos_axum",
"leptos_router/ssr",
]
[profile.release]
panic = "abort"
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.cargo-all-features]
denylist = ["axum", "tower", "tower-http", "tokio", "sqlx", "leptos_axum"]
skip_feature_sets = [["ssr", "hydrate"]]
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "regression"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "cargo make test-ui"
end2end-dir = "e2e"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head
watch = false
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 Leptos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,8 +0,0 @@
extend = [
{ path = "../cargo-make/main.toml" },
{ path = "../cargo-make/cargo-leptos-webdriver-test.toml" },
]
[env]
CLIENT_PROCESS_NAME = "regression"

View File

@@ -1,8 +0,0 @@
# Regression Tests
This example functions as a catch-all for all current and future regression
test cases that typically happens at integration.
## Quick Start
Run `cargo leptos watch` to run this example.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,18 +0,0 @@
[package]
name = "regression_e2e"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
anyhow = "1.0"
async-trait = "0.1.81"
cucumber = "0.21.1"
fantoccini = "0.21.1"
pretty_assertions = "1.4"
serde_json = "1.0"
tokio = { version = "1.39", features = ["macros", "rt-multi-thread", "time"] }
url = "2.5"
[[test]]
name = "app_suite"
harness = false # Allow Cucumber to print output instead of libtest

View File

@@ -1,20 +0,0 @@
extend = { path = "../../cargo-make/main.toml" }
[tasks.test]
env = { RUN_AUTOMATICALLY = false }
condition = { env_true = ["RUN_AUTOMATICALLY"] }
[tasks.ci]
[tasks.test-ui]
command = "cargo"
args = [
"test",
"--test",
"app_suite",
"--",
"--retry",
"2",
"--fail-fast",
"${@}",
]

View File

@@ -1,34 +0,0 @@
# E2E Testing
This example demonstrates e2e testing with Rust using executable requirements.
## Testing Stack
| | Role | Description |
|---|---|---|
| [Cucumber](https://github.com/cucumber-rs/cucumber/tree/main) | Test Runner | Run [Gherkin](https://cucumber.io/docs/gherkin/reference/) specifications as Rust tests |
| [Fantoccini](https://github.com/jonhoo/fantoccini/tree/main) | Browser Client | Interact with web pages through WebDriver |
| [Cargo Leptos](https://github.com/leptos-rs/cargo-leptos) | Build Tool | Compile example and start the server and end-2-end tests |
| [chromedriver](https://chromedriver.chromium.org/downloads) | WebDriver | Provide WebDriver for Chrome |
## Testing Organization
Testing is organized around what a user can do and see/not see. Test scenarios are grouped by the **user action** and the **object** of that action. This makes it easier to locate and reason about requirements.
Here is a brief overview of how things fit together.
```bash
features
└── {action}_{object}.feature # Specify test scenarios
tests
├── fixtures
│ ├── action.rs # Perform a user action (click, type, etc.)
│ ├── check.rs # Assert what a user can see/not see
│ ├── find.rs # Query page elements
│ ├── mod.rs
│ └── world
│ ├── action_steps.rs # Map Gherkin steps to user actions
│ ├── check_steps.rs # Map Gherkin steps to user expectations
│ └── mod.rs
└── app_suite.rs # Test main
```

View File

@@ -1,7 +0,0 @@
@check_issue_4005
Feature: Check that issue 4005 does not reappear
Scenario: The second item is selected.
Given I see the app
And I can access regression test 4005
Then I see the value of select is 2

View File

@@ -1,20 +0,0 @@
@check_issue_4088
Feature: Check that issue 4088 does not reappear
Scenario: I can see the navbar
Given I see the app
And I can access regression test 4088
Then I see the navbar
Scenario: The user info is shared via context
Given I see the app
And I can access regression test 4088
When I select the link Class 1
Then I see the result is the string Assignments for team of user with id 42
Scenario: The user info is shared via context
Given I see the app
And I can access regression test 4088
When I select the link Class 1
When I refresh the browser
Then I see the result is the string Assignments for team of user with id 42

View File

@@ -1,9 +0,0 @@
@check_issue_4217
Feature: Check that issue 4217 does not reappear
Scenario: All items are selected.
Given I see the app
And I can access regression test 4217
Then I see option1 is selected
And I see option2 is selected
And I see option3 is selected

View File

@@ -1,9 +0,0 @@
@check_issue_4285
Feature: Check that issue 4285 does not reappear
Scenario: Navigating several times to same lazy route does not cause issues.
Given I see the app
And I can access regression test 4285
And I can access regression test 4285
And I can access regression test 4285
Then I see the result is the string 42

View File

@@ -1,18 +0,0 @@
@check_issue_4296
Feature: Check that issue 4296 does not reappear
Scenario: Query param signals created in LazyRoute::data() are reactive in ::view().
Given I see the app
And I can access regression test 4296
Then I see the result is the string None
When I select the link abc
Then I see the result is the string Some("abc")
When I select the link def
Then I see the result is the string Some("def")
Scenario: Loading page with query signal works as well.
Given I see the app
And I can access regression test 4296
When I select the link abc
When I reload the page
Then I see the result is the string Some("abc")

View File

@@ -1,8 +0,0 @@
@check_pr_4015
Feature: Check that PR 4015 does not regress
Scenario: The correct text appears
Given I see the app
And I can access regression test 4015
Then I see the result is the string Some(42)

View File

@@ -1,48 +0,0 @@
@check_pr_4091
Feature: Regression from pull request 4091
Scenario: Signal for testing should work
Given I see the app
And I can access regression test 4091
When I select the link test1
Then I see the result is the string Test1
Scenario: The result returns to empty due to on_cleanup
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| 4091 Home |
Then I see the result is empty
Scenario: The result does not accumulate due to on_cleanup
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| 4091 Home |
| test1 |
| 4091 Home |
Then I see the result is empty
Scenario: I can see the navbar
Given I see the app
And I can access regression test 4091
Then I see the navbar
Scenario: If I navigate to home and back, I can still see the navbar
Given I see the app
And I can access regression test 4091
When I select the following links
| Home |
| 4091 |
Then I see the navbar
Scenario: The signal is not disposed too early
Given I see the app
And I can access regression test 4091
When I select the following links
| test1 |
| Home |
| 4091 |
Then I see the navbar

View File

@@ -1,30 +0,0 @@
mod fixtures;
use anyhow::Result;
use cucumber::World;
use fixtures::world::AppWorld;
use std::{ffi::OsStr, fs::read_dir};
#[tokio::main]
async fn main() -> Result<()> {
// Normally the below is done, but it's now gotten to the point of
// having a sufficient number of tests where the resource contention
// of the concurrently running browsers will cause failures on CI.
// AppWorld::cucumber()
// .fail_on_skipped()
// .run_and_exit("./features")
// .await;
// Mitigate the issue by manually stepping through each feature,
// rather than letting cucumber glob them and dispatch all at once.
for entry in read_dir("./features")? {
let path = entry?.path();
if path.extension() == Some(OsStr::new("feature")) {
AppWorld::cucumber()
.fail_on_skipped()
.run_and_exit(path)
.await;
}
}
Ok(())
}

View File

@@ -1,17 +0,0 @@
use super::{find, world::HOST};
use anyhow::Result;
use fantoccini::Client;
use std::result::Result::Ok;
pub async fn goto_path(client: &Client, path: &str) -> Result<()> {
let url = format!("{}{}", HOST, path);
client.goto(&url).await?;
Ok(())
}
pub async fn click_link(client: &Client, text: &str) -> Result<()> {
let link = find::link_with_text(&client, &text).await?;
link.click().await?;
Ok(())
}

View File

@@ -1,45 +0,0 @@
use crate::fixtures::find;
use anyhow::{Ok, Result};
use fantoccini::Client;
use pretty_assertions::assert_eq;
pub async fn result_text_is(
client: &Client,
expected_text: &str,
) -> Result<()> {
let actual = find::text_at_id(client, "result").await?;
assert_eq!(&actual, expected_text);
Ok(())
}
pub async fn element_exists(client: &Client, id: &str) -> Result<()> {
find::element_by_id(client, id)
.await
.expect(&format!("could not find element with id `{id}`"));
Ok(())
}
pub async fn select_option_is_selected(
client: &Client,
id: &str,
) -> Result<()> {
let el = find::element_by_id(client, id)
.await
.expect(&format!("could not find element with id `{id}`"));
let selected = el.prop("selected").await?;
assert_eq!(selected.as_deref(), Some("true"));
Ok(())
}
pub async fn element_value_is(
client: &Client,
id: &str,
expected: &str,
) -> Result<()> {
let el = find::element_by_id(client, id)
.await
.expect(&format!("could not find element with id `{id}`"));
let value = el.prop("value").await?;
assert_eq!(value.as_deref(), Some(expected));
Ok(())
}

View File

@@ -1,23 +0,0 @@
use anyhow::{Ok, Result};
use fantoccini::{elements::Element, Client, Locator};
pub async fn text_at_id(client: &Client, id: &str) -> Result<String> {
let element = element_by_id(client, id)
.await
.expect(format!("no such element with id `{}`", id).as_str());
let text = element.text().await?;
Ok(text)
}
pub async fn link_with_text(client: &Client, text: &str) -> Result<Element> {
let link = client
.wait()
.for_element(Locator::LinkText(text))
.await
.expect(format!("Link not found by `{}`", text).as_str());
Ok(link)
}
pub async fn element_by_id(client: &Client, id: &str) -> Result<Element> {
Ok(client.wait().for_element(Locator::Id(id)).await?)
}

View File

@@ -1,4 +0,0 @@
pub mod action;
pub mod check;
pub mod find;
pub mod world;

View File

@@ -1,47 +0,0 @@
use crate::fixtures::{action, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::{gherkin::Step, given, when};
#[given("I see the app")]
#[when("I open the app")]
async fn i_open_the_app(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
action::goto_path(client, "").await?;
Ok(())
}
#[given(regex = "^I can access regression test (.*)$")]
#[when(regex = "^I select the link (.*)$")]
async fn i_select_the_link(world: &mut AppWorld, text: String) -> Result<()> {
let client = &world.client;
action::click_link(client, &text).await?;
Ok(())
}
#[given(expr = "I select the following links")]
#[when(expr = "I select the following links")]
async fn i_select_the_following_links(
world: &mut AppWorld,
step: &Step,
) -> Result<()> {
let client = &world.client;
if let Some(table) = step.table.as_ref() {
for row in table.rows.iter() {
action::click_link(client, &row[0]).await?;
}
}
Ok(())
}
#[given(regex = "^I (refresh|reload) the (browser|page)$")]
#[when(regex = "^I (refresh|reload) the (browser|page)$")]
async fn i_refresh_the_browser(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
client.refresh().await?;
Ok(())
}

View File

@@ -1,45 +0,0 @@
use crate::fixtures::{check, world::AppWorld};
use anyhow::{Ok, Result};
use cucumber::then;
#[then(regex = r"^I see the result is empty$")]
async fn i_see_the_result_is_empty(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::result_text_is(client, "").await?;
Ok(())
}
#[then(regex = r"^I see the result is the string (.*)$")]
async fn i_see_the_result_is_the_string(
world: &mut AppWorld,
text: String,
) -> Result<()> {
let client = &world.client;
check::result_text_is(client, &text).await?;
Ok(())
}
#[then(regex = r"^I see the navbar$")]
async fn i_see_the_navbar(world: &mut AppWorld) -> Result<()> {
let client = &world.client;
check::element_exists(client, "nav").await?;
Ok(())
}
#[then(regex = r"^I see ([\d\w]+) is selected$")]
async fn i_see_the_select(world: &mut AppWorld, id: String) -> Result<()> {
let client = &world.client;
check::select_option_is_selected(client, &id).await?;
Ok(())
}
#[then(regex = r"^I see the value of (\w+) is (.*)$")]
async fn i_see_the_value(
world: &mut AppWorld,
id: String,
value: String,
) -> Result<()> {
let client = &world.client;
check::element_value_is(client, &id, &value).await?;
Ok(())
}

View File

@@ -1,39 +0,0 @@
pub mod action_steps;
pub mod check_steps;
use anyhow::Result;
use cucumber::World;
use fantoccini::{
error::NewSessionError, wd::Capabilities, Client, ClientBuilder,
};
pub const HOST: &str = "http://127.0.0.1:3000";
#[derive(Debug, World)]
#[world(init = Self::new)]
pub struct AppWorld {
pub client: Client,
}
impl AppWorld {
async fn new() -> Result<Self, anyhow::Error> {
let webdriver_client = build_client().await?;
Ok(Self {
client: webdriver_client,
})
}
}
async fn build_client() -> Result<Client, NewSessionError> {
let mut cap = Capabilities::new();
let arg = serde_json::from_str("{\"args\": [\"-headless\"]}").unwrap();
cap.insert("goog:chromeOptions".to_string(), arg);
let client = ClientBuilder::native()
.capabilities(cap)
.connect("http://localhost:4444")
.await?;
Ok(client)
}

View File

@@ -1,79 +0,0 @@
use crate::{
issue_4005::Routes4005, issue_4088::Routes4088, issue_4217::Routes4217,
issue_4285::Routes4285, issue_4296::Routes4296, pr_4015::Routes4015,
pr_4091::Routes4091,
};
use leptos::prelude::*;
use leptos_meta::{MetaTags, *};
use leptos_router::{
components::{Route, Router, Routes},
path,
};
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone()/>
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
provide_meta_context();
let fallback = || view! { "Page not found." }.into_view();
let (_, set_is_routing) = signal(false);
view! {
<Stylesheet id="leptos" href="/pkg/regression.css"/>
<Router set_is_routing>
<main>
<Routes fallback>
<Route path=path!("") view=HomePage/>
<Routes4091/>
<Routes4015/>
<Routes4088/>
<Routes4217/>
<Routes4005/>
<Routes4285/>
<Routes4296/>
</Routes>
</main>
</Router>
}
}
#[server]
async fn server_call() -> Result<(), ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
Ok(())
}
#[component]
fn HomePage() -> impl IntoView {
view! {
<Title text="Regression Tests"/>
<h1>"Listing of regression tests"</h1>
<nav>
<ul>
<li><a href="/4091/">"4091"</a></li>
<li><a href="/4015/">"4015"</a></li>
<li><a href="/4088/">"4088"</a></li>
<li><a href="/4217/">"4217"</a></li>
<li><a href="/4005/">"4005"</a></li>
<li><a href="/4285/">"4285"</a></li>
<li><a href="/4296/">"4296"</a></li>
</ul>
</nav>
}
}

View File

@@ -1,24 +0,0 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4005() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4005") view=Issue4005/>
}
.into_inner()
}
#[component]
fn Issue4005() -> impl IntoView {
view! {
<select id="select" prop:value="2">
<option value="1">"Option 1"</option>
<option value="2">"Option 2"</option>
<option value="3">"Option 3"</option>
</select>
}
}

View File

@@ -1,119 +0,0 @@
use leptos::{either::Either, prelude::*};
#[allow(unused_imports)]
use leptos_router::{
components::{Outlet, ParentRoute, Redirect, Route},
path, MatchNestedRoutes, NavigateOptions,
};
use serde::{Deserialize, Serialize};
#[component]
pub fn Routes4088() -> impl MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4088") view=|| view!{ <LoggedIn/> }>
<ParentRoute path=path!("") view=||view!{<AssignmentsSelector/>}>
<Route path=path!("/:team_id") view=||view!{<AssignmentsForTeam/>} />
<Route path=path!("") view=||view!{ <p>No class selected</p> }/>
</ParentRoute>
</ParentRoute>
}
.into_inner()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserInfo {
pub id: usize,
}
#[server]
pub async fn get_user_info() -> Result<Option<UserInfo>, ServerFnError> {
Ok(Some(UserInfo { id: 42 }))
}
#[component]
pub fn LoggedIn() -> impl IntoView {
let user_info_resource =
Resource::new(|| (), move |_| async { get_user_info().await });
view! {
<Transition fallback=move || view!{
"loading"
}
>
{move || {
user_info_resource.get()
.map(|a|
match a {
Ok(Some(a)) => Either::Left(view! {
<LoggedInContent user_info={a} />
}),
_ => Either::Right(view!{
<Redirect path="/not_logged_in"/>
})
})
}}
</Transition>
}
}
#[component]
/// Component which provides UserInfo and renders it's child
/// Can also contain some code to check for specific situations (e.g. privacy policies accepted or not? redirect if needed...)
pub fn LoggedInContent(user_info: UserInfo) -> impl IntoView {
provide_context(user_info.clone());
if user_info.id == 42 {
Either::Left(Outlet())
} else {
Either::Right(
view! { <Redirect path="/somewhere" options={NavigateOptions::default()}/> },
)
}
}
#[component]
/// This component also uses Outlet (so nested Outlet)
fn AssignmentsSelector() -> impl IntoView {
let user_info = use_context::<UserInfo>().expect("user info not provided");
view! {
<p>"Assignments for user with ID: "{user_info.id}</p>
<ul id="nav">
<li><a href="/4088/1">"Class 1"</a></li>
<li><a href="/4088/2">"Class 2"</a></li>
<li><a href="/4088/3">"Class 3"</a></li>
</ul>
<Outlet />
}
}
#[component]
fn AssignmentsForTeam() -> impl IntoView {
// THIS FAILS -> Because of the nested outlet in LoggedInContent > AssignmentsSelector?
// It did not fail when LoggedIn did not use a resource and transition (but a hardcoded UserInfo in the component)
let user_info = use_context::<UserInfo>().expect("user info not provided");
let items = vec!["Assignment 1", "Assignment 2", "Assignment 3"];
view! {
<p id="result">"Assignments for team of user with id " {user_info.id}</p>
<ul>
{
items.into_iter().map(|item| {
view! {
<Assignment name=item.to_string() />
}
}).collect_view()
}
</ul>
}
}
#[component]
fn Assignment(name: String) -> impl IntoView {
let user_info = use_context::<UserInfo>().expect("user info not provided");
view! {
<li>{name}" "{user_info.id}</li>
}
}

View File

@@ -1,24 +0,0 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4217() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4217") view=Issue4217/>
}
.into_inner()
}
#[component]
fn Issue4217() -> impl IntoView {
view! {
<select multiple=true>
<option id="option1" value="1" selected>"Option 1"</option>
<option id="option2" value="2" selected>"Option 2"</option>
<option id="option3" value="3" selected>"Option 3"</option>
</select>
}
}

View File

@@ -1,49 +0,0 @@
use leptos::prelude::*;
use leptos_router::LazyRoute;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
};
#[component]
pub fn Routes4285() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4285") view={Lazy::<Issue4285>::new()}/>
}
.into_inner()
}
struct Issue4285 {
data: Resource<Result<i32, ServerFnError>>,
}
impl LazyRoute for Issue4285 {
fn data() -> Self {
Self {
data: Resource::new(|| (), |_| slow_call()),
}
}
async fn view(this: Self) -> AnyView {
let Issue4285 { data } = this;
view! {
<Suspense>
{move || {
Suspend::new(async move {
let data = data.await;
view! {
<p id="result">{data}</p>
}
})
}}
</Suspense>
}
.into_any()
}
}
#[server]
async fn slow_call() -> Result<i32, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(42)
}

View File

@@ -1,36 +0,0 @@
use leptos::prelude::*;
#[allow(unused_imports)]
use leptos_router::{
components::Route, path, Lazy, MatchNestedRoutes, NavigateOptions,
};
use leptos_router::{hooks::use_query_map, LazyRoute};
#[component]
pub fn Routes4296() -> impl MatchNestedRoutes + Clone {
view! {
<Route path=path!("4296") view={Lazy::<Issue4296>::new()}/>
}
.into_inner()
}
struct Issue4296 {
query: Signal<Option<String>>,
}
impl LazyRoute for Issue4296 {
fn data() -> Self {
let query = use_query_map();
let query = Signal::derive(move || query.read().get("q"));
Self { query }
}
async fn view(this: Self) -> AnyView {
let Issue4296 { query } = this;
view! {
<a href="?q=abc">"abc"</a>
<a href="?q=def">"def"</a>
<p id="result">{move || format!("{:?}", query.get())}</p>
}
.into_any()
}
}

View File

@@ -1,16 +0,0 @@
pub mod app;
mod issue_4005;
mod issue_4088;
mod issue_4217;
mod issue_4285;
mod issue_4296;
mod pr_4015;
mod pr_4091;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}

View File

@@ -1,37 +0,0 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use regression::app::{shell, App};
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
// Generate the list of routes in your Leptos App
let routes = generate_route_list(App);
let app = Router::new()
.leptos_routes(&leptos_options, routes, {
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
})
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
println!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View File

@@ -1,29 +0,0 @@
use leptos::{context::Provider, prelude::*};
use leptos_router::{
components::{ParentRoute, Route},
nested_router::Outlet,
path,
};
#[component]
pub fn Routes4015() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4015") view=|| view! {
<Provider value=42i32>
<Outlet/>
</Provider>
}>
<Route path=path!("") view=Child/>
</ParentRoute>
}
.into_inner()
}
#[component]
fn Child() -> impl IntoView {
let value = use_context::<i32>();
view! {
<p id="result">{format!("{value:?}")}</p>
}
}

View File

@@ -1,68 +0,0 @@
use leptos::{context::Provider, prelude::*};
use leptos_router::{
components::{ParentRoute, Route, A},
nested_router::Outlet,
path,
};
// FIXME This should be a set rather than a naive vec for push and pop, as
// it may be possible for unexpected token be popped/pushed on multi-level
// navigation. For basic naive tests it should be Fine(TM).
#[derive(Clone)]
struct Expectations(Vec<&'static str>);
#[component]
pub fn Routes4091() -> impl leptos_router::MatchNestedRoutes + Clone {
view! {
<ParentRoute path=path!("4091") view=Container>
<Route path=path!("") view=Root/>
<Route path=path!("test1") view=Test1/>
</ParentRoute>
}
.into_inner()
}
#[component]
fn Container() -> impl IntoView {
let rw_signal = RwSignal::new(Expectations(Vec::new()));
provide_context(rw_signal);
view! {
<nav id="nav">
<ul>
<li><A href="/">"Home"</A></li>
<li><A href="./">"4091 Home"</A></li>
<li><A href="test1">"test1"</A></li>
</ul>
</nav>
<div id="result">{move || {
rw_signal.with(|ex| ex.0.iter().fold(String::new(), |a, b| a + b + " "))
}}</div>
<Provider value=rw_signal>
<Outlet/>
</Provider>
}
}
#[component]
fn Root() -> impl IntoView {
view! {
<div>"This is Root"</div>
}
}
#[component]
fn Test1() -> impl IntoView {
let signal = expect_context::<RwSignal<Expectations>>();
on_cleanup(move || {
signal.update(|ex| {
ex.0.pop();
});
});
view! {
{move || signal.update(|ex| ex.0.push("Test1"))}
<div>"This is Test1"</div>
}
}

View File

@@ -1,3 +0,0 @@
body {
font-family: sans-serif;
}

View File

@@ -29,7 +29,7 @@ tower-http = { version = "0.6.2", features = [
"trace",
], optional = true }
tokio = { version = "1.39", features = ["full"], optional = true }
thiserror = "2.0.12"
thiserror = "2.0.11"
wasm-bindgen = "0.2.93"
serde_toml = "0.0.1"
toml = "0.8.19"
@@ -38,6 +38,7 @@ strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
notify = { version = "8.0", optional = true }
pin-project-lite = "0.2.14"
dashmap = { version = "6.0", optional = true }
once_cell = { version = "1.19", optional = true }
async-broadcast = { version = "0.7.1", optional = true }
bytecheck = "0.8.0"
rkyv = { version = "0.8.8" }
@@ -53,6 +54,7 @@ ssr = [
"dep:leptos_axum",
"dep:notify",
"dep:dashmap",
"dep:once_cell",
"dep:async-broadcast",
]

View File

@@ -424,7 +424,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
use async_broadcast::{broadcast, Receiver, Sender};
use dashmap::DashMap;
use futures::Stream;
use std::sync::LazyLock;
use once_cell::sync::Lazy;
struct File {
total: usize,
@@ -432,8 +432,7 @@ pub fn FileUploadWithProgress() -> impl IntoView {
rx: Receiver<usize>,
}
static FILES: LazyLock<DashMap<String, File>> =
LazyLock::new(DashMap::new);
static FILES: Lazy<DashMap<String, File>> = Lazy::new(DashMap::new);
pub async fn add_chunk(filename: &str, len: usize) {
println!("[{filename}]\tadding {len}");
@@ -946,7 +945,9 @@ pub fn CustomClientExample() -> impl IntoView {
Item = Result<server_fn::Bytes, server_fn::Bytes>,
> + Send
+ 'static,
impl Sink<server_fn::Bytes> + Send + 'static,
impl Sink<Result<server_fn::Bytes, server_fn::Bytes>>
+ Send
+ 'static,
),
E,
>,

View File

@@ -2,6 +2,8 @@
name = "ssr_modes"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -17,7 +19,7 @@ leptos_actix = { path = "../../integrations/actix", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
thiserror = "1.0"
tokio = { version = "1.39", features = ["time"] }
wasm-bindgen = "0.2.93"

View File

@@ -2,6 +2,8 @@
name = "ssr_modes_axum"
version = "0.1.0"
edition = "2021"
# std::sync::LazyLock is stabilized in Rust version 1.80.0
rust-version = "1.80.0"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -17,7 +19,7 @@ leptos_axum = { path = "../../integrations/axum", optional = true }
leptos_router = { path = "../../router" }
log = "0.4.22"
serde = { version = "1.0", features = ["derive"] }
thiserror = "2.0.12"
thiserror = "1.0"
axum = { version = "0.8.1", optional = true }
tower = { version = "0.4.13", optional = true }
tower-http = { version = "0.5.2", features = ["fs"], optional = true }

Some files were not shown because too many files have changed in this diff Show More