fix: automatically reconnect after losing a WebSocket connection
Some checks failed
actionlint check / actionlint check (pull_request) Successful in 7s
conventional pull request title check / conventional pull request title check (pull_request) Successful in 3s
conventional commit messages check / conventional commit messages check (pull_request) Successful in 6s
dotenv-linter check / dotenv-linter check (pull_request) Successful in 9s
GitLeaks check / GitLeaks check (pull_request) Successful in 11s
hadolint check / hadolint check (pull_request) Successful in 14s
Prettier check / Prettier check (pull_request) Failing after 35s
markdownlint check / markdownlint check (pull_request) Failing after 40s
htmlhint check / htmlhint check (pull_request) Successful in 44s
checkov check / checkov check (pull_request) Failing after 1m20s
ShellCheck check / ShellCheck check (pull_request) Successful in 23s
Stylelint check / Stylelint check (pull_request) Successful in 24s
yamllint check / yamllint check (pull_request) Successful in 22s
Rust check / Rust check (pull_request) Failing after 57m45s

This commit is contained in:
2026-01-10 12:03:48 +01:00
parent c23397a941
commit 404dca86e1
40 changed files with 2742 additions and 444 deletions

View File

@@ -0,0 +1,13 @@
# Note
//*****************************************************************************
//
// This set of tests takes a heavy handed approach to errors, whereby the
// process is exited. This is done because panic! and assert_eq! failures
// are trapped within `dioxus::runtime::RuntimeGuard`.
// Unfortunately panic! is still made silent.
//
// Errors will be shown with:
// cargo test -- --nocapture
//
//*****************************************************************************

View File

@@ -0,0 +1,3 @@
mod test_hook;
pub(crate) use test_hook::test_hook;

View File

@@ -0,0 +1,85 @@
// Lifted from: https://dioxuslabs.com/learn/0.6/cookbook/testing
//
// Much curtialed functionality and massaged to use in the local testing
// here. This hook isn't intended for reuse.
//
use dioxus::{dioxus_core::NoOpMutations, prelude::*};
use futures::FutureExt;
use std::{cell::RefCell, fmt::Debug, rc::Rc};
pub(crate) fn test_hook<V: 'static>(
initialize: impl FnMut() -> V + 'static,
check: impl FnMut(V, &mut Assertions) + 'static,
) {
#[derive(Props)]
struct MockAppComponent<I: 'static, C: 'static> {
hook: Rc<RefCell<I>>,
check: Rc<RefCell<C>>,
}
impl<I, C> PartialEq for MockAppComponent<I, C> {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl<I, C> Clone for MockAppComponent<I, C> {
fn clone(&self) -> Self {
Self {
hook: self.hook.clone(),
check: self.check.clone(),
}
}
}
fn mock_app<I: FnMut() -> V, C: FnMut(V, &mut Assertions), V>(
props: MockAppComponent<I, C>,
) -> Element {
let value = props.hook.borrow_mut()();
let mut assertions = Assertions::new();
props.check.borrow_mut()(value, &mut assertions);
rsx! { div {} }
}
let mut vdom = VirtualDom::new_with_props(
mock_app,
MockAppComponent {
hook: Rc::new(RefCell::new(initialize)),
check: Rc::new(RefCell::new(check)),
},
);
vdom.rebuild_in_place();
while vdom.wait_for_work().now_or_never().is_some() {
vdom.render_immediate(&mut NoOpMutations);
}
}
#[derive(Debug)]
pub(crate) struct Assertions {}
impl Assertions {
pub fn new() -> Self {
Self {}
}
pub fn assert<T>(&mut self, actual: T, expected: T, id: &str)
where
T: PartialEq + Debug,
{
if actual != expected {
eprintln!(
"***** ERROR in {}: actual: '{:?}' != expected: '{:?}' *****\n",
id, actual, expected
);
std::process::exit(-1);
};
}
}

View File

@@ -0,0 +1,5 @@
fallback = fallback only
language = fallback language
script = fallback script
region = fallback region
variants = fallback variants

View File

@@ -0,0 +1 @@
variants = variants only

View File

@@ -0,0 +1,2 @@
region = region only
variants = region variants

View File

@@ -0,0 +1,3 @@
script = script only
region = script region
variants = script variants

View File

@@ -0,0 +1,4 @@
language = language only
script = language script
region = language region
variants = language variants

View File

@@ -0,0 +1,5 @@
hello = Hello, {$name}!
simple = Hello, Zaphod!
my_component = My Component
.placeholder = Component's placeholder
.hint = Component's hint with parameter {$name}

View File

@@ -0,0 +1,44 @@
mod common;
use common::*;
use dioxus_i18n::{
prelude::{use_init_i18n, I18n, I18nConfig},
t,
};
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn issue_15_recent_change_to_t_macro_unnecessarily_breaks_v0_3_code_test_attr() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
t!(&format!("hello"), name: name)
});
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"translate_from_static_source",
);
});
}
#[test]
fn issue_15_recent_change_to_t_macro_unnecessarily_breaks_v0_3_code_test_no_attr() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| t!(&format!("simple")));
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, Zaphod!".to_string(),
"translate_from_static_source",
);
});
}
const EN: LanguageIdentifier = langid!("en");
fn i18n_from_static() -> I18n {
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
use_init_i18n(|| config)
}

View File

@@ -0,0 +1,99 @@
mod common;
use common::*;
use dioxus_i18n::prelude::{use_init_i18n, I18n, I18nConfig};
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn exact_locale_match_will_use_translation() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("variants")
.expect("test message id must exist"),
"variants only".to_string(),
"exact_locale_match_will_use_translation",
);
});
}
#[test]
fn non_exact_locale_match_will_use_region() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("region")
.expect("test message id must exist"),
"region only".to_string(),
"non_exact_locale_match_will_use_region",
);
});
}
#[test]
fn non_exact_locale_match_will_use_script() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("script")
.expect("test message id must exist"),
"script only".to_string(),
"non_exact_locale_match_will_use_script",
);
});
}
#[test]
fn non_exact_locale_match_will_use_language() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("language")
.expect("test message id must exist"),
"language only".to_string(),
"non_exact_locale_match_will_use_language",
);
});
}
#[test]
fn no_locale_match_will_use_fallback() {
test_hook(i18n, |value, proxy| {
proxy.assert(
value
.try_translate("fallback")
.expect("test message id must exist"),
"fallback only".to_string(),
"no_locale_match_will_use_fallback",
);
});
}
fn i18n() -> I18n {
const FALLBACK_LANG: LanguageIdentifier = langid!("fb-FB");
const LANGUAGE_LANG: LanguageIdentifier = langid!("la");
const SCRIPT_LANG: LanguageIdentifier = langid!("la-Scpt");
const REGION_LANG: LanguageIdentifier = langid!("la-Scpt-LA");
let variants_lang: LanguageIdentifier = langid!("la-Scpt-LA-variants");
let config = I18nConfig::new(variants_lang.clone())
.with_locale((LANGUAGE_LANG, include_str!("../tests/data/fallback/la.ftl")))
.with_locale((
SCRIPT_LANG,
include_str!("../tests/data/fallback/la-Scpt.ftl"),
))
.with_locale((
REGION_LANG,
include_str!("../tests/data/fallback/la-Scpt-LA.ftl"),
))
.with_locale((
variants_lang.clone(),
include_str!("../tests/data/fallback/la-Scpt-LA-variants.ftl"),
))
.with_locale((
FALLBACK_LANG,
include_str!("../tests/data/fallback/fb-FB.ftl"),
))
.with_fallback(FALLBACK_LANG);
use_init_i18n(|| config)
}

View File

@@ -0,0 +1,85 @@
// Test that macros work correctly when re-exported from another module
// This verifies that $crate is used correctly instead of hard-coded dioxus_i18n
mod reexport_module {
// Re-export the macros as if they were from a different crate
pub use dioxus_i18n::{t, te, tid};
}
mod common;
use common::*;
use dioxus_i18n::prelude::{use_init_i18n, I18n, I18nConfig};
use unic_langid::{langid, LanguageIdentifier};
#[test]
fn reexported_t_macro_works() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
reexport_module::t!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "reexported_t_macro_works");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"reexported_t_macro_works",
);
});
}
#[test]
fn reexported_te_macro_works() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
reexport_module::te!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "reexported_te_macro_works");
proxy.assert(
panic.ok().unwrap().ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"reexported_te_macro_works",
);
});
}
#[test]
fn reexported_tid_macro_works() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
reexport_module::tid!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "reexported_tid_macro_works");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"reexported_tid_macro_works",
);
});
}
#[test]
fn reexported_macro_with_invalid_key_as_error() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| reexport_module::te!("invalid"));
proxy.assert(
panic.is_ok(),
true,
"reexported_macro_with_invalid_key_as_error",
);
proxy.assert(
panic.ok().unwrap().err().unwrap().to_string(),
"message id not found for key: 'invalid'".to_string(),
"reexported_macro_with_invalid_key_as_error",
);
});
}
const EN: LanguageIdentifier = langid!("en");
fn i18n_from_static() -> I18n {
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
use_init_i18n(|| config)
}

View File

@@ -0,0 +1,343 @@
mod common;
use common::*;
use dioxus_i18n::{
prelude::{use_init_i18n, I18n, I18nConfig},
t, te, tid,
};
use unic_langid::{langid, LanguageIdentifier};
use std::path::PathBuf;
#[test]
fn translate_from_static_source() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
t!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "translate_from_static_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"translate_from_static_source",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let _ = &t!("invalid");
});
proxy.assert(panic.is_err(), true, "failed_to_translate_with_invalid_key");
});
}
#[test]
fn failed_to_translate_with_invalid_key_as_error() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| te!("invalid"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_as_error",
);
proxy.assert(
panic.ok().unwrap().err().unwrap().to_string(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_as_error",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key_with_args_as_error() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| te!("invalid", name: "<don't care>"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_with_args_as_error",
);
proxy.assert(
panic.ok().unwrap().err().unwrap().to_string(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_with_args_as_error",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key_as_id() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("invalid"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_as_id",
);
proxy.assert(
panic.ok().unwrap(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_as_id",
);
});
}
#[test]
fn failed_to_translate_with_invalid_key_with_args_as_id() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("invalid", name: "<don't care>"));
proxy.assert(
panic.is_ok(),
true,
"failed_to_translate_with_invalid_key_with_args_as_id",
);
proxy.assert(
panic.ok().unwrap(),
"message id not found for key: 'invalid'".to_string(),
"failed_to_translate_with_invalid_key_with_args_as_id",
);
});
}
#[test]
fn translate_root_message_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component"));
proxy.assert(
panic.is_ok(),
true,
"translate_root_message_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"My Component".to_string(),
"translate_root_message_in_attributed_definition",
);
});
}
#[test]
fn translate_attribute_with_no_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.placeholder"));
proxy.assert(
panic.is_ok(),
true,
"translate_attribute_with_no_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"Component's placeholder".to_string(),
"translate_attribute_with_no_args_in_attributed_definition",
);
});
}
#[test]
fn translate_attribute_with_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.hint", name: "Zaphod"));
proxy.assert(
panic.is_ok(),
true,
"translate_attribute_with_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"Component's hint with parameter \u{2068}Zaphod\u{2069}".to_string(),
"translate_attribute_with_args_in_attributed_definition",
);
});
}
#[test]
fn fail_translate_invalid_attribute_with_no_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.not_a_placeholder"));
proxy.assert(
panic.is_ok(),
true,
"fail_translate_invalid_attribute_with_no_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"attribute id not found for key: 'my_component.not_a_placeholder'".to_string(),
"fail_translate_invalid_attribute_with_no_args_in_attributed_definition",
);
});
}
#[test]
fn fail_translate_invalid_attribute_with_args_in_attributed_definition() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.not_a_hint", name: "Zaphod"));
proxy.assert(
panic.is_ok(),
true,
"fail_translate_invalid_attribute_with_args_in_attributed_definition",
);
proxy.assert(
panic.ok().unwrap(),
"attribute id not found for key: 'my_component.not_a_hint'".to_string(),
"fail_translate_invalid_attribute_with_args_in_attributed_definition",
);
});
}
#[test]
fn fail_translate_with_invalid_attribute_key() {
test_hook(i18n_from_static, |_, proxy| {
let panic = std::panic::catch_unwind(|| tid!("my_component.placeholder.invalid"));
proxy.assert(
panic.is_ok(),
true,
"fail_translate_with_invalid_attribute_key",
);
proxy.assert(
panic.ok().unwrap(),
"invalid message id: 'my_component.placeholder.invalid'".to_string(),
"fail_translate_with_invalid_attribute_key",
);
});
}
#[test]
fn translate_from_dynamic_source() {
test_hook(i18n_from_dynamic, |_, proxy| {
let panic = std::panic::catch_unwind(|| {
let name = "World";
t!("hello", name: name)
});
proxy.assert(panic.is_ok(), true, "translate_from_dynamic_source");
proxy.assert(
panic.ok().unwrap(),
"Hello, \u{2068}World\u{2069}!".to_string(),
"translate_from_dynamic_source",
);
});
}
#[test]
#[should_panic]
#[ignore] // Panic hidden within test_hook.
fn fail_translate_from_dynamic_source_when_file_does_not_exist() {
test_hook(i18n_from_dynamic_none_existing, |_, _| unreachable!());
}
#[test]
fn initial_language_is_set() {
test_hook(i18n_from_static, |value, proxy| {
proxy.assert(value.language(), EN, "initial_language_is_set");
});
}
#[test]
fn language_can_be_set() {
test_hook(i18n_from_static, |mut value, proxy| {
value
.try_set_language(JP)
.expect("set_language must succeed");
proxy.assert(value.language(), JP, "language_can_be_set");
});
}
#[test]
fn no_default_fallback_language() {
test_hook(i18n_from_static, |value, proxy| {
proxy.assert(
format!("{:?}", value.fallback_language()),
"None".to_string(),
"no_default_fallback_language",
);
});
}
#[test]
fn some_default_fallback_language() {
test_hook(i18n_from_static_with_fallback, |value, proxy| {
proxy.assert(
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
"Some(\"jp\")".to_string(),
"some_default_fallback_language",
);
});
}
#[test]
fn fallback_language_can_be_set() {
test_hook(i18n_from_static_with_fallback, |mut value, proxy| {
value
.try_set_fallback_language(EN)
.expect("try_set_fallback_language must succeed");
proxy.assert(
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
"Some(\"en\")".to_string(),
"fallback_language_can_be_set",
);
});
}
#[test]
fn fallback_language_must_have_locale_translation() {
test_hook(i18n_from_static_with_fallback, |mut value, proxy| {
let result = value.try_set_fallback_language(IT);
proxy.assert(
result.is_err(),
true,
"fallback_language_must_have_locale_translation",
);
proxy.assert(
result.err().unwrap().to_string(),
"fallback for \"it\" must have locale".to_string(),
"fallback_language_must_have_locale_translation",
);
proxy.assert(
format!("{:?}", value.fallback_language().map(|l| l.to_string())),
"Some(\"jp\")".to_string(),
"fallback_language_must_have_locale_translation",
);
});
}
const EN: LanguageIdentifier = langid!("en");
const IT: LanguageIdentifier = langid!("it");
const JP: LanguageIdentifier = langid!("jp");
fn i18n_from_static() -> I18n {
let config = I18nConfig::new(EN).with_locale((EN, include_str!("./data/i18n/en.ftl")));
use_init_i18n(|| config)
}
fn i18n_from_static_with_fallback() -> I18n {
let config = I18nConfig::new(EN)
.with_locale((EN, include_str!("./data/i18n/en.ftl")))
.with_fallback(JP);
use_init_i18n(|| config)
}
fn i18n_from_dynamic() -> I18n {
let config = I18nConfig::new(EN).with_locale((
EN,
PathBuf::from(format!(
"{}/tests/data/i18n/en.ftl",
env!("CARGO_MANIFEST_DIR")
)),
));
use_init_i18n(|| config)
}
fn i18n_from_dynamic_none_existing() -> I18n {
let config = I18nConfig::new(EN).with_locale((
EN,
PathBuf::from(format!(
"{}/tests/data/i18n/non_existing.ftl",
env!("CARGO_MANIFEST_DIR")
)),
));
use_init_i18n(|| config)
}