Añade gestión de errores catastróficos

- Los errores controlados (403, 404, fatales) se renderizan con el tema
  activo; un `panic!` no controlado se intercepta con un HTML mínimo y
  autónomo, sin depender del tema ni de los componentes.
- Corrige además que `Context::template()` no seguía los cambios de tema
  hechos con `with_theme()` después de crear la página.
- Renombra `DefaultRegion(s)`/`DefaultTemplate(s)`, añade
  `Theme::admin_template()` y `Page::admin()`, y documenta la creación
  de temas e `InRegion` en `core/theme.rs`.
This commit is contained in:
Manuel Cillero 2026-07-13 00:38:11 +02:00
parent 0e72d51276
commit 24bd13414c
12 changed files with 458 additions and 139 deletions

54
tests/error_handling.rs Normal file
View file

@ -0,0 +1,54 @@
use pagetop::prelude::*;
// **< CatchPanicLayer >****************************************************************************
struct PanicExtension;
#[async_trait]
impl Extension for PanicExtension {
fn configure_router(&self, router: Router) -> Router {
router.route("/boom", web::get(boom))
}
}
async fn boom() -> Result<Markup, ErrorPage> {
panic!("boom")
}
#[pagetop::test]
async fn panic_in_handler_returns_minimal_500_page_instead_of_crashing() {
let app = web::test::init_router(Application::prepare(&PanicExtension).await.test());
let req = web::test::TestRequest::get().uri("/boom").to_request();
let resp = web::test::send_request(&app, req).await;
assert_eq!(resp.status(), web::http::StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(
resp.headers().get(web::http::header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8"
);
let body = web::test::read_body_text(resp).await;
assert!(body.contains("An unexpected error has occurred"));
}
// **< ErrorPage::NotFound >************************************************************************
// `EXTENSIONS` es un `OnceLock` global (`core/extension/all.rs`): se inicializa una sola vez por
// binario de test. Todos los tests de este fichero comparten la misma extensión raíz
// (`PanicExtension`) para que el orden de ejecución en paralelo no cambie qué rutas quedan
// registradas.
#[pagetop::test]
async fn unknown_route_returns_themed_404_page() {
let app = web::test::init_router(Application::prepare(&PanicExtension).await.test());
let req = web::test::TestRequest::get()
.uri("/does-not-exist")
.to_request();
let resp = web::test::send_request(&app, req).await;
assert_eq!(resp.status(), web::http::StatusCode::NOT_FOUND);
let body = web::test::read_body_text(resp).await;
assert!(body.contains("<html"));
}

83
tests/theme_template.rs Normal file
View file

@ -0,0 +1,83 @@
use pagetop::prelude::*;
// **< Tema con plantilla propia >******************************************************************
struct MarkerTemplate;
#[async_trait]
impl Template for MarkerTemplate {
async fn render(&self, _cx: &mut Context) -> Markup {
html! { "marker-template-output" }
}
}
struct MarkerTheme;
#[async_trait]
impl Extension for MarkerTheme {
fn theme(&self) -> Option<ThemeRef> {
Some(&Self)
}
}
#[async_trait]
impl Theme for MarkerTheme {
fn default_template(&self) -> TemplateRef {
&MarkerTemplate
}
fn admin_template(&self) -> TemplateRef {
&MarkerTemplate
}
}
async fn render_active_template(cx: &mut Context) -> String {
let template = cx.template();
template.render(cx).await.into_string()
}
// **< Context::template() sigue al tema activo >***************************************************
#[pagetop::test]
async fn with_theme_updates_the_effective_template() {
// Sin cambiar de tema, la plantilla activa no es la de `MarkerTheme`.
let mut cx = Context::new(None);
assert_ne!(
render_active_template(&mut cx).await,
"marker-template-output"
);
// Tras cambiar de tema con `with_theme()`, la plantilla activa pasa a ser la de ese tema, sin
// necesidad de llamar a `with_template()` explícitamente.
let mut cx = Context::new(None).with_theme(&MarkerTheme);
assert_eq!(
render_active_template(&mut cx).await,
"marker-template-output"
);
}
#[pagetop::test]
async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
// Una plantilla fijada explícitamente con `with_template()` prevalece aunque `with_theme()` se
// llame después, en cualquier orden.
let mut cx = Context::new(None)
.with_template(&MarkerTemplate)
.with_theme(&pagetop::base::theme::Basic);
assert_eq!(
render_active_template(&mut cx).await,
"marker-template-output"
);
}
// **< Page::admin() sigue al tema activo >*********************************************************
#[pagetop::test]
async fn page_admin_template_follows_a_later_with_theme() {
let request = web::test::TestRequest::get().to_http_request();
let mut page = Page::admin(request).with_theme(&MarkerTheme);
let markup = page.context().template().render(page.context()).await;
assert_eq!(markup.into_string(), "marker-template-output");
}