diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime/entries.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime/entries.rs index 4e36de50e4..136e5a8f83 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime/entries.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime/entries.rs @@ -502,18 +502,25 @@ fn performance_resource_timing_to_json_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(snapshot) = - PerformanceResourceTimingJsonSnapshotDeclaration::from_entry(scope, args.this()) - else { + let Some(snapshot) = resource_timing_json_snapshot(scope, args.this()) else { throw_type_error(scope, "Illegal invocation"); return; }; - let snapshot = snapshot - .bind(scope) - .expect("PerformanceResourceTiming toJSON snapshot declaration should bind"); rv.set(snapshot.into()); } +pub(super) fn resource_timing_json_snapshot<'s>( + scope: &mut v8::PinScope<'s, '_>, + entry: v8::Local<'s, v8::Object>, +) -> Option> { + let snapshot = PerformanceResourceTimingJsonSnapshotDeclaration::from_entry(scope, entry)?; + Some( + snapshot + .bind(scope) + .expect("PerformanceResourceTiming toJSON snapshot declaration should bind"), + ) +} + fn performance_entry_base_attribute_getter_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs index 0fc947230a..a857310ed8 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs @@ -461,24 +461,6 @@ fn performance_navigation_legacy_type(navigation_type: &str) -> f64 { } } -const NAV_INITIATOR_TYPE_SLOT: &str = "__moliPerformanceNavigationTimingInitiatorType"; -const NAV_NEXT_HOP_PROTOCOL_SLOT: &str = "__moliPerformanceNavigationTimingNextHopProtocol"; -const NAV_WORKER_START_SLOT: &str = "__moliPerformanceNavigationTimingWorkerStart"; -const NAV_REDIRECT_START_SLOT: &str = "__moliPerformanceNavigationTimingRedirectStart"; -const NAV_REDIRECT_END_SLOT: &str = "__moliPerformanceNavigationTimingRedirectEnd"; -const NAV_FETCH_START_SLOT: &str = "__moliPerformanceNavigationTimingFetchStart"; -const NAV_DOMAIN_LOOKUP_START_SLOT: &str = "__moliPerformanceNavigationTimingDomainLookupStart"; -const NAV_DOMAIN_LOOKUP_END_SLOT: &str = "__moliPerformanceNavigationTimingDomainLookupEnd"; -const NAV_CONNECT_START_SLOT: &str = "__moliPerformanceNavigationTimingConnectStart"; -const NAV_CONNECT_END_SLOT: &str = "__moliPerformanceNavigationTimingConnectEnd"; -const NAV_SECURE_CONNECTION_START_SLOT: &str = - "__moliPerformanceNavigationTimingSecureConnectionStart"; -const NAV_REQUEST_START_SLOT: &str = "__moliPerformanceNavigationTimingRequestStart"; -const NAV_RESPONSE_START_SLOT: &str = "__moliPerformanceNavigationTimingResponseStart"; -const NAV_RESPONSE_END_SLOT: &str = "__moliPerformanceNavigationTimingResponseEnd"; -const NAV_TRANSFER_SIZE_SLOT: &str = "__moliPerformanceNavigationTimingTransferSize"; -const NAV_ENCODED_BODY_SIZE_SLOT: &str = "__moliPerformanceNavigationTimingEncodedBodySize"; -const NAV_DECODED_BODY_SIZE_SLOT: &str = "__moliPerformanceNavigationTimingDecodedBodySize"; const NAV_UNLOAD_EVENT_START_SLOT: &str = "__moliPerformanceNavigationTimingUnloadEventStart"; const NAV_UNLOAD_EVENT_END_SLOT: &str = "__moliPerformanceNavigationTimingUnloadEventEnd"; const NAV_DOM_INTERACTIVE_SLOT: &str = "__moliPerformanceNavigationTimingDomInteractive"; @@ -493,23 +475,6 @@ const NAV_TYPE_SLOT: &str = "__moliPerformanceNavigationTimingType"; const NAV_REDIRECT_COUNT_SLOT: &str = "__moliPerformanceNavigationTimingRedirectCount"; const PERFORMANCE_NAVIGATION_TIMING_ATTRIBUTE_SLOTS: &[&str] = &[ - NAV_INITIATOR_TYPE_SLOT, - NAV_NEXT_HOP_PROTOCOL_SLOT, - NAV_WORKER_START_SLOT, - NAV_REDIRECT_START_SLOT, - NAV_REDIRECT_END_SLOT, - NAV_FETCH_START_SLOT, - NAV_DOMAIN_LOOKUP_START_SLOT, - NAV_DOMAIN_LOOKUP_END_SLOT, - NAV_CONNECT_START_SLOT, - NAV_CONNECT_END_SLOT, - NAV_SECURE_CONNECTION_START_SLOT, - NAV_REQUEST_START_SLOT, - NAV_RESPONSE_START_SLOT, - NAV_RESPONSE_END_SLOT, - NAV_TRANSFER_SIZE_SLOT, - NAV_ENCODED_BODY_SIZE_SLOT, - NAV_DECODED_BODY_SIZE_SLOT, NAV_UNLOAD_EVENT_START_SLOT, NAV_UNLOAD_EVENT_END_SLOT, NAV_DOM_INTERACTIVE_SLOT, @@ -525,40 +490,6 @@ const PERFORMANCE_NAVIGATION_TIMING_ATTRIBUTE_SLOTS: &[&str] = &[ #[derive(WebApiObject)] #[webapi(interface = web_api_interfaces::PerformanceNavigationTiming)] struct PerformanceNavigationTimingSlotDeclaration { - #[webapi(slot = NAV_INITIATOR_TYPE_SLOT, constructor_default = "navigation")] - initiator_type: &'static str, - #[webapi(slot = NAV_NEXT_HOP_PROTOCOL_SLOT, constructor_default = "")] - next_hop_protocol: &'static str, - #[webapi(slot = NAV_WORKER_START_SLOT, constructor_default)] - worker_start: f64, - #[webapi(slot = NAV_REDIRECT_START_SLOT, constructor_default)] - redirect_start: f64, - #[webapi(slot = NAV_REDIRECT_END_SLOT, constructor_default)] - redirect_end: f64, - #[webapi(slot = NAV_FETCH_START_SLOT, constructor_default)] - fetch_start: f64, - #[webapi(slot = NAV_DOMAIN_LOOKUP_START_SLOT, constructor_default)] - domain_lookup_start: f64, - #[webapi(slot = NAV_DOMAIN_LOOKUP_END_SLOT, constructor_default)] - domain_lookup_end: f64, - #[webapi(slot = NAV_CONNECT_START_SLOT, constructor_default)] - connect_start: f64, - #[webapi(slot = NAV_CONNECT_END_SLOT, constructor_default)] - connect_end: f64, - #[webapi(slot = NAV_SECURE_CONNECTION_START_SLOT, constructor_default)] - secure_connection_start: f64, - #[webapi(slot = NAV_REQUEST_START_SLOT, constructor_default)] - request_start: f64, - #[webapi(slot = NAV_RESPONSE_START_SLOT, constructor_default)] - response_start: f64, - #[webapi(slot = NAV_RESPONSE_END_SLOT, constructor_default)] - response_end: f64, - #[webapi(slot = NAV_TRANSFER_SIZE_SLOT, constructor_default)] - transfer_size: f64, - #[webapi(slot = NAV_ENCODED_BODY_SIZE_SLOT, constructor_default)] - encoded_body_size: f64, - #[webapi(slot = NAV_DECODED_BODY_SIZE_SLOT, constructor_default)] - decoded_body_size: f64, #[webapi(slot = NAV_UNLOAD_EVENT_START_SLOT, constructor_default)] unload_event_start: f64, #[webapi(slot = NAV_UNLOAD_EVENT_END_SLOT, constructor_default)] @@ -579,175 +510,73 @@ struct PerformanceNavigationTimingSlotDeclaration { navigation_type: &'static str, #[webapi(slot = NAV_REDIRECT_COUNT_SLOT, constructor_default)] redirect_count: f64, - #[webapi(method, name = "toJSON", length = 0, callback = performance_navigation_timing_to_json_callback)] - to_json: (), } #[derive(WebApiFunctionTemplate)] -#[webapi(interface = web_api_interfaces::PerformanceNavigationTiming, enumerable)] +#[webapi(interface = web_api_interfaces::PerformanceNavigationTiming, enumerable, receiver)] struct PerformanceNavigationTimingPrototypeAccessorsDeclaration { #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 0) )] - initiator_type: (), + unload_event_start: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 1) )] - next_hop_protocol: (), + unload_event_end: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 2) )] - worker_start: (), + dom_interactive: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 3) )] - redirect_start: (), + dom_content_loaded_event_start: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 4) )] - redirect_end: (), + dom_content_loaded_event_end: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 5) )] - fetch_start: (), + dom_complete: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 6) )] - domain_lookup_start: (), + load_event_start: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 7) )] - domain_lookup_end: (), + load_event_end: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 8) )] - connect_start: (), + r#type: (), #[webapi( accessor_property, getter = performance_navigation_timing_attribute_getter_callback, data = callback_data_index_value(scope, 9) )] - connect_end: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 10) - )] - secure_connection_start: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 11) - )] - request_start: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 12) - )] - response_start: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 13) - )] - response_end: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 14) - )] - transfer_size: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 15) - )] - encoded_body_size: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 16) - )] - decoded_body_size: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 17) - )] - unload_event_start: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 18) - )] - unload_event_end: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 19) - )] - dom_interactive: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 20) - )] - dom_content_loaded_event_start: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 21) - )] - dom_content_loaded_event_end: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 22) - )] - dom_complete: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 23) - )] - load_event_start: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 24) - )] - load_event_end: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 25) - )] - r#type: (), - #[webapi( - accessor_property, - getter = performance_navigation_timing_attribute_getter_callback, - data = callback_data_index_value(scope, 26) - )] redirect_count: (), + #[webapi(method, name = "toJSON", length = 0, callback = performance_navigation_timing_to_json_callback)] + to_json: (), } fn performance_navigation_entry_name<'s>( @@ -776,6 +605,17 @@ fn create_navigation_performance_entry<'s>( name: &str, ) -> v8::Local<'s, v8::Object> { let entry = create_performance_entry(scope, "navigation", name, 0.0, 0.0, None); + super::entries::initialize_resource_timing_slots( + scope, + entry, + "navigation", + 0.0, + 0.0, + 0.0, + "non-blocking", + 0.0, + "", + ); PerformanceNavigationTimingSlotDeclaration::new(performance_navigation_timing_type( navigation_type, )) @@ -1612,43 +1452,31 @@ fn performance_navigation_timing_to_json_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let output = object_json_snapshot( - scope, - args.this(), - &[ - "name", - "entryType", - "startTime", - "duration", - "initiatorType", - "nextHopProtocol", - "workerStart", - "redirectStart", - "redirectEnd", - "fetchStart", - "domainLookupStart", - "domainLookupEnd", - "connectStart", - "connectEnd", - "secureConnectionStart", - "requestStart", - "responseStart", - "responseEnd", - "transferSize", - "encodedBodySize", - "decodedBodySize", - "unloadEventStart", - "unloadEventEnd", - "domInteractive", - "domContentLoadedEventStart", - "domContentLoadedEventEnd", - "domComplete", - "loadEventStart", - "loadEventEnd", - "type", - "redirectCount", - ], - ); + let Some(output) = super::entries::resource_timing_json_snapshot(scope, args.this()) else { + throw_type_error(scope, "Illegal invocation"); + return; + }; + // Inherited and navigation-only attributes are serialized from the same + // native slots used by their getters, without consulting author properties. + for (name, slot) in [ + "unloadEventStart", + "unloadEventEnd", + "domInteractive", + "domContentLoadedEventStart", + "domContentLoadedEventEnd", + "domComplete", + "loadEventStart", + "loadEventEnd", + "type", + "redirectCount", + ] + .iter() + .zip(PERFORMANCE_NAVIGATION_TIMING_ATTRIBUTE_SLOTS) + { + let value = get_private_value(scope, args.this(), slot) + .expect("native PerformanceNavigationTiming should retain its attributes"); + let _ = output.create_data_property(scope, v8str(scope, name).into(), value); + } rv.set(output.into()); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index ad7bbc5829..cd52e19d4b 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -473,7 +473,8 @@ fn performance_navigation_timing_constructor_matches_navigation_entries() { ]; const descriptorStable = name => { const descriptor = - Object.getOwnPropertyDescriptor(PerformanceNavigationTiming.prototype, name); + Object.getOwnPropertyDescriptor(PerformanceNavigationTiming.prototype, name) + ?? Object.getOwnPropertyDescriptor(PerformanceResourceTiming.prototype, name); return !!descriptor && typeof descriptor.get === "function" && descriptor.get.name === `get ${name}` @@ -494,6 +495,9 @@ fn performance_navigation_timing_constructor_matches_navigation_entries() { name: navigation && navigation.name, entryType: navigation && navigation.entryType, isNavigationTiming: navigation instanceof PerformanceNavigationTiming, + isResourceTiming: navigation instanceof PerformanceResourceTiming, + constructorParent: Object.getPrototypeOf(PerformanceNavigationTiming) === PerformanceResourceTiming, + prototypeParent: Object.getPrototypeOf(PerformanceNavigationTiming.prototype) === PerformanceResourceTiming.prototype, inheritsPerformanceEntry: PerformanceNavigationTiming.prototype instanceof PerformanceEntry, prototypeAttributeNames: Object.getOwnPropertyNames(PerformanceNavigationTiming.prototype) .filter(name => attributeNames.includes(name)) @@ -508,7 +512,7 @@ fn performance_navigation_timing_constructor_matches_navigation_entries() { assert_eq!( result, - r#"{"ctor":"function","name":"https://performance-navigation-timing.test/","entryType":"navigation","isNavigationTiming":true,"inheritsPerformanceEntry":true,"prototypeAttributeNames":"initiatorType,nextHopProtocol,workerStart,redirectStart,redirectEnd,fetchStart,domainLookupStart,domainLookupEnd,connectStart,connectEnd,secureConnectionStart,requestStart,responseStart,responseEnd,transferSize,encodedBodySize,decodedBodySize,unloadEventStart,unloadEventEnd,domInteractive,domContentLoadedEventStart,domContentLoadedEventEnd,domComplete,loadEventStart,loadEventEnd,type,redirectCount","descriptorsStable":true,"constructError":"TypeError"}"# + r#"{"ctor":"function","name":"https://performance-navigation-timing.test/","entryType":"navigation","isNavigationTiming":true,"isResourceTiming":true,"constructorParent":true,"prototypeParent":true,"inheritsPerformanceEntry":true,"prototypeAttributeNames":"unloadEventStart,unloadEventEnd,domInteractive,domContentLoadedEventStart,domContentLoadedEventEnd,domComplete,loadEventStart,loadEventEnd,type,redirectCount","descriptorsStable":true,"constructError":"TypeError"}"# ); } @@ -648,8 +652,8 @@ fn performance_entries_hide_backing_slots_and_ignore_spoofing() { byRealMark: performance.getEntriesByName("real-mark", "mark").length, bySpoofMark: performance.getEntriesByName("spoof", "resource").length, fakeName: getterResult(entryNameGetter, fake), - fakeNavigationType: String(navTypeGetter.call(fake)), - fakeLoadEventEnd: String(navLoadGetter.call(fake)), + fakeNavigationType: getterResult(navTypeGetter, fake), + fakeLoadEventEnd: getterResult(navLoadGetter, fake), fakeResourceValues: resourceGetters.map(getter => getterResult(getter, {})).join("|") }); })() @@ -659,7 +663,7 @@ fn performance_entries_hide_backing_slots_and_ignore_spoofing() { assert_eq!( result, - r#"{"initialNavigationNames":[],"initialMarkNames":[],"initialMeasureNames":[],"markName":"real-mark","markEntryType":"mark","markStartSpoofIgnored":true,"markDuration":0,"markDetailNull":true,"measureDetail":"real","entryDescriptors":["name:true:function:get name:0:undefined:true:true:false","entryType:true:function:get entryType:0:undefined:true:true:false","startTime:true:function:get startTime:0:undefined:true:true:false","duration:true:function:get duration:0:undefined:true:true:false"],"detailDescriptors":["detail:true:function:get detail:0:undefined:true:true:false","detail:true:function:get detail:0:undefined:true:true:false"],"resourceDescriptors":["initiatorType:true:function:get initiatorType:0:undefined:true:true:false","nextHopProtocol:true:function:get nextHopProtocol:0:undefined:true:true:false","workerStart:true:function:get workerStart:0:undefined:true:true:false","redirectStart:true:function:get redirectStart:0:undefined:true:true:false","redirectEnd:true:function:get redirectEnd:0:undefined:true:true:false","fetchStart:true:function:get fetchStart:0:undefined:true:true:false","domainLookupStart:true:function:get domainLookupStart:0:undefined:true:true:false","domainLookupEnd:true:function:get domainLookupEnd:0:undefined:true:true:false","connectStart:true:function:get connectStart:0:undefined:true:true:false","connectEnd:true:function:get connectEnd:0:undefined:true:true:false","secureConnectionStart:true:function:get secureConnectionStart:0:undefined:true:true:false","requestStart:true:function:get requestStart:0:undefined:true:true:false","responseStart:true:function:get responseStart:0:undefined:true:true:false","responseEnd:true:function:get responseEnd:0:undefined:true:true:false","transferSize:true:function:get transferSize:0:undefined:true:true:false","encodedBodySize:true:function:get encodedBodySize:0:undefined:true:true:false","decodedBodySize:true:function:get decodedBodySize:0:undefined:true:true:false","renderBlockingStatus:true:function:get renderBlockingStatus:0:undefined:true:true:false","responseStatus:true:function:get responseStatus:0:undefined:true:true:false","contentType:true:function:get contentType:0:undefined:true:true:false"],"navigationType":"navigate","navigationLoadEventEnd":0,"navigationDuration":0,"byRealMark":1,"bySpoofMark":0,"fakeName":"TypeError","fakeNavigationType":"undefined","fakeLoadEventEnd":"undefined","fakeResourceValues":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError"}"# + r#"{"initialNavigationNames":[],"initialMarkNames":[],"initialMeasureNames":[],"markName":"real-mark","markEntryType":"mark","markStartSpoofIgnored":true,"markDuration":0,"markDetailNull":true,"measureDetail":"real","entryDescriptors":["name:true:function:get name:0:undefined:true:true:false","entryType:true:function:get entryType:0:undefined:true:true:false","startTime:true:function:get startTime:0:undefined:true:true:false","duration:true:function:get duration:0:undefined:true:true:false"],"detailDescriptors":["detail:true:function:get detail:0:undefined:true:true:false","detail:true:function:get detail:0:undefined:true:true:false"],"resourceDescriptors":["initiatorType:true:function:get initiatorType:0:undefined:true:true:false","nextHopProtocol:true:function:get nextHopProtocol:0:undefined:true:true:false","workerStart:true:function:get workerStart:0:undefined:true:true:false","redirectStart:true:function:get redirectStart:0:undefined:true:true:false","redirectEnd:true:function:get redirectEnd:0:undefined:true:true:false","fetchStart:true:function:get fetchStart:0:undefined:true:true:false","domainLookupStart:true:function:get domainLookupStart:0:undefined:true:true:false","domainLookupEnd:true:function:get domainLookupEnd:0:undefined:true:true:false","connectStart:true:function:get connectStart:0:undefined:true:true:false","connectEnd:true:function:get connectEnd:0:undefined:true:true:false","secureConnectionStart:true:function:get secureConnectionStart:0:undefined:true:true:false","requestStart:true:function:get requestStart:0:undefined:true:true:false","responseStart:true:function:get responseStart:0:undefined:true:true:false","responseEnd:true:function:get responseEnd:0:undefined:true:true:false","transferSize:true:function:get transferSize:0:undefined:true:true:false","encodedBodySize:true:function:get encodedBodySize:0:undefined:true:true:false","decodedBodySize:true:function:get decodedBodySize:0:undefined:true:true:false","renderBlockingStatus:true:function:get renderBlockingStatus:0:undefined:true:true:false","responseStatus:true:function:get responseStatus:0:undefined:true:true:false","contentType:true:function:get contentType:0:undefined:true:true:false"],"navigationType":"navigate","navigationLoadEventEnd":0,"navigationDuration":0,"byRealMark":1,"bySpoofMark":0,"fakeName":"TypeError","fakeNavigationType":"TypeError","fakeLoadEventEnd":"TypeError","fakeResourceValues":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError"}"# ); } @@ -1280,7 +1284,7 @@ fn performance_to_json_returns_declared_snapshot_objects() { toJsonDescriptors: [ methodSummary(performance.timing, "toJSON"), methodSummary(performance.navigation, "toJSON"), - methodSummary(navigationEntry, "toJSON") + methodSummary(PerformanceNavigationTiming.prototype, "toJSON") ] }); })() @@ -1290,7 +1294,7 @@ fn performance_to_json_returns_declared_snapshot_objects() { assert_eq!( result, - r#"{"timeOriginNumber":true,"timeOriginEnumerable":true,"timingEnumerable":true,"navigationEnumerable":true,"timingSnapshot":true,"navigationSnapshot":true,"navigationEntrySnapshot":true,"timingOwn":true,"navigationOwn":true,"toJsonDescriptors":["toJSON:function:toJSON:0:false:true:true:true","toJSON:function:toJSON:0:false:true:true:true","toJSON:function:toJSON:0:false:true:true:true"]}"# + r#"{"timeOriginNumber":true,"timeOriginEnumerable":true,"timingEnumerable":true,"navigationEnumerable":true,"timingSnapshot":true,"navigationSnapshot":true,"navigationEntrySnapshot":true,"timingOwn":true,"navigationOwn":true,"toJsonDescriptors":["toJSON:function:toJSON:0:false:true:true:true","toJSON:function:toJSON:0:false:true:true:true","toJSON:function:toJSON:0:true:true:true:true"]}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 357b88a3e8..fb1c2098aa 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -15233,3 +15233,5 @@ mod window_execution_context; mod string_timers; mod history_replace_forward; + +mod navigation_timing_inheritance; diff --git a/moli-renderer-v8/src/script_vm/tests/navigation_timing_inheritance.rs b/moli-renderer-v8/src/script_vm/tests/navigation_timing_inheritance.rs new file mode 100644 index 0000000000..f8f6f5237d --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/navigation_timing_inheritance.rs @@ -0,0 +1,106 @@ +use super::http_fixture::StaticHttpServer; +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn navigation_timing_inherits_native_resource_bindings_across_realms() { + let server = StaticHttpServer::spawn_with_bodies(vec!["resource".to_owned()]).await; + let resource_url = server.base_url().join("resource").unwrap(); + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap(); + let mut vm = + new_storage_page_task_executor_test_vm_with_loader(server.base_url().as_str(), &loader); + vm.eval(&format!( + "globalThis.__navigationInheritanceResult = null; globalThis.__navigationResourceUrl = {};", + serde_json::to_string(resource_url.as_str()).unwrap() + )) + .unwrap(); + let fixture = include_str!("../../../tests/fixtures/navigation-timing-inheritance.js"); + vm.eval(&format!( + "({}).then(result => __navigationInheritanceResult = result, error => __navigationInheritanceResult = String(error));", + fixture.trim().trim_end_matches(';') + )) + .unwrap(); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__navigationInheritanceResult !== null)", + "true", + "navigation timing native inheritance and receiver checks", + ) + .await; + let result = vm.eval("__navigationInheritanceResult").unwrap(); + let result: serde_json::Value = serde_json::from_str(&result).unwrap_or_else(|error| { + panic!("navigation inheritance probe returned {result:?}: {error}") + }); + assert_eq!(result["total"], 1540); + assert_eq!(result["failures"], serde_json::json!([])); + assert_eq!(server.finish_targets().await, ["/resource"]); +} + +#[test] +fn navigation_timing_json_reads_native_inherited_state_after_freezing_and_lifecycle() { + let mut vm = new_storage_test_vm("https://navigation-timing-native.test/"); + vm.eval( + r#" + globalThis.navigationEntry = performance.getEntriesByType('navigation')[0]; + globalThis.nativeNavigationSnapshot = () => + PerformanceNavigationTiming.prototype.toJSON.call(navigationEntry); + globalThis.initialNavigationSnapshot = nativeNavigationSnapshot(); + globalThis.navigationAuthorReads = 0; + for (const name of Object.keys(initialNavigationSnapshot)) { + Object.defineProperty(navigationEntry, name, { + get() { navigationAuthorReads++; throw new Error('author getter called'); } + }); + } + Object.setPrototypeOf(navigationEntry, null); + Object.freeze(navigationEntry); + 'ready'; + "#, + ) + .unwrap(); + vm.dispatch_document_lifecycle_event("DOMContentLoaded") + .unwrap(); + vm.dispatch_window_load_event().unwrap(); + let result = vm + .eval( + r#" + (() => { + const json = nativeNavigationSnapshot(); + const base = PerformanceEntry.prototype.toJSON.call(navigationEntry); + const resource = PerformanceResourceTiming.prototype.toJSON.call(navigationEntry); + const read = (prototype, key) => + Object.getOwnPropertyDescriptor(prototype, key).get.call(navigationEntry); + return JSON.stringify({ + identity: ['name', 'entryType', 'startTime'].every(key => + json[key] === initialNavigationSnapshot[key] + && base[key] === json[key] && resource[key] === json[key] + && read(PerformanceEntry.prototype, key) === json[key]), + inheritedContentType: json.contentType === '' && resource.contentType === '' + && read(PerformanceResourceTiming.prototype, 'contentType') === '', + // Web IDL's default serializer uses the interface declaring + // the operation, including that interface's inherited members. + interfaceKeySets: !Object.hasOwn(base, 'initiatorType') + && !Object.hasOwn(base, 'loadEventEnd') + && resource.initiatorType === 'navigation' + && !Object.hasOwn(resource, 'loadEventEnd'), + lifecycleUpdated: initialNavigationSnapshot.loadEventEnd === 0 + && json.domInteractive > 0 + && json.domContentLoadedEventStart >= json.domInteractive + && json.domContentLoadedEventEnd >= json.domContentLoadedEventStart + && json.domComplete >= json.domContentLoadedEventEnd + && json.loadEventStart >= json.domComplete + && json.loadEventEnd >= json.loadEventStart + && json.duration === json.loadEventEnd, + snapshotsAgree: Object.keys(base).every(key => base[key] === json[key]) + && Object.keys(resource).every(key => resource[key] === json[key]), + authorReads: navigationAuthorReads, + frozen: Object.isFrozen(navigationEntry) + }); + })() + "#, + ) + .unwrap(); + assert_eq!( + result, + r#"{"identity":true,"inheritedContentType":true,"interfaceKeySets":true,"lifecycleUpdated":true,"snapshotsAgree":true,"authorReads":0,"frozen":true}"# + ); +} diff --git a/moli-renderer-v8/src/web_api_interfaces.rs b/moli-renderer-v8/src/web_api_interfaces.rs index 06b67a5dbe..bdecde0402 100644 --- a/moli-renderer-v8/src/web_api_interfaces.rs +++ b/moli-renderer-v8/src/web_api_interfaces.rs @@ -323,7 +323,7 @@ interfaces! { PerformanceMark: PerformanceEntry; PerformanceMeasure: PerformanceEntry; PerformanceNavigation; - PerformanceNavigationTiming: PerformanceEntry; + PerformanceNavigationTiming: PerformanceResourceTiming; PerformanceObserver; PerformanceObserverEntryList; PerformanceResourceTiming: PerformanceEntry; diff --git a/moli-renderer-v8/tests/fixtures/navigation-timing-inheritance.js b/moli-renderer-v8/tests/fixtures/navigation-timing-inheritance.js new file mode 100644 index 0000000000..8a5708ee68 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/navigation-timing-inheritance.js @@ -0,0 +1,161 @@ +(async () => { + const rows = []; + const mainNavigation = performance.getEntriesByType('navigation')[0]; + const assert = (condition, message) => { if (!condition) throw new Error(message); }; + const check = (name, run) => { + try { run(); rows.push({name, passed: true}); } + catch (error) { rows.push({name, passed: false, error: String(error)}); } + }; + const baseNames = ['name', 'entryType', 'startTime', 'duration']; + const resourceNames = ['initiatorType', 'nextHopProtocol', 'workerStart', 'redirectStart', 'redirectEnd', + 'fetchStart', 'domainLookupStart', 'domainLookupEnd', 'connectStart', 'connectEnd', 'secureConnectionStart', + 'requestStart', 'responseStart', 'responseEnd', 'transferSize', 'encodedBodySize', 'decodedBodySize', + 'renderBlockingStatus', 'responseStatus']; + // contentType is covered by the Rust regression; the reference browser + // does not expose it yet. + const navigationNames = ['unloadEventStart', 'unloadEventEnd', 'domInteractive', 'domContentLoadedEventStart', + 'domContentLoadedEventEnd', 'domComplete', 'loadEventStart', 'loadEventEnd', 'type', 'redirectCount']; + const allNames = [...baseNames, ...resourceNames, ...navigationNames]; + let observer; + const resourceReady = new Promise(resolve => { + observer = new PerformanceObserver(list => { + const resource = list.getEntriesByName(__navigationResourceUrl, 'resource')[0]; + if (resource) { observer.disconnect(); resolve(resource); } + }); + observer.observe({type: 'resource'}); + }); + let resource; + try { + await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', __navigationResourceUrl); + xhr.onload = () => { + if (xhr.status === 200 && xhr.responseText === 'resource') resolve(); + else reject(new Error('resource response body')); + }; + xhr.onerror = () => reject(new Error('resource request failed')); + xhr.send(); + }); + resource = await resourceReady; + } finally { observer.disconnect(); } + const frame = document.body.appendChild(document.createElement('iframe')); + const realms = {main: window, child: frame.contentWindow}; + const retained = []; + try { + for (const [ownerName, owner] of Object.entries(realms)) { + const navigation = owner.performance.getEntriesByType('navigation')[0]; + assert(navigation, 'navigation entry exists'); + const initial = Object.fromEntries(allNames.map(name => [name, navigation[name]])); + retained.push({navigation, owner, initial}); + check(`${ownerName} native interface inheritance`, () => { + assert(Object.getPrototypeOf(owner.PerformanceNavigationTiming) === owner.PerformanceResourceTiming, 'constructor parent'); + assert(Object.getPrototypeOf(owner.PerformanceNavigationTiming.prototype) === owner.PerformanceResourceTiming.prototype, 'prototype parent'); + assert(navigation instanceof owner.PerformanceNavigationTiming && navigation instanceof owner.PerformanceResourceTiming && navigation instanceof owner.PerformanceEntry, 'instance chain'); + assert(Object.getOwnPropertyNames(navigation).length === 0, 'no own IDL fields or methods'); + for (const name of resourceNames) assert(!Object.hasOwn(owner.PerformanceNavigationTiming.prototype, name), 'resource members are inherited'); + assert(Object.prototype.toString.call(navigation) === '[object PerformanceNavigationTiming]', 'most-derived interface tag'); + }); + for (const name of allNames) check(`${ownerName} ${name} readonly native value`, () => { + const value = navigation[name]; + assert(value !== undefined, 'complete initialized attribute'); + const written = Reflect.set(navigation, name, null); + if (written) Reflect.set(navigation, name, value); + assert(!written && navigation[name] === value, 'readonly attribute'); + }); + for (const [calleeName, callee] of Object.entries(realms)) { + for (const [iface, names] of [ + ['PerformanceEntry', baseNames], + ['PerformanceResourceTiming', resourceNames], + ['PerformanceNavigationTiming', navigationNames], + ]) { + const prototype = callee[iface].prototype; + const methods = names.map(name => [name, () => Object.getOwnPropertyDescriptor(prototype, name)?.get]); + methods.push(['toJSON', () => prototype.toJSON]); + for (const [name, method] of methods) { + const label = `${ownerName} ${iface}.${name} via ${calleeName}`; + check(`${label} genuine navigation receiver`, () => { + const descriptor = Object.getOwnPropertyDescriptor(prototype, name), fn = method(); + assert(descriptor?.enumerable && descriptor.configurable, 'enumerable configurable prototype binding'); + assert(typeof fn === 'function' && fn.length === 0, 'zero-argument native binding'); + if (name === 'toJSON') { + assert(fn.name === 'toJSON' && descriptor.writable, 'operation descriptor'); + const json = fn.call(navigation); + assert(Object.getPrototypeOf(json) === callee.Object.prototype, 'result belongs to binding realm'); + const expected = iface === 'PerformanceEntry' ? baseNames : iface === 'PerformanceResourceTiming' ? [...baseNames, ...resourceNames] : allNames; + for (const key of expected) { + const data = Object.getOwnPropertyDescriptor(json, key); + assert(data?.writable && data.enumerable && data.configurable && data.value === initial[key], `native JSON ${key}`); + } + // The interface-specific JSON key set is checked separately + // against Web IDL; Chromium includes derived attributes here. + json.name = 'changed'; + assert(navigation.name === initial.name, 'snapshot is independent'); + } else { + assert(descriptor.set === undefined && fn.name === `get ${name}`, 'readonly getter signature'); + assert(fn.call(navigation) === initial[name], 'borrowed getter sees canonical native data'); + } + }); + let calls = 0; + const poison = () => { calls++; throw new Error('author trap ran'); }; + const revoked = Proxy.revocable(navigation, {}); revoked.revoke(); + const invalid = [null, undefined, {}, prototype, Object.create(navigation), + new Proxy(navigation, {get: poison, getPrototypeOf: poison, has: poison}), revoked.proxy]; + if (iface === 'PerformanceNavigationTiming') invalid.push(resource, new owner.PerformanceMark('wrong-subtype')); + else if (iface === 'PerformanceResourceTiming') invalid.push(new owner.PerformanceMark('wrong-subtype')); + invalid.forEach((receiver, index) => check(`${label} rejects receiver ${index}`, () => { + const fn = method(); + assert(typeof fn === 'function', 'prototype binding exists'); + calls = 0; + let error; try { fn.call(receiver); } catch (caught) { error = caught; } + assert(error instanceof callee.TypeError && calls === 0, 'binding-realm TypeError without author traps'); + })); + check(`${label} survives expando and prototype tampering`, () => { + const fn = method(); + assert(typeof fn === 'function', 'prototype binding exists'); + const saved = Object.getOwnPropertyDescriptors(navigation), originalPrototype = Object.getPrototypeOf(navigation); + try { + for (const key of allNames) Object.defineProperty(navigation, key, {get: poison, configurable: true}); + Object.setPrototypeOf(navigation, null); calls = 0; + const result = fn.call(navigation); + if (name === 'toJSON') { + const expected = iface === 'PerformanceEntry' ? baseNames : iface === 'PerformanceResourceTiming' ? [...baseNames, ...resourceNames] : allNames; + for (const key of expected) assert(result[key] === initial[key], `serialized native ${key}`); + } else assert(result === initial[name], 'native getter state'); + assert(calls === 0, 'public accessors are ignored'); + } finally { + Object.setPrototypeOf(navigation, originalPrototype); + for (const key of allNames) Reflect.deleteProperty(navigation, key); + Object.defineProperties(navigation, saved); + } + }); + } + } + check(`${ownerName} resource bindings via ${calleeName} still accept resource entries`, () => { + for (const name of resourceNames) { + const getter = Object.getOwnPropertyDescriptor(callee.PerformanceResourceTiming.prototype, name).get; + assert(getter.call(resource) === resource[name], name); + } + const json = callee.PerformanceResourceTiming.prototype.toJSON.call(resource); + assert(json.name === __navigationResourceUrl && json.initiatorType === 'xmlhttprequest', 'real loaded resource'); + assert(!Object.hasOwn(json, 'loadEventEnd'), 'resource remains separate from navigation'); + }); + } + check(`${ownerName} navigation entry is shared by timeline and buffered observer`, () => { + const observer = new owner.PerformanceObserver(() => {}); + try { + observer.observe({type: 'navigation', buffered: true}); + const records = observer.takeRecords(); + assert(records.length === 1 && records[0] === navigation, 'buffered entry identity'); + assert(owner.performance.getEntriesByName(navigation.name, 'navigation')[0] === navigation, 'timeline identity'); + if (owner === window) assert(navigation === mainNavigation, 'navigation identity survives resource loading'); + } finally { observer.disconnect(); } + }); + } + } finally { frame.remove(); } + for (const {navigation, owner, initial} of retained) check('retained entry keeps native inheritance state after frame removal', () => { + const json = owner.PerformanceNavigationTiming.prototype.toJSON.call(navigation); + assert(json.name === initial.name && json.type === initial.type && json.loadEventEnd === initial.loadEventEnd, 'retained navigation state'); + assert(owner.PerformanceResourceTiming.prototype.toJSON.call(navigation).initiatorType === 'navigation', 'retained resource state'); + }); + return JSON.stringify({total: rows.length, failures: rows.filter(row => !row.passed)}); +})();