Implement closed typed effect path
This commit is contained in:
+42
-54
@@ -1,11 +1,10 @@
|
||||
#[test]
|
||||
fn runtime_abi_version_is_explicit_and_stable() {
|
||||
assert_eq!(hemx_js::RUNTIME_ABI_VERSION, 1); // test req: abi/001
|
||||
assert_eq!(hemx_js::RUNTIME_ABI_VERSION, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_exposes_debug_api_before_startup_side_effects() {
|
||||
// req: runtime/002
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
let api = source
|
||||
@@ -23,7 +22,6 @@ fn runtime_exposes_debug_api_before_startup_side_effects() {
|
||||
|
||||
#[test]
|
||||
fn runtime_reports_http_failures_without_applying_effects() {
|
||||
// req: runtime/005
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function httpError(response)"));
|
||||
@@ -50,7 +48,6 @@ fn runtime_posts_urlencoded_forms_by_default() {
|
||||
|
||||
#[test]
|
||||
fn runtime_turns_page_get_forms_into_url_state_navigation() {
|
||||
// req: page_swap/009 req: page_swap/010
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function pageFormHistoryMode(source, form)"));
|
||||
@@ -68,7 +65,6 @@ fn runtime_turns_page_get_forms_into_url_state_navigation() {
|
||||
|
||||
#[test]
|
||||
fn runtime_preserves_multipart_file_upload_fallback_shape() {
|
||||
// req: multipart/003
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains(
|
||||
@@ -81,7 +77,6 @@ fn runtime_preserves_multipart_file_upload_fallback_shape() {
|
||||
|
||||
#[test]
|
||||
fn runtime_uses_root_scoped_walks_not_dom_selector_apis() {
|
||||
// req: pitch/002 req: runtime/001 req: target_policy/001
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function firstElement(scope, predicate)"));
|
||||
@@ -97,7 +92,6 @@ fn runtime_uses_root_scoped_walks_not_dom_selector_apis() {
|
||||
|
||||
#[test]
|
||||
fn runtime_targets_generated_resources_not_response_selectors() {
|
||||
// req: target_policy/001 req: target_policy/002
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function targetFor(scope, ref)"));
|
||||
@@ -105,22 +99,20 @@ fn runtime_targets_generated_resources_not_response_selectors() {
|
||||
assert!(source.contains("function firstElement(scope, predicate)"));
|
||||
assert!(source.contains("function generatedResource(el, id)"));
|
||||
assert!(source.contains("return firstElement(scope, (el) => attrEquals(el, \"data-key\", key) && withinGeneratedResource(el, scope, id))"));
|
||||
assert!(
|
||||
source.contains("const nodes = fragmentNodes(op.payload, op.key, op.target.resource.id)")
|
||||
);
|
||||
assert!(source.contains("if (op.target.scope && op.target.scope.kind === \"key\" && op.payload.kind === \"html\") replacePayload(target, op.payload, op.target.scope.value, op.target.resource.id)"));
|
||||
assert!(source.contains("function replacePayload(target, payload, key, resourceId)"));
|
||||
assert!(source.contains("if (node.hasAttribute && node.hasAttribute(ROOT)) continue"));
|
||||
assert!(source.contains("if (!applyOp(scope, op)) return"));
|
||||
assert!(!source.contains("canApplyOp"));
|
||||
assert!(source.contains("if (op.position === \"first\") target.prepend"));
|
||||
assert!(source.contains("else if (op.position === \"last\") target.append"));
|
||||
assert!(source.contains("target.replaceWith(...nodes)"));
|
||||
assert!(source.contains("firstElement.setAttribute(\"data-sid\", resourceId)"));
|
||||
assert!(source.contains("const target = generatedTarget(scope, id)"));
|
||||
assert!(source.contains("if (!target) return missing(scope, op.target)"));
|
||||
assert!(source.contains("emit(scope, op.event, op.payload)"));
|
||||
assert!(!source.contains("handleRuntimeEvent"));
|
||||
assert!(!source.contains("data-hemx-target"));
|
||||
assert!(!source.contains("data-hemx-select"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_interval_dispatch_avoids_duplicate_timers() {
|
||||
// req: convention/005
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("const everyTimers = new WeakMap()"));
|
||||
@@ -138,7 +130,6 @@ fn runtime_interval_dispatch_avoids_duplicate_timers() {
|
||||
|
||||
#[test]
|
||||
fn runtime_supports_tiny_delay_and_revealed_scheduling() {
|
||||
// req: convention/001 req: convention/003 req: convention/005
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("data-hemx-delay"));
|
||||
@@ -146,26 +137,24 @@ fn runtime_supports_tiny_delay_and_revealed_scheduling() {
|
||||
assert!(source.contains("const revealObservers = new WeakMap()"));
|
||||
assert!(source.contains("const revealed = new WeakSet()"));
|
||||
assert!(source.contains("data-hemx-revealed"));
|
||||
assert!(source.contains("data-hemx-revealed-ahead")); // req: convention/011 test
|
||||
assert!(source.contains("data-hemx-revealed-ahead"));
|
||||
assert!(source.contains("typeof IntersectionObserver === \"undefined\""));
|
||||
assert!(source.contains("rootMargin"));
|
||||
assert!(source.contains("addEventListener(\"resize\", () => rebindRevealed(false))"));
|
||||
assert!(source.contains("schedule(entry.target, \"revealed\")"));
|
||||
assert!(source.contains("window.addEventListener(\"pageshow\", restoreRevealed)"));
|
||||
assert!(source.contains("revealed.delete(el)")); // req: convention/014 test
|
||||
assert!(source.contains("revealed.delete(el)"));
|
||||
assert!(source.contains("record.addedNodes.forEach((node) =>"));
|
||||
assert!(source.contains("descendantRoots(node).forEach(bindRoot)"));
|
||||
assert!(source.contains("const owner = rootOf(node.parentElement)"));
|
||||
assert!(source.contains("bindPolling(owner)"));
|
||||
assert!(source.contains("bindRevealed(owner)"));
|
||||
assert!(source.contains(
|
||||
"for (const op of batch.ops) applyOp(scope, op);\n bindPolling(scope);\n bindRevealed(scope);"
|
||||
)); // req: runtime/007 test
|
||||
assert!(source.contains("if (!applyOp(scope, op)) return;"));
|
||||
assert!(source.contains("bindPolling(scope);\n bindRevealed(scope);"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_toggles_pending_conventions_around_requests() {
|
||||
// req: convention/007 req: convention/008
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function showPending(el, on)"));
|
||||
@@ -202,7 +191,6 @@ fn runtime_toggles_pending_conventions_around_requests() {
|
||||
|
||||
#[test]
|
||||
fn runtime_confirms_before_handler_dispatch() {
|
||||
// req: convention/004
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("el.getAttribute(\"data-hemx-confirm\") && !confirm(el.getAttribute(\"data-hemx-confirm\"))"));
|
||||
@@ -211,7 +199,6 @@ fn runtime_confirms_before_handler_dispatch() {
|
||||
|
||||
#[test]
|
||||
fn runtime_bounds_and_cancels_ordinary_requests() {
|
||||
// test req: operations/003
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("const REQUEST_TIMEOUT_MS = 10_000"));
|
||||
@@ -225,7 +212,6 @@ fn runtime_bounds_and_cancels_ordinary_requests() {
|
||||
|
||||
#[test]
|
||||
fn runtime_fetches_with_same_origin_credentials() {
|
||||
// req: auth/005
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert_eq!(source.matches("credentials: \"same-origin\"").count(), 2);
|
||||
@@ -242,7 +228,6 @@ fn runtime_handles_get_forms_without_request_body() {
|
||||
|
||||
#[test]
|
||||
fn runtime_clicking_submitter_schedules_form_submit() {
|
||||
// req: convention/004 req: runtime/001
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function formOwner(el)"));
|
||||
@@ -266,7 +251,6 @@ fn runtime_clicking_submitter_schedules_form_submit() {
|
||||
|
||||
#[test]
|
||||
fn runtime_supports_drag_drop_params_without_user_js() {
|
||||
// req: htmx_equivalents/002, req: dx/008
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("const dragKeys = new WeakMap()"));
|
||||
@@ -278,7 +262,6 @@ fn runtime_supports_drag_drop_params_without_user_js() {
|
||||
|
||||
#[test]
|
||||
fn runtime_supports_queued_request_policy() {
|
||||
// req: convention/006
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function normalizedPolicy(value)"));
|
||||
@@ -293,7 +276,6 @@ fn runtime_supports_queued_request_policy() {
|
||||
|
||||
#[test]
|
||||
fn runtime_latest_request_policy_releases_superseded_pending_state() {
|
||||
// req: convention/006 req: convention/007
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("if (active && policy === \"latest\") {\n active.abort.abort();\n showPending(target, false);\n }"));
|
||||
@@ -302,7 +284,6 @@ fn runtime_latest_request_policy_releases_superseded_pending_state() {
|
||||
|
||||
#[test]
|
||||
fn runtime_parallel_request_policy_releases_each_pending_state() {
|
||||
// req: convention/006 req: convention/007 req: convention/008
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains(
|
||||
@@ -322,7 +303,7 @@ fn runtime_exposes_page_swap_hooks() {
|
||||
assert!(source.contains("history.pushState"));
|
||||
assert!(source.contains("snapshotPage(root)"));
|
||||
assert!(source.contains("restorePage(event.state, root)"));
|
||||
assert!(source.contains("scrollTo(state.scrollX, state.scrollY)")); // req: page_swap/005 test
|
||||
assert!(source.contains("scrollTo(state.scrollX, state.scrollY)"));
|
||||
assert!(source.contains("popstate"));
|
||||
assert!(source.contains("x-hemx-title"));
|
||||
assert!(source.contains("x-hemx-fingerprint"));
|
||||
@@ -332,7 +313,6 @@ fn runtime_exposes_page_swap_hooks() {
|
||||
|
||||
#[test]
|
||||
fn runtime_popstate_failed_partials_reload_instead_of_stale_ui() {
|
||||
// req: page_swap/005 req: page_swap/006 req: runtime/004
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("if (mode === \"none\") location.reload();"));
|
||||
@@ -341,7 +321,6 @@ fn runtime_popstate_failed_partials_reload_instead_of_stale_ui() {
|
||||
|
||||
#[test]
|
||||
fn runtime_preserves_native_navigation_escape_hatches() {
|
||||
// req: page_swap/007 req: failure/006
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function sameOriginNav(event, anchor)"));
|
||||
@@ -355,7 +334,6 @@ fn runtime_preserves_native_navigation_escape_hatches() {
|
||||
|
||||
#[test]
|
||||
fn runtime_refuses_partial_updates_on_fingerprint_mismatch() {
|
||||
// req: abi/003 req: abi/004 req: runtime/004
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function compatibleFingerprint(response, root)"));
|
||||
@@ -368,7 +346,6 @@ fn runtime_refuses_partial_updates_on_fingerprint_mismatch() {
|
||||
|
||||
#[test]
|
||||
fn runtime_malformed_bootstrap_state_reports_and_continues() {
|
||||
// req: state/004 req: state/006 req: runtime/001
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function bootstrapState(root)"));
|
||||
@@ -381,7 +358,6 @@ fn runtime_malformed_bootstrap_state_reports_and_continues() {
|
||||
|
||||
#[test]
|
||||
fn runtime_applies_sse_effect_batches_inside_roots() {
|
||||
// req: push/001 req: push/003 req: push/006 req: runtime/001
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("const sseSources = new WeakMap()"));
|
||||
@@ -400,7 +376,6 @@ fn runtime_applies_sse_effect_batches_inside_roots() {
|
||||
|
||||
#[test]
|
||||
fn runtime_applies_binary_websocket_effect_batches_inside_roots() {
|
||||
// req: push/001 req: push/003 req: push/009 req: runtime/001 req: failure/001
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("const webSockets = new WeakMap()"));
|
||||
@@ -417,7 +392,6 @@ fn runtime_applies_binary_websocket_effect_batches_inside_roots() {
|
||||
|
||||
#[test]
|
||||
fn runtime_page_swaps_lowered_slot_ids() {
|
||||
// req: wire/001
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function replaceLoweredSlots(scope, doc)"));
|
||||
@@ -441,21 +415,18 @@ fn runtime_keeps_form_field_targets_separate_from_error_targets() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_preflights_batches_before_applying_ops() {
|
||||
fn runtime_applies_batches_in_order_and_stops_on_failure() {
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains(
|
||||
"const missingTarget = batch.ops.map((op) => canApplyOp(scope, op)).find(Boolean)"
|
||||
));
|
||||
assert!(source.contains("function canApplyOp(scope, op)"));
|
||||
assert!(source.contains("for (const op of batch.ops) applyOp(scope, op)"));
|
||||
assert!(source.contains("for (const op of batch.ops)"));
|
||||
assert!(source.contains("if (!applyOp(scope, op)) return;"));
|
||||
assert!(!source.contains("function canApplyOp(scope, op)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_ships_typescript_definitions() {
|
||||
let source = hemx_js::RUNTIME_D_TS;
|
||||
|
||||
assert!(source.contains("req: ts/001"));
|
||||
assert!(source.contains("export interface EffectBatch"));
|
||||
assert!(source.contains("fingerprint: bigint"));
|
||||
assert!(source.contains("export type Effect ="));
|
||||
@@ -464,14 +435,31 @@ fn runtime_ships_typescript_definitions() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_clears_error_for_elements_on_form_reset() {
|
||||
// req: runtime/005 req: form_effects/003
|
||||
fn runtime_dispatches_payload_without_application_policy() {
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains(r#"if (name === "hemx:form-reset")"#));
|
||||
assert!(source.contains("form.reset();"));
|
||||
assert!(source.contains(r#"if (el.hasAttribute("data-hemx-error-for"))"#));
|
||||
assert!(source.contains("forEachElement(form, (el) => {"));
|
||||
assert!(source.contains("el.setCustomValidity(\"\")"));
|
||||
assert!(source.contains("el.textContent = \"\""));
|
||||
assert!(source.contains("emit(scope, op.event, op.payload)"));
|
||||
assert!(!source.contains("hemx:form-reset"));
|
||||
assert!(!source.contains("handleRuntimeEvent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_executes_every_closed_effect_with_owned_targets() {
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
for kind in [
|
||||
"patch", "insert", "remove", "move", "focus", "scroll", "visit", "dispatch",
|
||||
] {
|
||||
assert!(
|
||||
source.contains(&format!("op.kind === \"{kind}\"")),
|
||||
"missing {kind} executor"
|
||||
);
|
||||
}
|
||||
assert!(source.contains("target.replaceWith(...nodes)"));
|
||||
assert!(source.contains("target.parentNode.prepend(target)"));
|
||||
assert!(source.contains("target.parentNode.append(target)"));
|
||||
assert!(source.contains("target.focus({ preventScroll: op.preventScroll })"));
|
||||
assert!(source.contains("target.scrollIntoView({ block: op.block, behavior: op.behavior })"));
|
||||
assert!(source.contains("navigateUrl(op.url, scope, op.history).catch"));
|
||||
assert!(source.contains("emit(scope, op.event, op.payload)"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user