Compare commits

..

12 Commits
3872 ... 4088

Author SHA1 Message Date
Greg Johnston
54764261b1 fix memory leak! 2025-06-25 10:02:41 -04:00
Greg Johnston
acd8fd52c3 fix ownership for root 2025-06-24 22:01:26 -04:00
Greg Johnston
51bd63a91a fix context providers (for <A> etc) 2025-06-23 21:50:21 -04:00
Greg Johnston
1d34ea7133 Merge remote-tracking branch 'origin' into 4088 2025-06-23 20:05:40 -04:00
Greg Johnston
a8894460e2 Merge remote-tracking branch 'origin' into 4088 2025-06-20 20:40:11 -04:00
Greg Johnston
403d6ed84e chore: clippy 2025-06-20 17:42:56 -04:00
Greg Johnston
b37900ec55 update rebuild 2025-06-19 14:40:19 -04:00
Greg Johnston
fda2a1348c fix overwritten Outlet 2025-06-19 14:23:08 -04:00
Greg Johnston
c2ea59ca96 correct top-level Outlet rendering again 2025-06-18 15:38:09 -04:00
Greg Johnston
7af2a31e21 waypoint 2025-06-18 15:24:03 -04:00
Greg Johnston
0e45694c6e correctly render top-level Outlet once 2025-06-17 19:44:42 -04:00
Greg Johnston
ca5c179cb1 fix: correctly provide context via nested outlets (closes #4088) 2025-06-16 21:12:42 -04:00
25 changed files with 95 additions and 663 deletions

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 = "1.0"
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,26 +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

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,13 +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(())
}

View File

@@ -1,21 +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 = client
.wait()
.for_element(Locator::Id(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)
}

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,22 +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(())
}

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,61 +0,0 @@
use crate::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();
view! {
<Stylesheet id="leptos" href="/pkg/regression.css"/>
<Router>
<main>
<Routes fallback>
<Route path=path!("") view=HomePage/>
<Routes4091/>
</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>
</ul>
</nav>
}
}

View File

@@ -1,10 +0,0 @@
pub mod app;
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,67 +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>
<ul>
<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

@@ -111,39 +111,28 @@ where
let on_submit = {
move |ev: SubmitEvent| {
// request_animation_frame here schedules this event handler to run slightly later
// this means that this `submit` handler will run *after* any other `submit` handlers
// that have been added by the user. this is useful because it means that the user can
// add an `on:submit` handler and call `ev.prevent_default()` to prevent the form submission
//
// without this delay, this handler will always run before the user's handler (which was added
// later), which means the user can't prevent the form submission in the same way
//
// see https://github.com/leptos-rs/leptos/issues/3872
request_animation_frame(move || {
if ev.default_prevented() {
return;
}
if ev.default_prevented() {
return;
}
ev.prevent_default();
ev.prevent_default();
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
}
Err(err) => {
crate::logging::error!(
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error())));
version.update(|n| *n += 1);
}
match ServFn::from_event(&ev) {
Ok(new_input) => {
action.dispatch(new_input);
}
});
Err(err) => {
crate::logging::error!(
"Error converting form field into server function \
arguments: {err:?}"
);
value.set(Some(Err(ServerFnErrorErr::Serialization(
err.to_string(),
)
.into_app_error())));
version.update(|n| *n += 1);
}
}
}
};

View File

@@ -167,6 +167,7 @@ impl Owner {
.map(|parent| parent.read().or_poisoned().arena.clone())
.unwrap_or_default(),
paused: false,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -201,6 +202,7 @@ impl Owner {
#[cfg(feature = "sandboxed-arenas")]
arena: Default::default(),
paused: false,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context,
@@ -226,6 +228,7 @@ impl Owner {
#[cfg(feature = "sandboxed-arenas")]
arena,
paused,
joined_owners: Vec::new(),
})),
#[cfg(feature = "hydration")]
shared_context: self.shared_context.clone(),
@@ -461,6 +464,7 @@ pub(crate) struct OwnerInner {
#[cfg(feature = "sandboxed-arenas")]
arena: Arc<RwLock<ArenaMap>>,
paused: bool,
joined_owners: Vec<WeakOwner>,
}
impl Debug for OwnerInner {

View File

@@ -6,6 +6,15 @@ use std::{
};
impl Owner {
#[doc(hidden)]
pub fn join_contexts(&self, other: &Owner) {
self.inner
.write()
.or_poisoned()
.joined_owners
.push(other.downgrade());
}
fn provide_context<T: Send + Sync + 'static>(&self, value: T) {
self.inner
.write()
@@ -25,18 +34,27 @@ impl Owner {
if let Some(context) = contexts.remove(&ty) {
context.downcast::<T>().ok().map(|n| *n)
} else {
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.remove(&ty);
let downcast =
value.and_then(|context| context.downcast::<T>().ok());
if let Some(value) = downcast {
return Some(*value);
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.flat_map(|owner| owner.upgrade().map(|owner| owner.inner));
for parent in parent.into_iter().chain(joined) {
let mut parent = Some(parent);
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.remove(&ty);
let downcast =
value.and_then(|context| context.downcast::<T>().ok());
if let Some(value) = downcast {
return Some(*value);
} else {
parent = this_parent
.parent
.as_ref()
.and_then(|p| p.upgrade());
}
}
}
None
@@ -53,21 +71,29 @@ impl Owner {
let reference = if let Some(context) = contexts.get(&ty) {
context.downcast_ref::<T>()
} else {
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
while let Some(ref this_parent) = parent.clone() {
let this_parent = this_parent.read().or_poisoned();
let contexts = &this_parent.contexts;
let value = contexts.get(&ty);
let downcast =
value.and_then(|context| context.downcast_ref::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.flat_map(|owner| owner.upgrade().map(|owner| owner.inner));
for parent in parent.into_iter().chain(joined) {
let mut parent = Some(parent);
while let Some(ref this_parent) = parent.clone() {
let this_parent = this_parent.read().or_poisoned();
let contexts = &this_parent.contexts;
let value = contexts.get(&ty);
let downcast =
value.and_then(|context| context.downcast_ref::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent = this_parent
.parent
.as_ref()
.and_then(|p| p.upgrade());
}
}
}
None
};
reference.map(cb)
@@ -83,18 +109,27 @@ impl Owner {
let reference = if let Some(context) = contexts.get_mut(&ty) {
context.downcast_mut::<T>()
} else {
let mut parent = inner.parent.as_ref().and_then(|p| p.upgrade());
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.get_mut(&ty);
let downcast =
value.and_then(|context| context.downcast_mut::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent =
this_parent.parent.as_ref().and_then(|p| p.upgrade());
let parent = inner.parent.as_ref().and_then(|p| p.upgrade());
let joined = inner
.joined_owners
.iter()
.flat_map(|owner| owner.upgrade().map(|owner| owner.inner));
for parent in parent.into_iter().chain(joined) {
let mut parent = Some(parent);
while let Some(ref this_parent) = parent.clone() {
let mut this_parent = this_parent.write().or_poisoned();
let contexts = &mut this_parent.contexts;
let value = contexts.get_mut(&ty);
let downcast =
value.and_then(|context| context.downcast_mut::<T>());
if let Some(value) = downcast {
return Some(cb(value));
} else {
parent = this_parent
.parent
.as_ref()
.and_then(|p| p.upgrade());
}
}
}
None