fix(wasm): preserve binary fetch responses (#48)
* fix wasm response handling * fix URL inputs in fetch polyfill * test(wasm): cover binary fetch responses
This commit is contained in:
1 parent
fdec52c70f
commit
53a04fa905
6 files changed
+216
-29
No files matched your search
Generated
+1
@@ -1663,6 +1663,7 @@ name = "obscura-js"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64",
|
||||||
"deno_core",
|
"deno_core",
|
||||||
"deno_error",
|
"deno_error",
|
||||||
"html5ever",
|
"html5ever",
|
||||||
|
|||||||
@@ -39,3 +39,4 @@ clap = { version = "4", features = ["derive"] }
|
|||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
base64 = "0.22"
|
||||||
@@ -20,3 +20,4 @@ anyhow = { workspace = true }
|
|||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
html5ever = { workspace = true }
|
html5ever = { workspace = true }
|
||||||
deno_error = "0.6"
|
deno_error = "0.6"
|
||||||
|
base64 = { workspace = true }
|
||||||
Vendored
+121
-27
@@ -363,7 +363,13 @@ class Element extends Node {
|
|||||||
return "http://www.w3.org/1999/xhtml";
|
return "http://www.w3.org/1999/xhtml";
|
||||||
}
|
}
|
||||||
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
|
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
|
||||||
set innerHTML(v) { _dom("set_inner_html", this._nid, String(v ?? "")); }
|
set innerHTML(v) {
|
||||||
|
if (this.localName === 'template') {
|
||||||
|
this.content.innerHTML = v;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_dom("set_inner_html", this._nid, String(v ?? ""));
|
||||||
|
}
|
||||||
get outerHTML() { return _domParse("outer_html", this._nid) ?? ""; }
|
get outerHTML() { return _domParse("outer_html", this._nid) ?? ""; }
|
||||||
get innerText() { return this.textContent; }
|
get innerText() { return this.textContent; }
|
||||||
set innerText(v) { this.textContent = v; }
|
set innerText(v) { this.textContent = v; }
|
||||||
@@ -371,6 +377,11 @@ class Element extends Node {
|
|||||||
const ids = _domParse("element_children", this._nid) || [];
|
const ids = _domParse("element_children", this._nid) || [];
|
||||||
return ids.map(_wrapEl).filter(Boolean);
|
return ids.map(_wrapEl).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
get content() {
|
||||||
|
if (this.localName !== 'template') return undefined;
|
||||||
|
if (!this._templateContent) this._templateContent = document.createDocumentFragment();
|
||||||
|
return this._templateContent;
|
||||||
|
}
|
||||||
get childElementCount() { return this.children.length; }
|
get childElementCount() { return this.children.length; }
|
||||||
get firstElementChild() { return this.children[0] || null; }
|
get firstElementChild() { return this.children[0] || null; }
|
||||||
get lastElementChild() { const ch = this.children; return ch[ch.length-1] || null; }
|
get lastElementChild() { const ch = this.children; return ch[ch.length-1] || null; }
|
||||||
@@ -741,6 +752,9 @@ class Document extends Node {
|
|||||||
getElementsByClassName(c) { return this.querySelectorAll("." + c); }
|
getElementsByClassName(c) { return this.querySelectorAll("." + c); }
|
||||||
createElement(t) {
|
createElement(t) {
|
||||||
const el = _wrapEl(+_dom("create_element", t.toLowerCase()));
|
const el = _wrapEl(+_dom("create_element", t.toLowerCase()));
|
||||||
|
if (el && t.toLowerCase() === 'template') {
|
||||||
|
el._templateContent = this.createDocumentFragment();
|
||||||
|
}
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
createElementNS(ns, t) {
|
createElementNS(ns, t) {
|
||||||
@@ -753,9 +767,8 @@ class Document extends Node {
|
|||||||
const nid = +_dom("create_text_node", "");
|
const nid = +_dom("create_text_node", "");
|
||||||
const n = new Node(nid);
|
const n = new Node(nid);
|
||||||
n._isComment = true;
|
n._isComment = true;
|
||||||
n.nodeType = 8; // Override nodeType
|
Object.defineProperty(n, "nodeType", { value: 8, writable: false, configurable: true });
|
||||||
Object.defineProperty(n, "nodeType", { value: 8, writable: false });
|
Object.defineProperty(n, "nodeName", { value: "#comment", writable: false, configurable: true });
|
||||||
Object.defineProperty(n, "nodeName", { value: "#comment", writable: false });
|
|
||||||
_cache.set(nid, n);
|
_cache.set(nid, n);
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
@@ -913,11 +926,27 @@ class Document extends Node {
|
|||||||
class DocumentFragment extends Node {
|
class DocumentFragment extends Node {
|
||||||
get nodeType() { return 11; }
|
get nodeType() { return 11; }
|
||||||
get nodeName() { return "#document-fragment"; }
|
get nodeName() { return "#document-fragment"; }
|
||||||
querySelector(s) { return null; }
|
get innerHTML() { return _domParse("inner_html", this._nid) ?? ""; }
|
||||||
querySelectorAll(s) { return []; }
|
set innerHTML(v) { _dom("set_inner_html", this._nid, String(v ?? "")); }
|
||||||
get children() { return []; }
|
querySelector(s) { return _wrapEl(+_dom("query_selector", s)); }
|
||||||
get firstElementChild() { return null; }
|
querySelectorAll(s) {
|
||||||
|
const ids = _domParse("query_selector_all", s) || [];
|
||||||
|
const list = ids.map(_wrapEl).filter(Boolean);
|
||||||
|
list.item = (i) => list[i] || null;
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
get children() {
|
||||||
|
const ids = _domParse("element_children", this._nid) || [];
|
||||||
|
return ids.map(_wrapEl).filter(Boolean);
|
||||||
|
}
|
||||||
|
get firstElementChild() { return this.children[0] || null; }
|
||||||
|
get lastElementChild() { const ch = this.children; return ch[ch.length - 1] || null; }
|
||||||
getElementById(id) { return null; }
|
getElementById(id) { return null; }
|
||||||
|
cloneNode(deep) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
if (deep) frag.innerHTML = this.innerHTML;
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentType extends Node {
|
class DocumentType extends Node {
|
||||||
@@ -993,6 +1022,13 @@ globalThis.frames = globalThis;
|
|||||||
globalThis.frameElement = null;
|
globalThis.frameElement = null;
|
||||||
globalThis.length = 0;
|
globalThis.length = 0;
|
||||||
|
|
||||||
|
globalThis.Window = globalThis.Window || function Window() {};
|
||||||
|
Object.defineProperty(globalThis.Window, Symbol.hasInstance, {
|
||||||
|
value(obj) { return obj === globalThis || (obj && obj.window === obj); },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const _iframeRegistry = [];
|
const _iframeRegistry = [];
|
||||||
function _registerIframe(iframeEl) {
|
function _registerIframe(iframeEl) {
|
||||||
const idx = _iframeRegistry.length;
|
const idx = _iframeRegistry.length;
|
||||||
@@ -1113,8 +1149,63 @@ globalThis.pageXOffset = 0; globalThis.pageYOffset = 0;
|
|||||||
globalThis.__fetchInterceptEnabled = false;
|
globalThis.__fetchInterceptEnabled = false;
|
||||||
globalThis.__fetchInterceptCallback = null; // Set by CDP to handle paused requests
|
globalThis.__fetchInterceptCallback = null; // Set by CDP to handle paused requests
|
||||||
|
|
||||||
|
function _base64ToUint8Array(b64) {
|
||||||
|
const clean = String(b64 || '').replace(/[\r\n\s]/g, '');
|
||||||
|
if (!clean) return new Uint8Array();
|
||||||
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
const padding = clean.endsWith('==') ? 2 : (clean.endsWith('=') ? 1 : 0);
|
||||||
|
const bytes = new Uint8Array((clean.length * 3 >> 2) - padding);
|
||||||
|
let out = 0;
|
||||||
|
for (let i = 0; i < clean.length; i += 4) {
|
||||||
|
const a = alphabet.indexOf(clean[i]);
|
||||||
|
const b = alphabet.indexOf(clean[i + 1]);
|
||||||
|
const c = clean[i + 2] === '=' ? 0 : alphabet.indexOf(clean[i + 2]);
|
||||||
|
const d = clean[i + 3] === '=' ? 0 : alphabet.indexOf(clean[i + 3]);
|
||||||
|
const n = (a << 18) | (b << 12) | (c << 6) | d;
|
||||||
|
if (out < bytes.length) bytes[out++] = (n >> 16) & 0xff;
|
||||||
|
if (out < bytes.length) bytes[out++] = (n >> 8) & 0xff;
|
||||||
|
if (out < bytes.length) bytes[out++] = n & 0xff;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bodyToUint8Array(body) {
|
||||||
|
if (body == null) return new Uint8Array();
|
||||||
|
if (body instanceof Uint8Array) return body;
|
||||||
|
if (body instanceof ArrayBuffer) return new Uint8Array(body);
|
||||||
|
if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
||||||
|
return new TextEncoder().encode(String(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _arrayBufferFromBytes(bytes) {
|
||||||
|
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _installWasmStreamingFallback() {
|
||||||
|
if (typeof WebAssembly === 'undefined') return;
|
||||||
|
if (WebAssembly.instantiateStreaming && WebAssembly.instantiateStreaming.__obscuraFallback) return;
|
||||||
|
const nativeInstantiateStreaming = WebAssembly.instantiateStreaming;
|
||||||
|
const fallback = async function instantiateStreaming(source, imports) {
|
||||||
|
const response = await source;
|
||||||
|
if (response && typeof response.arrayBuffer === 'function') {
|
||||||
|
return WebAssembly.instantiate(await response.arrayBuffer(), imports);
|
||||||
|
}
|
||||||
|
if (typeof nativeInstantiateStreaming === 'function') {
|
||||||
|
return nativeInstantiateStreaming.call(WebAssembly, response, imports);
|
||||||
|
}
|
||||||
|
return WebAssembly.instantiate(response, imports);
|
||||||
|
};
|
||||||
|
fallback.__obscuraFallback = true;
|
||||||
|
WebAssembly.instantiateStreaming = fallback;
|
||||||
|
}
|
||||||
|
_installWasmStreamingFallback();
|
||||||
|
|
||||||
globalThis.fetch = async (input, init = {}) => {
|
globalThis.fetch = async (input, init = {}) => {
|
||||||
let url = typeof input === "string" ? input : (input instanceof Request ? input.url : input?.url || "");
|
let url = typeof input === "string"
|
||||||
|
? input
|
||||||
|
: (input instanceof Request
|
||||||
|
? input.url
|
||||||
|
: ((typeof URL === 'function' && input instanceof URL) ? input.href : (input?.url || input?.href || String(input || ""))));
|
||||||
if (url && !url.includes('://')) {
|
if (url && !url.includes('://')) {
|
||||||
try {
|
try {
|
||||||
const base = _domParse("document_url") || "about:blank";
|
const base = _domParse("document_url") || "about:blank";
|
||||||
@@ -1138,16 +1229,15 @@ globalThis.fetch = async (input, init = {}) => {
|
|||||||
throw new TypeError('Failed to fetch: ' + (parsed.corsError || 'CORS error'));
|
throw new TypeError('Failed to fetch: ' + (parsed.corsError || 'CORS error'));
|
||||||
}
|
}
|
||||||
const respType = parsed.status === 0 ? "opaque" : (fetchMode === "no-cors" ? "opaque" : "basic");
|
const respType = parsed.status === 0 ? "opaque" : (fetchMode === "no-cors" ? "opaque" : "basic");
|
||||||
return {
|
const responseBody = parsed.bodyBase64 ? _base64ToUint8Array(parsed.bodyBase64) : (parsed.body || "");
|
||||||
ok: parsed.status >= 200 && parsed.status < 300, status: parsed.status, statusText: "",
|
return new Response(responseBody, {
|
||||||
type: respType, redirected: false, url: parsed.url || url,
|
status: parsed.status,
|
||||||
headers: new Headers(parsed.headers || {}),
|
statusText: "",
|
||||||
async text() { return parsed.body || ""; },
|
headers: parsed.headers || {},
|
||||||
async json() { return JSON.parse(parsed.body || "null"); },
|
type: respType,
|
||||||
async arrayBuffer() { return new TextEncoder().encode(parsed.body || "").buffer; },
|
url: parsed.url || url,
|
||||||
async blob() { return new Blob([parsed.body || ""]); },
|
redirected: false,
|
||||||
clone() { return this; },
|
});
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof Headers === "undefined") {
|
if (typeof Headers === "undefined") {
|
||||||
@@ -1419,7 +1509,8 @@ if (typeof Request === 'undefined') {
|
|||||||
constructor(input, init = {}) {
|
constructor(input, init = {}) {
|
||||||
if (typeof input === 'string') { this.url = input; }
|
if (typeof input === 'string') { this.url = input; }
|
||||||
else if (input instanceof Request) { this.url = input.url; init = { ...input, ...init }; }
|
else if (input instanceof Request) { this.url = input.url; init = { ...input, ...init }; }
|
||||||
else { this.url = String(input); }
|
else if (typeof URL === 'function' && input instanceof URL) { this.url = input.href; }
|
||||||
|
else { this.url = input?.url || input?.href || String(input); }
|
||||||
this.method = (init.method || 'GET').toUpperCase();
|
this.method = (init.method || 'GET').toUpperCase();
|
||||||
this.headers = new Headers(init.headers);
|
this.headers = new Headers(init.headers);
|
||||||
this.body = init.body || null;
|
this.body = init.body || null;
|
||||||
@@ -1440,16 +1531,16 @@ if (typeof Request === 'undefined') {
|
|||||||
if (typeof Response === 'undefined') {
|
if (typeof Response === 'undefined') {
|
||||||
globalThis.Response = class Response {
|
globalThis.Response = class Response {
|
||||||
constructor(body, init = {}) {
|
constructor(body, init = {}) {
|
||||||
this._body = body; this.status = init.status || 200; this.statusText = init.statusText || '';
|
this._bodyBytes = _bodyToUint8Array(body); this.status = init.status || 200; this.statusText = init.statusText || '';
|
||||||
this.ok = this.status >= 200 && this.status < 300;
|
this.ok = this.status >= 200 && this.status < 300;
|
||||||
this.headers = new Headers(init.headers);
|
this.headers = new Headers(init.headers);
|
||||||
this.type = 'basic'; this.url = '';
|
this.type = init.type || 'basic'; this.url = init.url || ''; this.redirected = !!init.redirected;
|
||||||
}
|
}
|
||||||
async text() { return this._body ? String(this._body) : ''; }
|
async text() { return new TextDecoder().decode(this._bodyBytes); }
|
||||||
async json() { return JSON.parse(await this.text()); }
|
async json() { return JSON.parse(await this.text()); }
|
||||||
async arrayBuffer() { return new TextEncoder().encode(await this.text()).buffer; }
|
async arrayBuffer() { return _arrayBufferFromBytes(this._bodyBytes); }
|
||||||
async blob() { return new Blob([await this.text()]); }
|
async blob() { return new Blob([this._bodyBytes]); }
|
||||||
clone() { return new Response(this._body, { status: this.status, headers: this.headers }); }
|
clone() { return new Response(this._bodyBytes, { status: this.status, statusText: this.statusText, headers: this.headers, type: this.type, url: this.url, redirected: this.redirected }); }
|
||||||
static error() { return new Response(null, { status: 0 }); }
|
static error() { return new Response(null, { status: 0 }); }
|
||||||
static redirect(url, status) { return new Response(null, { status: status || 302, headers: { Location: url } }); }
|
static redirect(url, status) { return new Response(null, { status: status || 302, headers: { Location: url } }); }
|
||||||
static json(data, init) { return new Response(JSON.stringify(data), { ...init, headers: { 'content-type': 'application/json', ...(init?.headers || {}) } }); }
|
static json(data, init) { return new Response(JSON.stringify(data), { ...init, headers: { 'content-type': 'application/json', ...(init?.headers || {}) } }); }
|
||||||
@@ -1544,7 +1635,9 @@ if (typeof TextDecoder === 'undefined') {
|
|||||||
constructor(label) { this.encoding = label || 'utf-8'; }
|
constructor(label) { this.encoding = label || 'utf-8'; }
|
||||||
decode(buf) {
|
decode(buf) {
|
||||||
if (!buf) return '';
|
if (!buf) return '';
|
||||||
const bytes = new Uint8Array(buf.buffer || buf);
|
const bytes = ArrayBuffer.isView(buf)
|
||||||
|
? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
|
||||||
|
: new Uint8Array(buf);
|
||||||
let str = '', i = 0;
|
let str = '', i = 0;
|
||||||
while (i < bytes.length) {
|
while (i < bytes.length) {
|
||||||
let c = bytes[i++];
|
let c = bytes[i++];
|
||||||
@@ -2908,6 +3001,7 @@ if (typeof Document !== 'undefined' && !Document.prototype.importNode) {
|
|||||||
globalThis.__obscura_init = function() {
|
globalThis.__obscura_init = function() {
|
||||||
_fpSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF >>> 0);
|
_fpSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF >>> 0);
|
||||||
_fpCache = null;
|
_fpCache = null;
|
||||||
|
_installWasmStreamingFallback();
|
||||||
|
|
||||||
globalThis.document = new Document(+_dom("document_node_id"));
|
globalThis.document = new Document(+_dom("document_node_id"));
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::sync::{Arc, OnceLock};
|
|||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
use deno_core::Extension;
|
use deno_core::Extension;
|
||||||
|
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||||
use obscura_dom::{DomTree, NodeData, NodeId};
|
use obscura_dom::{DomTree, NodeData, NodeId};
|
||||||
use obscura_net::{CookieJar, ObscuraHttpClient};
|
use obscura_net::{CookieJar, ObscuraHttpClient};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -517,16 +518,19 @@ async fn op_fetch_url(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp_body = response
|
let resp_bytes = response
|
||||||
.text()
|
.bytes()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| deno_error::JsErrorBox::generic(e.to_string()))?;
|
.map_err(|e| deno_error::JsErrorBox::generic(e.to_string()))?;
|
||||||
|
let resp_body = String::from_utf8_lossy(&resp_bytes).to_string();
|
||||||
|
let resp_body_base64 = BASE64.encode(&resp_bytes);
|
||||||
|
|
||||||
tracing::debug!("op_fetch_url completed: {} {} ({} bytes)", method, url, resp_body.len());
|
tracing::debug!("op_fetch_url completed: {} {} ({} bytes)", method, url, resp_body.len());
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"status": status,
|
"status": status,
|
||||||
"body": resp_body,
|
"body": resp_body,
|
||||||
|
"bodyBase64": resp_body_base64,
|
||||||
"url": url,
|
"url": url,
|
||||||
"headers": resp_headers,
|
"headers": resp_headers,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1282,6 +1282,92 @@ mod tests {
|
|||||||
assert_eq!(result.as_str().unwrap(), "http://localhost:8080/dir/api.json");
|
assert_eq!(result.as_str().unwrap(), "http://localhost:8080/dir/api.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn test_fetch_url_input_decodes_binary_body_base64() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let result = rt.call_function_on_for_cdp(
|
||||||
|
r#"async () => {
|
||||||
|
const originalFetchOp = Deno.core.ops.op_fetch_url;
|
||||||
|
try {
|
||||||
|
Deno.core.ops.op_fetch_url = (url) => {
|
||||||
|
globalThis.__capturedFetchUrl = url;
|
||||||
|
return JSON.stringify({
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "application/wasm" },
|
||||||
|
bodyBase64: "AGFzbQEAAAA=",
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const response = await fetch(new URL("/pkg/app_bg.wasm", document.URL));
|
||||||
|
const bytes = Array.from(new Uint8Array(await response.arrayBuffer()));
|
||||||
|
return { url: globalThis.__capturedFetchUrl, bytes };
|
||||||
|
} finally {
|
||||||
|
Deno.core.ops.op_fetch_url = originalFetchOp;
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
None,
|
||||||
|
&[],
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
result.value.unwrap(),
|
||||||
|
serde_json::json!({
|
||||||
|
"url": "http://example.com/pkg/app_bg.wasm",
|
||||||
|
"bytes": [0, 97, 115, 109, 1, 0, 0, 0],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn test_response_array_buffer_preserves_typed_array_view() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let result = rt.call_function_on_for_cdp(
|
||||||
|
r#"async () => {
|
||||||
|
const bytes = new Uint8Array([9, 0, 97, 115, 109, 1, 8]);
|
||||||
|
const response = new Response(bytes.subarray(1, 6));
|
||||||
|
return Array.from(new Uint8Array(await response.arrayBuffer()));
|
||||||
|
}"#,
|
||||||
|
None,
|
||||||
|
&[],
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.value.unwrap(), serde_json::json!([0, 97, 115, 109, 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn test_wasm_instantiate_streaming_uses_response_array_buffer() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let result = rt.call_function_on_for_cdp(
|
||||||
|
r#"async () => {
|
||||||
|
const bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]);
|
||||||
|
const result = await WebAssembly.instantiateStreaming(
|
||||||
|
Promise.resolve(new Response(bytes)),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return result.instance instanceof WebAssembly.Instance;
|
||||||
|
}"#,
|
||||||
|
None,
|
||||||
|
&[],
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.value.unwrap(), serde_json::json!(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_text_decoder_respects_typed_array_view() {
|
||||||
|
let mut rt = setup_runtime("<html><body></body></html>");
|
||||||
|
let result = rt.evaluate(
|
||||||
|
"new TextDecoder().decode(new Uint8Array([65, 66, 67]).subarray(1, 2))"
|
||||||
|
).unwrap();
|
||||||
|
assert_eq!(result.as_str().unwrap(), "B");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_document_doctype() {
|
fn test_document_doctype() {
|
||||||
let mut rt = setup_runtime("<!DOCTYPE html><html><body></body></html>");
|
let mut rt = setup_runtime("<!DOCTYPE html><html><body></body></html>");
|
||||||
|
|||||||
Reference in new issue
Block a user