01df0bdaa6
Cover multipart runtime request construction so file-upload forms keep FormData, omit File values from URL encoding, and only mark URLSearchParams bodies as urlencoded. req: multipart/003
210 lines
8.7 KiB
Rust
210 lines
8.7 KiB
Rust
#[test]
|
|
fn runtime_posts_urlencoded_forms_by_default() {
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("new URLSearchParams()"));
|
|
assert!(source.contains("form.getAttribute(\"method\")"));
|
|
assert!(source.contains("multipart/form-data"));
|
|
assert!(source.contains("const body = method === \"GET\" || method === \"HEAD\" ? undefined : requestBody(data, multipart)"));
|
|
assert!(source.contains("application/x-www-form-urlencoded;charset=UTF-8"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_preserves_multipart_file_upload_fallback_shape() {
|
|
// req: multipart/003
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("multipart: form && String(form.enctype).toLowerCase() === \"multipart/form-data\""));
|
|
assert!(source.contains("if (value instanceof File) continue"));
|
|
assert!(source.contains("return multipart ? data : urlEncoded(data)"));
|
|
assert!(source.contains("if (body instanceof URLSearchParams) headers[\"Content-Type\"] = \"application/x-www-form-urlencoded;charset=UTF-8\""));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_interval_dispatch_avoids_duplicate_timers() {
|
|
// req: convention/005
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("root.querySelectorAll(\"[data-slhx-every]\")"));
|
|
assert!(source.contains("if (timers.has(el)) return"));
|
|
assert!(source.contains("setInterval(() => document.contains(el) ? send(el, \"every\")"));
|
|
assert!(source.contains("clearInterval(timers.get(el))"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_toggles_pending_conventions_around_requests() {
|
|
// req: convention/007 req: convention/008
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function showPending(el, on)"));
|
|
assert!(source.contains("el.getAttribute(\"data-slhx-pending-class\")"));
|
|
assert!(source.contains("el.classList.toggle(klass, on)"));
|
|
assert!(source.contains("root.querySelectorAll(\"[data-slhx-indicator]\")"));
|
|
assert!(source.contains("i.hidden = !on"));
|
|
assert!(source.contains("el.hasAttribute(\"data-slhx-disable-while-pending\")"));
|
|
assert!(source.contains("c.disabled = on"));
|
|
assert!(source.contains("showPending(target, true)"));
|
|
assert!(source.contains("showPending(target, false)"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_confirms_before_handler_dispatch() {
|
|
// req: convention/004
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("el.getAttribute(\"data-slhx-confirm\") && !confirm(el.getAttribute(\"data-slhx-confirm\"))"));
|
|
assert!(source.contains("return;"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_fetches_with_same_origin_credentials() {
|
|
// req: auth/005
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert_eq!(source.matches("credentials: \"same-origin\"").count(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_handles_get_forms_without_request_body() {
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function requestUrl(form, data, method)"));
|
|
assert!(source.contains("method === \"GET\" || method === \"HEAD\" ? undefined : requestBody"));
|
|
assert!(source.contains("if (method === \"GET\") url.search = urlEncoded(data).toString()"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_clicking_submitter_schedules_form_submit() {
|
|
// req: convention/004
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function formOwner(el)"));
|
|
assert!(source.contains("function formHandleId(form)"));
|
|
assert!(source.contains("document.getElementById(el.getAttribute(\"form\"))"));
|
|
assert!(source.contains("const direct = closestInRoot(event.target, root, `[${HID}]`)"));
|
|
assert!(source.contains("button[type=submit], input[type=submit], button:not([type])"));
|
|
assert!(source.contains("form.reportValidity && !form.reportValidity()"));
|
|
assert!(source.contains("schedule(form, \"submit\")"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_supports_drag_drop_params_without_user_js() {
|
|
// req: htmx_equivalents/002, req: dx/008
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("const dragKeys = new WeakMap()"));
|
|
assert!(source.contains("dragstart"));
|
|
assert!(source.contains("dragover"));
|
|
assert!(source.contains("eventName === \"drop\""));
|
|
assert!(source.contains("data.set(\"work_id\", dragKey)"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_supports_queued_request_policy() {
|
|
// req: convention/006
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function normalizedPolicy(value)"));
|
|
assert!(source.contains("value === \"latest\" || value === \"queue\" || value === \"drop\" || value === \"parallel\""));
|
|
assert!(source.contains("const policy = normalizedPolicy(el.getAttribute(\"data-slhx-policy\"))"));
|
|
assert!(source.contains("const queues = new WeakMap()"));
|
|
assert!(source.contains("policy === \"queue\""));
|
|
assert!(source.contains("const base = queues.get(target) || active.done"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_exposes_page_swap_hooks() {
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("data-slhx-nav"));
|
|
assert!(source.contains("data-slhx-boost"));
|
|
assert!(source.contains("history.pushState"));
|
|
assert!(source.contains("popstate"));
|
|
assert!(source.contains("x-slhx-title"));
|
|
assert!(source.contains("x-slhx-fingerprint"));
|
|
assert!(source.contains("slhx:missing-content-slot"));
|
|
assert!(source.contains("location.href = href"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_preserves_native_navigation_escape_hatches() {
|
|
// req: page_swap/007 req: failure/006
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function sameOriginNav(event, anchor)"));
|
|
assert!(source.contains("!event.defaultPrevented"));
|
|
assert!(source.contains("event.button === 0"));
|
|
assert!(source.contains("!event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey"));
|
|
assert!(source.contains("anchor.origin === location.origin"));
|
|
assert!(source.contains("!anchor.download"));
|
|
assert!(source.contains("anchor.target !== \"_blank\""));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_refuses_partial_updates_on_fingerprint_mismatch() {
|
|
// req: abi/003 req: abi/004 req: runtime/004
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function compatibleFingerprint(response, root)"));
|
|
assert!(source.contains("const received = response.headers.get(\"x-slhx-fingerprint\")"));
|
|
assert!(source.contains("const expected = root && root.getAttribute(FINGERPRINT)"));
|
|
assert!(source.contains("if (!compatibleFingerprint(response, root))"));
|
|
assert!(source.contains("location.reload()"));
|
|
assert!(source.contains("if (expected && String(batch.fingerprint) !== expected)"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_applies_sse_effect_batches_inside_roots() {
|
|
// req: push/001 req: push/003 req: runtime/001
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("const sseSources = new WeakMap()"));
|
|
assert!(source.contains("const url = root.getAttribute(\"data-slhx-sse\")"));
|
|
assert!(source.contains("new EventSource(new URL(url, location.href).href)"));
|
|
assert!(source.contains("source.addEventListener(\"slhx\", (event) => applySseMessage(root, event))"));
|
|
assert!(source.contains("source.addEventListener(\"message\", (event) => applySseMessage(root, event))"));
|
|
assert!(source.contains("applyBatch(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), root)"));
|
|
assert!(source.contains("emit(root, \"slhx:sse-error\", url)"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_page_swaps_lowered_slot_ids() {
|
|
// req: wire/001
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function replaceLoweredSlots(scope, doc)"));
|
|
assert!(source.contains("doc.querySelectorAll(\"[data-sid], [data-slot-id]\")"));
|
|
assert!(source.contains("target.innerHTML = source.innerHTML"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_keeps_form_field_targets_separate_from_error_targets() {
|
|
let source = slhx_js::RUNTIME_JS;
|
|
|
|
assert!(source.contains("function fieldTarget(scope, id, field)"));
|
|
assert!(source.contains("[data-fid=\"${id}\"] [name=\"${escapedField}\"]"));
|
|
assert!(source.contains("function formErrorTarget(scope, id, field)"));
|
|
assert!(source.contains("[data-slhx-error-for=\"${escapedField}\"]"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_preflights_batches_before_applying_ops() {
|
|
let source = slhx_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)"));
|
|
}
|
|
|
|
#[test]
|
|
fn runtime_ships_typescript_definitions() {
|
|
let source = slhx_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 ="));
|
|
assert!(source.contains("decodeBatch(buffer: ArrayBuffer): EffectBatch"));
|
|
assert!(source.contains("interface Window"));
|
|
}
|