fix(performance): inherit navigation timing from resource entries

This commit is contained in:
ldm0
2026-09-27 05:48:09 +08:00
committed by Donough Liu
parent e53b60bd7c
commit 47aff07f91
7 changed files with 342 additions and 234 deletions
@@ -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<v8::Local<'s, v8::Object>> {
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>,
@@ -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());
}
@@ -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"]}"#
);
}
@@ -15233,3 +15233,5 @@ mod window_execution_context;
mod string_timers;
mod history_replace_forward;
mod navigation_timing_inheritance;
@@ -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}"#
);
}
+1 -1
View File
@@ -323,7 +323,7 @@ interfaces! {
PerformanceMark: PerformanceEntry;
PerformanceMeasure: PerformanceEntry;
PerformanceNavigation;
PerformanceNavigationTiming: PerformanceEntry;
PerformanceNavigationTiming: PerformanceResourceTiming;
PerformanceObserver;
PerformanceObserverEntryList;
PerformanceResourceTiming: PerformanceEntry;
@@ -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)});
})();