From e802e3790bd6e370ea3f1751f8f7bbbf6f397494 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 7 Oct 2026 10:47:49 +0800 Subject: [PATCH] fix(canvas): expose native transform snapshots Return a fresh native DOMMatrix snapshot from CanvasRenderingContext2D.getTransform and OffscreenCanvasRenderingContext2D.getTransform. Reuse the saved native canvas transform state and generated native receiver checks. Window and Worker regressions cover transform updates, save/restore, snapshot independence, dimension resets, mutated public constructors, cross-realm calls and invalid receivers. Expose the existing DOMMatrix and DOMPoint interface pairs in Workers so the snapshot return type and transformPoint result remain usable there. --- .../src/context_bootstrap/canvas.rs | 2 + .../src/context_bootstrap/canvas/transform.rs | 62 +++++++++- .../exposed_interfaces/metadata.rs | 4 + .../src/context_bootstrap/geometry_runtime.rs | 16 ++- .../context_bootstrap/media_file_template.rs | 6 +- .../tests/canvas_transform_snapshots.js | 115 ++++++++++++++++++ .../tests/canvas_transform_snapshots.rs | 92 ++++++++++++++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../tests/canvas_transform_snapshots.rs | 22 ++++ .../src/worker/thread/tests/mod.rs | 3 + 10 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.js create mode 100644 moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.rs create mode 100644 moli-renderer-v8/src/worker/thread/tests/canvas_transform_snapshots.rs diff --git a/moli-renderer-v8/src/context_bootstrap/canvas.rs b/moli-renderer-v8/src/context_bootstrap/canvas.rs index 2105e55202..3aeebbee45 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas.rs @@ -197,6 +197,8 @@ mod state; mod transform; mod webgl; +pub(crate) use transform::install_canvas_transform_bindings; + pub(crate) use backing_store::{ attach_canvas_like_context_object, canvas_like_to_data_url, reset_html_canvas_backing_store_for_dimension_assignment, diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/transform.rs b/moli-renderer-v8/src/context_bootstrap/canvas/transform.rs index 3c433bad99..587fbc336a 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/transform.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/transform.rs @@ -1,6 +1,66 @@ //! WebIDL conversion and DOMMatrix2DInit alias validation for Canvas transforms. -use crate::{util::throw_type_error, webidl}; +use crate::{util::throw_type_error, web_api_interfaces, webidl}; +use moli_geometry::DomMatrixComponents; +use moli_webapi_declare::WebApiFunctionTemplate; + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::CanvasRenderingContext2D, enumerable, receiver)] +struct CanvasTransformPrototypeDeclaration { + #[webapi(method = "getTransform", length = 0, callback = get_transform_callback)] + get_transform: (), +} + +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::OffscreenCanvasRenderingContext2D, enumerable, receiver)] +struct OffscreenCanvasTransformPrototypeDeclaration { + #[webapi(method = "getTransform", length = 0, callback = get_transform_callback)] + get_transform: (), +} + +pub(crate) fn install_canvas_transform_bindings<'s>( + scope: &mut v8::PinScope<'s, '_, ()>, + prototype: v8::Local<'s, v8::ObjectTemplate>, + interface: &str, +) { + match interface { + "CanvasRenderingContext2D" => { + CanvasTransformPrototypeDeclaration::initialize_prototype_template(scope, prototype); + } + "OffscreenCanvasRenderingContext2D" => { + OffscreenCanvasTransformPrototypeDeclaration::initialize_prototype_template( + scope, prototype, + ); + } + _ => unreachable!("only 2D canvas contexts have transform bindings"), + } +} + +fn get_transform_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let context = moli_webapi_declare::web_api_object_target(scope, args.this()) + .expect("validated canvas receiver has native identity"); + let [a, b, c, d, e, f] = super::state::canvas_path_state(scope, context) + .borrow() + .transform() + .coefficients; + let matrix = super::super::geometry_runtime::build_dom_matrix_object( + scope, + DomMatrixComponents { + m11: a, + m12: b, + m21: c, + m22: d, + m41: e, + m42: f, + ..DomMatrixComponents::identity() + }, + ); + rv.set(matrix.into()); +} #[derive(Default, webidl::WebIdlDictionary)] #[webidl(prefix = "DOMMatrix2DInit")] diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs index ec525e4614..bfbd97a502 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/metadata.rs @@ -21,6 +21,10 @@ pub(in crate::context_bootstrap) const STORAGE_INTERFACE_NAMES: &[&str] = &[ pub(in crate::context_bootstrap) const WORKER_SHARED_INTERFACE_NAMES: &[&str] = &[ "DOMRectReadOnly", "DOMRect", + "DOMPointReadOnly", + "DOMPoint", + "DOMMatrixReadOnly", + "DOMMatrix", "EventTarget", "Event", "CustomEvent", diff --git a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs index 756b540523..3e8edbeaf8 100644 --- a/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/geometry_runtime.rs @@ -109,9 +109,6 @@ macro_rules! dom_matrix_object_declaration { } } - fn identity() -> Self { - Self::from_components(DomMatrixComponents::identity()) - } } }; } @@ -566,7 +563,14 @@ pub(in crate::context_bootstrap) fn build_dom_point_object<'s>( pub(in crate::context_bootstrap) fn build_dom_matrix_identity_object<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - DomMatrixObjectDeclaration::identity() + build_dom_matrix_object(scope, DomMatrixComponents::identity()) +} + +pub(in crate::context_bootstrap) fn build_dom_matrix_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + components: DomMatrixComponents, +) -> v8::Local<'s, v8::Object> { + DomMatrixObjectDeclaration::from_components(components) .bind(scope) .expect("DOMMatrix declaration should bind") } @@ -574,7 +578,7 @@ pub(in crate::context_bootstrap) fn build_dom_matrix_identity_object<'s>( fn build_dom_matrix_readonly_identity_object<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> v8::Local<'s, v8::Object> { - DomMatrixReadOnlyObjectDeclaration::identity() + DomMatrixReadOnlyObjectDeclaration::from_components(DomMatrixComponents::identity()) .bind(scope) .expect("DOMMatrixReadOnly declaration should bind") } @@ -1904,7 +1908,7 @@ fn initialize_dom_matrix_identity_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, ) { - DomMatrixObjectDeclaration::identity() + DomMatrixObjectDeclaration::from_components(DomMatrixComponents::identity()) .initialize(scope, object) .expect("DOMMatrix declaration should initialize object"); } diff --git a/moli-renderer-v8/src/context_bootstrap/media_file_template.rs b/moli-renderer-v8/src/context_bootstrap/media_file_template.rs index b8c60b9c4a..8c4a9c39cf 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_file_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_file_template.rs @@ -32,8 +32,9 @@ use super::canvas::{ canvas_context_stroke_style_setter_callback, canvas_context_stroke_text_callback, canvas_context_transform_callback, canvas_context_translate_callback, canvas_gradient_add_color_stop_callback, install_canvas_template_bindings, - offscreen_canvas_convert_to_blob_callback, offscreen_canvas_get_context_callback, - webgl_boolean_callback, webgl_check_framebuffer_status_callback, webgl_create_buffer_callback, + install_canvas_transform_bindings, offscreen_canvas_convert_to_blob_callback, + offscreen_canvas_get_context_callback, webgl_boolean_callback, + webgl_check_framebuffer_status_callback, webgl_create_buffer_callback, webgl_create_framebuffer_callback, webgl_create_program_callback, webgl_create_renderbuffer_callback, webgl_create_shader_callback, webgl_get_attrib_location_callback, webgl_get_context_attributes_callback, @@ -688,6 +689,7 @@ pub(super) fn install_media_file_template_bindings<'s>( CanvasRenderingContext2dTemplateDeclaration::initialize_prototype_template( scope, proto, ); + install_canvas_transform_bindings(scope, proto, spec_name); } "WebGLRenderingContext" => { let proto = template.prototype_template(scope); diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.js b/moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.js new file mode 100644 index 0000000000..caca900a65 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.js @@ -0,0 +1,115 @@ +(function canvasContract(owner, callee, kind, tag) { + const checks = []; + const check = (name, run) => { + try { checks.push({name: 'canvas/' + tag + '/' + name, passed: run() === true}); } + catch (error) { checks.push({name: 'canvas/' + tag + '/' + name, passed: false, error: String(error)}); } + }; + const interfaceName = kind === 'element' ? 'CanvasRenderingContext2D' : 'OffscreenCanvasRenderingContext2D'; + const makeCanvas = () => kind === 'element' ? owner.document.createElement('canvas') : new owner.OffscreenCanvas(96, 96); + const canvas = makeCanvas(); canvas.width = canvas.height = 96; + const ctx = canvas.getContext('2d'); + const prototype = callee[interfaceName].prototype; + const method = prototype.getTransform; + const Matrix = callee.DOMMatrix; + const matrixPrototype = Matrix.prototype; + const get = (receiver = ctx, ...args) => { + if (typeof method !== 'function') throw new Error('getTransform missing'); + return method.call(receiver, ...args); + }; + const components = matrix => ['a', 'b', 'c', 'd', 'e', 'f'].map(name => matrix[name]); + const equal = (actual, expected) => actual.length === expected.length && actual.every((value, index) => + value === expected[index] || Math.abs(value - expected[index]) <= 1e-12 * Math.max(1, Math.abs(expected[index]))); + const matches = expected => equal(components(get()), expected); + const identity = [1, 0, 0, 1, 0, 0]; + const throwsTypeError = run => { try { run(); return false; } catch (error) { return error instanceof callee.TypeError; } }; + + check('own descriptor', () => { + const d = Object.getOwnPropertyDescriptor(prototype, 'getTransform'); + return !!d && d.value === method && d.enumerable && d.writable && d.configurable; + }); + check('function metadata', () => typeof method === 'function' && method.length === 0 && method.name === 'getTransform' && method instanceof callee.Function); + check('initial identity', () => matches(identity)); + check('DOMMatrix prototype in callee realm', () => Object.getPrototypeOf(get()) === matrixPrototype); + check('mutable and readonly brands', () => get() instanceof Matrix && get() instanceof callee.DOMMatrixReadOnly); + check('initial 2D and identity flags', () => get().is2D && get().isIdentity); + check('fresh objects', () => get() !== get()); + check('no own expando components', () => !Object.hasOwn(get(), 'a') && !Object.hasOwn(get(), 'm11')); + check('Float64 mapping and realm', () => { + const array = get().toFloat64Array(); + return array instanceof callee.Float64Array && equal(Array.from(array), [1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]); + }); + check('Float32 mapping and realm', () => get().toFloat32Array() instanceof callee.Float32Array); + check('JSON mapping', () => get().toJSON().m44 === 1 && get().toJSON().is2D === true); + check('ignored extra arguments', () => { + let traps = 0; + const extra = new Proxy({}, {get() { traps++; throw new Error('argument converted'); }}); + return equal(components(get(ctx, extra)), identity) && traps === 0; + }); + + for (const [name, receiver] of [ + ['null', null], ['undefined', undefined], ['number', 0], ['string', ''], ['boolean', false], + ['plain', {}], ['prototype', prototype], ['forged prototype', Object.create(prototype)], + ['inherited real object', Object.create(ctx)], ['author Proxy', new Proxy(ctx, {})] + ]) check('receiver ' + name, () => throwsTypeError(() => { + if (typeof method !== 'function') throw new Error('getTransform missing'); + method.call(receiver); + })); + const revoked = Proxy.revocable(ctx, {}); revoked.revoke(); + check('revoked Proxy receiver', () => throwsTypeError(() => get(revoked.proxy))); + check('receiver validation without author traps', () => { + let traps = 0; + const fake = new Proxy(ctx, {get() { traps++; throw new Error('receiver trap'); }, getPrototypeOf() { traps++; return prototype; }}); + return throwsTypeError(() => get(fake)) && traps === 0; + }); + if (owner.document) check('other context interface rejected', () => { + const other = kind === 'element' ? new owner.OffscreenCanvas(96,96) : owner.document.createElement('canvas'); + return throwsTypeError(() => get(other.getContext('2d'))); + }); + + check('six scalar coefficients', () => { ctx.setTransform(1, 2, 3, 4, 5, 6); return matches([1,2,3,4,5,6]); }); + check('six scalar 4D mapping', () => equal(Array.from(get().toFloat64Array()), [1,2,0,0,3,4,0,0,0,0,1,0,5,6,0,1])); + check('nonidentity flag', () => !get().isIdentity && get().is2D); + check('snapshot mutation leaves context unchanged', () => { const m = get(); m.a = 8; m.e = 30; return matches([1,2,3,4,5,6]); }); + check('snapshot mutator leaves context unchanged', () => { get().translateSelf(20,30).scaleSelf(2); return matches([1,2,3,4,5,6]); }); + check('context mutation leaves snapshot unchanged', () => { const m = get(); ctx.setTransform(2,0,0,3,10,20); return equal(components(m),[1,2,3,4,5,6]) && matches([2,0,0,3,10,20]); }); + check('postmultiply translation', () => { ctx.translate(4,5); return matches([2,0,0,3,18,35]); }); + check('postmultiply scale', () => { ctx.scale(4,5); return matches([8,0,0,15,18,35]); }); + check('postmultiply rotation', () => { ctx.rotate(Math.PI/2); return matches([0,15,-8,0,18,35]); }); + check('reset transform', () => { ctx.resetTransform(); return matches(identity); }); + check('noncommutative multiplication', () => { ctx.setTransform(1,2,3,4,5,6); ctx.transform(7,8,9,10,11,12); return matches([31,46,39,58,52,76]); }); + check('fractional double state', () => { const a=[1.125,0.0625,-0.03125,2.25,5.125,-7.75]; ctx.setTransform(...a); return matches(a); }); + check('singular matrix', () => { ctx.setTransform(0,0,0,0,5,6); return matches([0,0,0,0,5,6]); }); + + const names = ['a','b','c','d','e','f']; + const aliases = ['m11','m12','m21','m22','m41','m42']; + for (let index=0; index<6; index++) { + check('dictionary component ' + names[index], () => { ctx.setTransform({[names[index]]:2}); const a=identity.slice(); a[index]=2; return matches(a); }); + check('dictionary alias ' + aliases[index], () => { ctx.setTransform({[aliases[index]]:3}); const a=identity.slice(); a[index]=3; return matches(a); }); + check('inconsistent aliases atomic ' + names[index], () => { ctx.setTransform(1,2,3,4,5,6); let failed=false; try { ctx.setTransform({[names[index]]:1,[aliases[index]]:2}); } catch (e) { failed=e instanceof owner.TypeError; } return failed && matches([1,2,3,4,5,6]); }); + } + for (const [label,value] of [['empty',{}],['undefined',undefined],['null',null]]) + check('dictionary identity ' + label, () => { ctx.translate(20,30); ctx.setTransform(value); return matches(identity); }); + check('no argument identity', () => { ctx.translate(20,30); ctx.setTransform(); return matches(identity); }); + check('dictionary from snapshot', () => { ctx.setTransform(1,2,3,4,5,6); const m=get(); ctx.resetTransform(); ctx.setTransform(m); return matches([1,2,3,4,5,6]); }); + for (const [label,value] of [['NaN',NaN],['positive Infinity',Infinity],['negative Infinity',-Infinity]]) { + check('nonfinite scalar ignored ' + label, () => { ctx.setTransform(1,2,3,4,5,6); ctx.setTransform(value,0,0,1,0,0); return matches([1,2,3,4,5,6]); }); + check('nonfinite dictionary ignored ' + label, () => { ctx.setTransform(1,2,3,4,5,6); ctx.setTransform({a:value,m11:value}); return matches([1,2,3,4,5,6]); }); + } + check('getter exception preserves state and identity', () => { + ctx.setTransform(1,2,3,4,5,6); const sentinel={}; let same=false; + try { ctx.setTransform({a:8,get e(){throw sentinel;}}); } catch(e) {same=e===sentinel;} + return same && matches([1,2,3,4,5,6]); + }); + check('beginPath preserves transform', () => { ctx.beginPath(); return matches([1,2,3,4,5,6]); }); + check('fresh independent context', () => matches([1,2,3,4,5,6]) && equal(components(get(makeCanvas().getContext('2d'))),identity)); + check('repeated getContext preserves state', () => canvas.getContext('2d') === ctx && matches([1,2,3,4,5,6])); + check('width reset', () => { canvas.width = canvas.width; return matches(identity); }); + check('height reset', () => { ctx.translate(20,30); canvas.height = canvas.height; return matches(identity); }); + check('snapshot survives bitmap reset', () => { ctx.setTransform(1,2,3,4,5,6); const m=get(); canvas.width=canvas.width+1; return equal(components(m),[1,2,3,4,5,6]) && matches(identity); }); + check('global constructor spoofing', () => { + const original=callee.DOMMatrix; let calls=0; + try { callee.DOMMatrix=function(){calls++; throw new Error('author constructor');}; return Object.getPrototypeOf(get()) === matrixPrototype && calls === 0; } + finally {callee.DOMMatrix=original;} + }); + return checks; +}) diff --git a/moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.rs b/moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.rs new file mode 100644 index 0000000000..514e699627 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/canvas_transform_snapshots.rs @@ -0,0 +1,92 @@ +use super::*; + +#[test] +fn canvas_transform_snapshots_copy_native_state_with_brands_and_realms() { + let mut vm = new_storage_page_task_executor_test_vm("https://canvas-transform.test/"); + vm.eval("document.body.innerHTML = ''") + .unwrap(); + let source = format!( + r#"(() => {{ + const probe = {}; + const realms = [globalThis, document.getElementById('child').contentWindow]; + const checks = []; + for (let owner = 0; owner < 2; owner++) for (let callee = 0; callee < 2; callee++) + for (const kind of ['element', 'offscreen']) + checks.push(...probe(realms[owner], realms[callee], kind, 'window-' + owner + '-' + callee + '-' + kind)); + globalThis.__canvasTransformChecks = checks; + return checks.length; + }})()"#, + include_str!("canvas_transform_snapshots.js"), + ); + assert_eq!(vm.eval(&source).unwrap(), "600"); + assert_eq!( + vm.eval("JSON.stringify(__canvasTransformChecks.filter(row => !row.passed))") + .unwrap(), + "[]" + ); +} + +#[test] +fn canvas_transform_snapshots_accept_registered_native_proxies_and_share_paint_state() { + let mut vm = new_storage_page_task_executor_test_vm("https://canvas-transform-native.test/"); + vm.eval( + r#"document.body.innerHTML = ''; + globalThis.nativeCanvasContexts = [document.createElement('canvas'), new OffscreenCanvas(96,96)] + .map(canvas => canvas.getContext('2d')); + for (const ctx of nativeCanvasContexts) ctx.setTransform(1,0,0,1,20,0);"#, + ) + .unwrap(); + let context_ptr = &vm.page_default_context as *const _; + vm.with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| { + let global = scope.get_current_context().global(scope); + let key = crate::util::v8str(scope, "nativeCanvasContexts"); + let value = global.get(scope, key.into()).unwrap(); + let contexts = v8::Local::::try_from(value).unwrap(); + let proxies = v8::Array::new(scope, 0); + for index in 0..contexts.length() { + let value = contexts.get_index(scope, index).unwrap(); + let context = v8::Local::::try_from(value).unwrap(); + let handler = crate::util::new_null_prototype_object(scope); + let proxy = v8::Proxy::new(scope, context, handler).unwrap(); + moli_webapi_declare::register_web_api_proxy(scope, proxy).unwrap(); + assert_eq!(proxies.set_index(scope, index, proxy.into()), Some(true)); + } + let key = crate::util::v8str(scope, "nativeCanvasContextProxies"); + assert_eq!( + global.create_data_property(scope, key.into(), proxies.into()), + Some(true) + ); + Ok(()) + }) + .unwrap(); + assert_eq!( + vm.eval( + r#"(() => { + const realms = [globalThis, document.getElementById('child').contentWindow]; + for (let index=0;index<2;index++) { + const ctx=nativeCanvasContexts[index], proxy=nativeCanvasContextProxies[index]; + const name=index===0?'CanvasRenderingContext2D':'OffscreenCanvasRenderingContext2D'; + for (const realm of realms) { + const get=realm[name].prototype.getTransform; + const matrix=get.call(proxy); + if(matrix.e!==20 || !(matrix instanceof realm.DOMMatrix)) throw Error('native proxy snapshot'); + matrix.e=80; + if(get.call(ctx).e!==20) throw Error('snapshot mutated paint state'); + let rejected=false; + try {get.call(new Proxy(proxy,{}));} catch(error) {rejected=error instanceof realm.TypeError;} + if(!rejected) throw Error('author proxy accepted'); + } + ctx.beginPath(); ctx.rect(0,0,10,10); ctx.fill(); + if(ctx.getImageData(25,5,1,1).data[3]!==255 || ctx.getImageData(5,5,1,1).data[3]!==0) + throw Error('snapshot transform differs from native painting'); + ctx.__moliCanvasPathState=0; + Object.setPrototypeOf(ctx,null); Object.freeze(ctx); + if(globalThis[name].prototype.getTransform.call(ctx).e!==20) throw Error('native identity lost'); + } + return true; + })()"#, + ) + .unwrap(), + "true" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 1ecca2f9ad..aaaaee84a3 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2135,6 +2135,7 @@ mod browser_api; mod cache_interfaces; mod canvas_arguments; mod canvas_paths; +mod canvas_transform_snapshots; mod canvas_webgl; mod child_dynamic_inline_scripts; mod device_events; diff --git a/moli-renderer-v8/src/worker/thread/tests/canvas_transform_snapshots.rs b/moli-renderer-v8/src/worker/thread/tests/canvas_transform_snapshots.rs new file mode 100644 index 0000000000..8acf4d135c --- /dev/null +++ b/moli-renderer-v8/src/worker/thread/tests/canvas_transform_snapshots.rs @@ -0,0 +1,22 @@ +use super::*; + +#[tokio::test] +async fn worker_offscreen_canvas_transform_snapshots_preserve_native_state_and_identity() { + ensure_v8(); + let source = format!( + r#"const probe = {}; + const checks = probe(globalThis, globalThis, 'offscreen', 'worker'); + const point = new DOMMatrix().translate(3, 4).transformPoint(new DOMPoint(1, 2)); + if (!(point instanceof DOMPoint) || point.x !== 4 || point.y !== 6) + throw Error('worker transform result types'); + postMessage({{total: checks.length, failures: checks.filter(row => !row.passed)}}); + close();"#, + include_str!("../../../script_vm/tests/canvas_transform_snapshots.js"), + ); + let mut worker = spawn_worker(source, "test://canvas_transform_snapshots".into()); + let message = timeout(TIMEOUT, worker.recv()) + .await + .expect("timed out") + .expect("channel closed"); + assert_eq!(expect_post_json(message), r#"{"total":74,"failures":[]}"#); +} diff --git a/moli-renderer-v8/src/worker/thread/tests/mod.rs b/moli-renderer-v8/src/worker/thread/tests/mod.rs index 4305cc88d8..f6d2fe628f 100644 --- a/moli-renderer-v8/src/worker/thread/tests/mod.rs +++ b/moli-renderer-v8/src/worker/thread/tests/mod.rs @@ -1534,6 +1534,9 @@ mod strict_script_mime; mod network; mod postmessage; + +mod canvas_transform_snapshots; + mod tls; mod xhr_failure;