mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 00:01:34 +00:00
fix(performance): inherit navigation timing from resource entries
This commit is contained in:
@@ -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}"#
|
||||
);
|
||||
}
|
||||
@@ -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)});
|
||||
})();
|
||||
Reference in New Issue
Block a user