mirror of
https://github.com/lexmount/moli.git
synced 2026-11-13 16:10:01 +00:00
3870 lines
150 KiB
Rust
3870 lines
150 KiB
Rust
use crate::content_security_policy::ContentSecurityPolicy;
|
|
|
|
use std::{
|
|
cell::RefCell,
|
|
collections::{HashMap, HashSet, VecDeque},
|
|
rc::Weak,
|
|
sync::Arc,
|
|
};
|
|
|
|
use url::Url;
|
|
|
|
mod devtools_mutations;
|
|
mod document_write;
|
|
mod dom_facade;
|
|
mod events;
|
|
mod inspector_issues;
|
|
mod lifecycle;
|
|
mod main_parser_continuation;
|
|
mod meta_refresh;
|
|
mod mutation_commands;
|
|
mod native_events;
|
|
pub(crate) use native_events::NativeEventDispatch;
|
|
mod parser_blocking;
|
|
mod parser_modulepreload;
|
|
mod query_facade;
|
|
mod runtime_core;
|
|
mod script_lifecycle;
|
|
mod script_scheduling;
|
|
mod security_policy;
|
|
#[path = "stylesheet_runtime/mod.rs"]
|
|
mod stylesheet_runtime;
|
|
mod timing;
|
|
pub(crate) use timing::{DocumentLifecycleTiming, DocumentTimingSnapshot};
|
|
|
|
#[cfg(test)]
|
|
pub(crate) use mutation_commands::ParserPostStepRuntimeWorkForTest;
|
|
use mutation_commands::{ParserPostStepRuntimeWork, TreeAdoptionPlan};
|
|
|
|
pub(super) use super::host::EventTargetHandle;
|
|
use super::{
|
|
context_bootstrap, custom_elements,
|
|
host::{
|
|
HostDocumentState, HostEventTargetRegistry, HostScriptScheduler, HostTimeoutRunResult,
|
|
HostTimeoutScheduler, SelectorDebugCounters, SelectorDebugSnapshot, create_host_event,
|
|
dispatch_public_event, dispatch_public_event_with_original_target, event_target_value,
|
|
host_event_defaults,
|
|
},
|
|
mutation_coordinator::{MutationCoordinator, RuntimeMutationOptions},
|
|
native_bridge::{self, PageScriptHost},
|
|
page_task_queue::{PageTask, PageTaskQueue, PostParseLifecycleWork, PostParsePageOwnedWork},
|
|
planning::PreparedScript,
|
|
runtime::RendererBrowserContextRuntime,
|
|
stylesheet_blocking::{
|
|
DocumentBlockingStylesheetSignature, StylesheetBlockingState, StylesheetBlockingStatus,
|
|
},
|
|
util::v8str,
|
|
};
|
|
#[cfg(test)]
|
|
use crate::dom::native::ShadowRootBindingSnapshot;
|
|
pub(crate) use crate::meta_refresh_scheduler::MetaRefreshNavigation;
|
|
use crate::{
|
|
dom::{
|
|
NodeId,
|
|
native::{
|
|
DocumentReadyState, DomHost, DomMutationEffects, DomStylesheetOwnerChangeKind,
|
|
NativeDom, NativeNodeId, Node,
|
|
},
|
|
},
|
|
frame_owner_model::FrameDocumentTaskOwner,
|
|
live_document_parser::{
|
|
DocumentParserLifetime, DocumentParserRunState, DocumentParserSession,
|
|
DocumentParserSessionControlHandle, ParserResumeOwner, ParserResumePermit,
|
|
ParserSuspensionCause,
|
|
},
|
|
module_runtime::ModuleMapKey,
|
|
network::ResourceRequestClient,
|
|
parser::{HtmlParser, ParserInputContext, ParserInputSession},
|
|
selector::{QueryEngine, SelectorError},
|
|
service_worker_runtime::ServiceWorkerClientId,
|
|
types::SubresourceResourceType,
|
|
};
|
|
pub(crate) use devtools_mutations::{
|
|
DevToolsDomChildListMutationFact, DevToolsDomMutationFact, DevToolsDomPrepublishedRemoval,
|
|
};
|
|
pub(crate) use inspector_issues::PendingInspectorIssue;
|
|
use parser_blocking::{ParserInsertionWork, PendingParserBlockingWork, PendingParserInsertion};
|
|
pub(crate) use parser_modulepreload::MainDocumentModulepreloadFetchOutcome;
|
|
pub(crate) use script_lifecycle::{
|
|
DeferredPageTask, DeferredPageTaskLane, DeferredPageTaskState, DocumentScriptLifecycle,
|
|
FollowupPageTaskDisposition, PendingMainParserDeferredScriptStart, RuntimeScriptWorkPauseKind,
|
|
RuntimeScriptWorkState, SharedRuntimeScriptWorkState, parser_prepared_script_page_owned_work,
|
|
parser_script_preparation_failure_page_owned_work,
|
|
};
|
|
pub(crate) use security_policy::{
|
|
DocumentConnectPolicySnapshot, DocumentContentSecurityPolicyCheck,
|
|
DocumentContentSecurityPolicyViolation, DocumentNavigationEmbeddingContext,
|
|
DocumentSubresourceCspKind, create_content_security_policy_violation_event,
|
|
document_content_security_policy_error_message,
|
|
};
|
|
pub(crate) use stylesheet_runtime::attribute_reprocesses_connected_stylesheet;
|
|
#[cfg(test)]
|
|
use stylesheet_runtime::{ConnectedLinkReadinessFetchOptions, ConnectedLoadParameters};
|
|
pub(crate) use stylesheet_runtime::{
|
|
ConnectedLoadCompletion, LinkedStylesheetImportGraphCompletion,
|
|
LiveStylesheetImportLoadCompletion, StylesheetImportCompletionAuthority,
|
|
fetch_complete_stylesheet_import_graph, fetch_complete_stylesheet_import_graph_from_root,
|
|
live_stylesheet_import_responses,
|
|
};
|
|
use stylesheet_runtime::{
|
|
ConnectedLoadOperation, LinkedStylesheetImportGraphs, QueuedConnectedStyleLoad,
|
|
StylesheetLinkClientIndex, StylesheetOwnerCspDisposition, StylesheetOwnerRuntimeStates,
|
|
};
|
|
pub(crate) use stylesheet_runtime::{
|
|
ConnectedStyleEventElementKind, ConnectedStyleLoadEventAdmission, ConnectedStyleLoadEventPlan,
|
|
ConnectedStyleLoadPrimeResult, InstallLinkedStylesheet, NativeModulepreloadLinkFetchOutcome,
|
|
OwnerlessStylesheetAdmissionError, PendingNativeModulepreloadLinkEvent,
|
|
PreparedConnectedStyleLoad, PreparedLinkedStylesheetResource, ReadyConnectedStyleLoad,
|
|
StylesheetLinkClientTerminal,
|
|
};
|
|
|
|
pub(super) type DomHandle = NativeNodeId;
|
|
pub(crate) type ParserStreamHandle = crate::live_document_parser::DocumentParserStreamHandle;
|
|
type ParserStreamWeakHandle = Weak<RefCell<crate::parser::DocumentStream>>;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum HtmlFragmentCustomElementUpgradeTiming {
|
|
InReturnedFragment,
|
|
AfterInsertion,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum DocumentProcessingAction {
|
|
PostParsePageOwnedWork(Box<PostParsePageOwnedWork>),
|
|
DispatchConnectedStyleLoad(ReadyConnectedStyleLoad),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum DocumentProcessingWakeSource {
|
|
InjectedPageTask,
|
|
TaskSourceLoadCompletion,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum ParseTimeWakeSource {
|
|
InjectedPageTask,
|
|
TaskSourceLoadCompletion,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum ParseTimeWakeObservation {
|
|
ReadyNow,
|
|
Arrived(ParseTimeWakeSource),
|
|
TimedOutNoReady,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum DocumentProcessingWakeObservation {
|
|
ReadyNow,
|
|
Arrived(DocumentProcessingWakeSource),
|
|
NoWake,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct PostParseOwnerReadiness {
|
|
pub(crate) should_poll_document_processing: bool,
|
|
pub(crate) blocks_page_task_pop: bool,
|
|
pub(crate) has_pending_progress_source: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum PostParseOwnerDriverStep {
|
|
Ready(Box<DocumentProcessingAction>),
|
|
NeedsContinuation,
|
|
AwaitProgress,
|
|
Idle,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ParserConnectedScriptContext {
|
|
bridge: ParserConnectedScriptBridge,
|
|
_input_context: ParserInputContext,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CurrentScriptContext {
|
|
handle: Option<DomHandle>,
|
|
parser_connected: Option<ParserConnectedScriptContext>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct CurrentScriptContextSpec {
|
|
pub(crate) handle: Option<DomHandle>,
|
|
pub(crate) parser_write_insertion_point_active: bool,
|
|
pub(crate) parser_bridge: Option<ParserConnectedScriptBridge>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct ParserInsertionController {
|
|
input_session: ParserInputSession,
|
|
parser_stream: ParserStreamWeakHandle,
|
|
}
|
|
|
|
impl std::fmt::Debug for ParserInsertionController {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ParserInsertionController")
|
|
.field("input_session", &self.input_session)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl ParserInsertionController {
|
|
pub(crate) fn for_stream(stream: &ParserStreamHandle) -> Self {
|
|
let input_session = stream.borrow().script_input_session();
|
|
Self {
|
|
input_session,
|
|
parser_stream: std::rc::Rc::downgrade(stream),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn input_session(&self) -> ParserInputSession {
|
|
self.input_session.clone()
|
|
}
|
|
|
|
pub(crate) fn with_parser_stream<R>(
|
|
&self,
|
|
operation: impl FnOnce(&crate::parser::DocumentStream) -> R,
|
|
) -> R {
|
|
let stream = self
|
|
.parser_stream
|
|
.upgrade()
|
|
.expect("synchronous parser insertion permission outlived its parser session");
|
|
let stream = stream.borrow();
|
|
operation(&stream)
|
|
}
|
|
}
|
|
|
|
/// The explicit bridge between parser-connected script execution and the
|
|
/// phase-owned parser session.
|
|
///
|
|
/// The insertion controller is deliberately limited to synchronous input
|
|
/// insertion. Pump/suspend/script-nesting transitions are reported through
|
|
/// the separate session control handle so retaining insertion permission can
|
|
/// never itself authorize parser finalization.
|
|
#[derive(Clone)]
|
|
pub(crate) struct ParserConnectedScriptBridge {
|
|
insertion_controller: ParserInsertionController,
|
|
parser_control: DocumentParserSessionControlHandle,
|
|
}
|
|
|
|
impl std::fmt::Debug for ParserConnectedScriptBridge {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ParserConnectedScriptBridge")
|
|
.field("insertion_controller", &self.insertion_controller)
|
|
.field("session_id", &self.parser_control.session_id())
|
|
.field("run_state", &self.parser_control.run_state())
|
|
.field(
|
|
"pump_session_nesting_level",
|
|
&self.parser_control.pump_session_nesting_level(),
|
|
)
|
|
.field(
|
|
"parser_script_nesting_level",
|
|
&self.parser_control.parser_script_nesting_level(),
|
|
)
|
|
.field(
|
|
"finish_request_state",
|
|
&self.parser_control.finish_request_state(),
|
|
)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl ParserConnectedScriptBridge {
|
|
#[cfg(test)]
|
|
pub(crate) fn for_stream(stream: &ParserStreamHandle) -> Self {
|
|
Self {
|
|
insertion_controller: ParserInsertionController::for_stream(stream),
|
|
parser_control: DocumentParserSessionControlHandle::new(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn for_session(parser: &DocumentParserSession) -> Option<Self> {
|
|
let stream = parser.html_stream_handle()?;
|
|
Some(Self {
|
|
insertion_controller: ParserInsertionController::for_stream(&stream),
|
|
parser_control: parser.control_handle(),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn insertion_controller(&self) -> &ParserInsertionController {
|
|
&self.insertion_controller
|
|
}
|
|
|
|
pub(crate) fn enter_parser_script_nesting(
|
|
&self,
|
|
) -> crate::live_document_parser::DocumentParserScriptNestingGuard {
|
|
self.parser_control.enter_parser_script_nesting()
|
|
}
|
|
|
|
pub(crate) fn run_state(&self) -> DocumentParserRunState {
|
|
self.parser_control.run_state()
|
|
}
|
|
|
|
pub(crate) fn suspend(&self, cause: ParserSuspensionCause) -> ParserResumePermit {
|
|
self.parser_control
|
|
.suspend(cause, ParserResumeOwner::DocumentWrite)
|
|
}
|
|
|
|
pub(crate) fn resume(&self, permit: ParserResumePermit) -> bool {
|
|
self.parser_control
|
|
.resume(permit, ParserResumeOwner::DocumentWrite)
|
|
}
|
|
|
|
pub(crate) fn begin_pump(&self) -> crate::live_document_parser::DocumentParserPumpGuard {
|
|
self.parser_control.begin_pump()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
struct StylesheetLinkPromotionTrace {
|
|
promotion_count: u64,
|
|
inspected_link_states: u64,
|
|
promoted_clients: u64,
|
|
max_indexed_link_clients: usize,
|
|
total_elapsed_us: u128,
|
|
}
|
|
|
|
struct StylesheetLifecycleState {
|
|
pending_connected_loads: VecDeque<Arc<QueuedConnectedStyleLoad>>,
|
|
/// Synthetic ready events used only by low-level ordering tests.
|
|
///
|
|
/// Async stylesheet work never enters this queue; it always publishes a
|
|
/// typed DOM-manipulation task through `task_producer`.
|
|
#[cfg(test)]
|
|
injected_ready_connected_loads: VecDeque<ReadyConnectedStyleLoad>,
|
|
/// Owners already processed through a connected mutation before the
|
|
/// one-time post-parse discovery scan.
|
|
///
|
|
/// Their event tasks may finish before that scan now that DOM work has a
|
|
/// stable Page source, so transient event residence cannot be used as the
|
|
/// discovery deduplication fact.
|
|
pre_initial_scan_processed_owners: HashSet<DomHandle>,
|
|
ready_connected_load_network_results: VecDeque<ConnectedLoadNetworkResult>,
|
|
/// Client completions accepted while applying the selected physical
|
|
/// stylesheet Networking task.
|
|
ready_stylesheet_link_client_terminals: VecDeque<StylesheetLinkClientTerminal>,
|
|
linked_stylesheet_import_graphs: LinkedStylesheetImportGraphs,
|
|
// One canonical entry owns each stylesheet-related owner's operation,
|
|
// completion/event disposition, and link state.
|
|
owner_states: StylesheetOwnerRuntimeStates,
|
|
link_client_index: StylesheetLinkClientIndex,
|
|
link_promotion_trace: StylesheetLinkPromotionTrace,
|
|
fetches: StylesheetBlockingState,
|
|
task_sender: crate::page_task_queue::RendererPageStylesheetTaskSender,
|
|
task_producer: Option<crate::page_task_queue::RendererPageStylesheetTaskProducer>,
|
|
service_worker_connected_link_context: Option<ServiceWorkerConnectedLinkContext>,
|
|
#[cfg(test)]
|
|
task_test_residence: Option<crate::page_task_queue::RendererPageStylesheetTaskTestResidence>,
|
|
}
|
|
|
|
impl StylesheetLifecycleState {
|
|
fn new(task_sender: crate::page_task_queue::RendererPageStylesheetTaskSender) -> Self {
|
|
Self {
|
|
pending_connected_loads: VecDeque::new(),
|
|
#[cfg(test)]
|
|
injected_ready_connected_loads: VecDeque::new(),
|
|
pre_initial_scan_processed_owners: HashSet::new(),
|
|
ready_connected_load_network_results: VecDeque::new(),
|
|
ready_stylesheet_link_client_terminals: VecDeque::new(),
|
|
linked_stylesheet_import_graphs: LinkedStylesheetImportGraphs::default(),
|
|
owner_states: StylesheetOwnerRuntimeStates::default(),
|
|
link_client_index: StylesheetLinkClientIndex::default(),
|
|
link_promotion_trace: StylesheetLinkPromotionTrace::default(),
|
|
fetches: StylesheetBlockingState::default(),
|
|
task_sender,
|
|
task_producer: None,
|
|
service_worker_connected_link_context: None,
|
|
#[cfg(test)]
|
|
task_test_residence: None,
|
|
}
|
|
}
|
|
|
|
fn set_service_worker_connected_link_context(
|
|
&mut self,
|
|
browser_context_runtime: RendererBrowserContextRuntime,
|
|
client_id: ServiceWorkerClientId,
|
|
) {
|
|
self.service_worker_connected_link_context = Some(ServiceWorkerConnectedLinkContext {
|
|
browser_context_runtime,
|
|
client_id,
|
|
});
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct ServiceWorkerConnectedLinkContext {
|
|
browser_context_runtime: RendererBrowserContextRuntime,
|
|
client_id: ServiceWorkerClientId,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
pub(crate) struct ConnectedStyleImportRoot {
|
|
pub(crate) owner: DomHandle,
|
|
pub(crate) stylesheet_id: crate::live_stylesheet::StylesheetId,
|
|
pub(crate) contents_revision: u64,
|
|
pub(crate) import_generation: u64,
|
|
/// Original resource URL; the live stylesheet retains the final parser base.
|
|
/// Inline roots have no resource identity to exclude from their imports.
|
|
pub(crate) root_resource_url: Option<Url>,
|
|
}
|
|
|
|
impl ConnectedStyleImportRoot {
|
|
pub(crate) fn new(
|
|
owner: DomHandle,
|
|
stylesheet: &crate::live_stylesheet::LiveStylesheetRef,
|
|
root_resource_url: Option<Url>,
|
|
) -> Self {
|
|
Self {
|
|
owner,
|
|
stylesheet_id: stylesheet.id(),
|
|
contents_revision: stylesheet.contents_revision(),
|
|
import_generation: stylesheet.import_generation(),
|
|
root_resource_url,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn matches_stylesheet(
|
|
&self,
|
|
stylesheet: &crate::live_stylesheet::LiveStylesheetRef,
|
|
) -> bool {
|
|
stylesheet.id() == self.stylesheet_id
|
|
&& stylesheet.contents_revision() == self.contents_revision
|
|
&& stylesheet.import_generation() == self.import_generation
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct ConnectedLoadNetworkResult {
|
|
pub(crate) stylesheet_fetch: Option<crate::stylesheet_blocking::StylesheetFetch>,
|
|
pub(crate) blocking_operation: Option<crate::stylesheet_blocking::StylesheetBlockingOperation>,
|
|
pub(crate) source_operation: Option<Arc<ConnectedLoadOperation>>,
|
|
pub(crate) import_roots: Vec<ConnectedStyleImportRoot>,
|
|
pub(crate) document_url: Url,
|
|
pub(crate) request_url: Url,
|
|
pub(crate) source_owners: Vec<DomHandle>,
|
|
pub(crate) resource_type: SubresourceResourceType,
|
|
pub(crate) start_unix_millis: Option<f64>,
|
|
pub(crate) origin_clean: bool,
|
|
pub(crate) result: std::result::Result<crate::protocol_types::NavigationResponse, String>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DocumentWriteExternalScriptStart {
|
|
node: DomHandle,
|
|
host_script_handle: String,
|
|
script: PreparedScript,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct SuspendedDocumentWriteInsertion {
|
|
document_handle: DomHandle,
|
|
document_incarnation: DocumentRuntimeIncarnationIdentity,
|
|
parser_bridge: ParserConnectedScriptBridge,
|
|
resume_permit: ParserResumePermit,
|
|
resume_permit_consumed: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum SuspendedDocumentWriteContinuation {
|
|
ResumeAfterCompleted {
|
|
start: DocumentWriteExternalScriptStart,
|
|
insertion: SuspendedDocumentWriteInsertion,
|
|
},
|
|
StartExternal {
|
|
start: DocumentWriteExternalScriptStart,
|
|
insertion: SuspendedDocumentWriteInsertion,
|
|
blocking_signatures_before: HashSet<DocumentBlockingStylesheetSignature>,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) enum DocumentWriteExternalScriptLoadApplication {
|
|
Applied,
|
|
SupersededDuringApplication,
|
|
RejectedStaleTarget,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct DocumentWriteScriptPreload {
|
|
request: crate::runtime::BufferedScriptPreloadRequest,
|
|
target: crate::types::DocumentWriteExternalScriptFetchTarget,
|
|
ready_completion: Option<crate::types::DocumentWriteExternalScriptLoadCompletion>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum DocumentWriteScriptRunOutcome {
|
|
Complete,
|
|
Suspend(Box<DocumentWriteExternalScriptStart>),
|
|
}
|
|
|
|
impl std::fmt::Debug for StylesheetLifecycleState {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let mut debug = f.debug_struct("StylesheetLifecycleState");
|
|
debug.field(
|
|
"pending_connected_loads_len",
|
|
&self.pending_connected_loads.len(),
|
|
);
|
|
#[cfg(test)]
|
|
debug.field(
|
|
"injected_ready_connected_loads_len",
|
|
&self.injected_ready_connected_loads.len(),
|
|
);
|
|
debug
|
|
.field("owner_states_len", &self.owner_states.len())
|
|
.field(
|
|
"indexed_stylesheet_link_clients",
|
|
&self.link_client_index.len(),
|
|
)
|
|
.field("link_promotion_trace", &self.link_promotion_trace)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum DocumentRuntimeIncarnationIdentity {
|
|
MainFrame(FrameDocumentTaskOwner),
|
|
Standalone(Arc<()>),
|
|
}
|
|
|
|
impl DocumentRuntimeIncarnationIdentity {
|
|
fn standalone() -> Self {
|
|
Self::Standalone(Arc::new(()))
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for DocumentRuntimeIncarnationIdentity {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::MainFrame(owner) => formatter.debug_tuple("MainFrame").field(owner).finish(),
|
|
Self::Standalone(token) => formatter
|
|
.debug_tuple("Standalone")
|
|
.field(&Arc::as_ptr(token))
|
|
.finish(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialEq for DocumentRuntimeIncarnationIdentity {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
match (self, other) {
|
|
(Self::MainFrame(left), Self::MainFrame(right)) => left == right,
|
|
(Self::Standalone(left), Self::Standalone(right)) => Arc::ptr_eq(left, right),
|
|
(Self::MainFrame(_), Self::Standalone(_))
|
|
| (Self::Standalone(_), Self::MainFrame(_)) => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Eq for DocumentRuntimeIncarnationIdentity {}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct ParserStepOwnerGuard {
|
|
document_incarnation: DocumentRuntimeIncarnationIdentity,
|
|
depth: usize,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
struct ParserReentryState {
|
|
active_step_owner_guard: Option<ParserStepOwnerGuard>,
|
|
custom_element_reaction_queue_active: bool,
|
|
// Counts nested synchronous parser-blocking script execution scopes,
|
|
// including load/error completion dispatched before parser resume. This
|
|
// does not describe parser-inserted ownership, deferred-script execution,
|
|
// parser pause depth, or document.write insertion-point availability.
|
|
script_nesting_level: usize,
|
|
pause_depth: usize,
|
|
insertion_session_depth: usize,
|
|
dynamic_markup_insertion_counters: HashMap<DomHandle, usize>,
|
|
}
|
|
|
|
pub(crate) struct ParserScriptNestingGuard {
|
|
runtime: *mut DocumentRuntime,
|
|
_parser_session: Option<crate::live_document_parser::DocumentParserScriptNestingGuard>,
|
|
}
|
|
|
|
pub(crate) struct ParserPauseGuard {
|
|
runtime: *mut DocumentRuntime,
|
|
}
|
|
|
|
pub(crate) struct ParserInsertionSessionGuard {
|
|
runtime: *mut DocumentRuntime,
|
|
}
|
|
|
|
impl Drop for ParserScriptNestingGuard {
|
|
fn drop(&mut self) {
|
|
// SAFETY: The guard is created from `DocumentRuntime::enter_parser_script_nesting`
|
|
// and is only used on the renderer owner thread while the owning
|
|
// `DocumentRuntime` remains alive. The raw pointer avoids holding a
|
|
// broad mutable borrow across V8 script execution, which can re-enter
|
|
// the runtime through DOM callbacks.
|
|
unsafe { &mut *self.runtime }.exit_parser_script_nesting();
|
|
}
|
|
}
|
|
|
|
impl Drop for ParserPauseGuard {
|
|
fn drop(&mut self) {
|
|
// SAFETY: The guard is created from `DocumentRuntime::enter_parser_pause`
|
|
// and is scoped to the renderer owner thread. The raw pointer avoids
|
|
// holding a broad mutable borrow while V8 executes user constructor code.
|
|
unsafe { &mut *self.runtime }.exit_parser_pause();
|
|
}
|
|
}
|
|
|
|
impl Drop for ParserInsertionSessionGuard {
|
|
fn drop(&mut self) {
|
|
// SAFETY: The guard is created from `DocumentRuntime::enter_parser_insertion_session`
|
|
// and stays on the renderer owner thread. The raw pointer avoids
|
|
// holding a broad mutable borrow across parser pumping and script handoff.
|
|
unsafe { &mut *self.runtime }.exit_parser_insertion_session();
|
|
}
|
|
}
|
|
|
|
struct StructuralMutationGuard {
|
|
runtime: *mut DocumentRuntime,
|
|
}
|
|
|
|
impl Drop for StructuralMutationGuard {
|
|
fn drop(&mut self) {
|
|
// SAFETY: The guard is created from `DocumentRuntime::enter_structural_mutation`
|
|
// and is scoped to the renderer owner thread. The raw pointer avoids
|
|
// holding a broad mutable borrow while the runtime mutates its native
|
|
// DOM storage.
|
|
unsafe { &mut *self.runtime }.exit_structural_mutation();
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub(crate) struct DocumentPolicyContainer {
|
|
pub(crate) document_referrer: String,
|
|
pub(crate) referrer_policy: Option<String>,
|
|
pub(crate) cross_origin_embedder_policy:
|
|
crate::cross_origin_isolation::CrossOriginEmbedderPolicy,
|
|
pub(crate) document_isolation_policy: crate::cross_origin_isolation::DocumentIsolationPolicy,
|
|
pub(crate) cross_origin_isolated: bool,
|
|
pub(crate) document_content_security_policies: Vec<ContentSecurityPolicy>,
|
|
pub(crate) response_content_security_policies: Vec<ContentSecurityPolicy>,
|
|
pub(crate) response_content_security_report_only_policies: Vec<ContentSecurityPolicy>,
|
|
pub(crate) content_security_reporting_endpoints:
|
|
crate::content_security_policy::ContentSecurityPolicyReportingEndpoints,
|
|
pub(crate) credentialless: bool,
|
|
pub(crate) credentialless_storage_nonce: Option<moli_storage_key::OpaqueOriginNonce>,
|
|
pub(crate) sandbox: DocumentSandboxPolicy,
|
|
pub(crate) permissions_policy: crate::permissions_policy::DocumentPermissionsPolicy,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct DocumentSandboxPolicy {
|
|
pub(crate) forces_opaque_origin: bool,
|
|
pub(crate) allows_scripts: bool,
|
|
pub(crate) allows_modals: bool,
|
|
pub(crate) allows_top_navigation: bool,
|
|
pub(crate) allows_popups_to_escape: bool,
|
|
pub(crate) sandboxes_document_domain: bool,
|
|
}
|
|
|
|
impl Default for DocumentSandboxPolicy {
|
|
fn default() -> Self {
|
|
Self {
|
|
forces_opaque_origin: false,
|
|
allows_scripts: true,
|
|
allows_modals: true,
|
|
allows_top_navigation: true,
|
|
allows_popups_to_escape: false,
|
|
sandboxes_document_domain: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DocumentSandboxPolicy {
|
|
pub(crate) fn from_response_content_security_policies(
|
|
policies: &[ContentSecurityPolicy],
|
|
) -> Self {
|
|
Self {
|
|
forces_opaque_origin:
|
|
crate::content_security_policy::content_security_policy_forces_opaque_origin(
|
|
policies,
|
|
),
|
|
allows_scripts:
|
|
crate::content_security_policy::content_security_policy_sandbox_allows_scripts(
|
|
policies,
|
|
),
|
|
allows_modals:
|
|
crate::content_security_policy::content_security_policy_sandbox_allows_modals(
|
|
policies,
|
|
),
|
|
allows_top_navigation:
|
|
crate::content_security_policy::content_security_policy_sandbox_allows_top_navigation(
|
|
policies,
|
|
),
|
|
allows_popups_to_escape:
|
|
crate::content_security_policy::content_security_policy_sandbox_allows_popups_to_escape(
|
|
policies,
|
|
),
|
|
sandboxes_document_domain:
|
|
crate::content_security_policy::content_security_policy_sandboxes_document_domain(
|
|
policies,
|
|
),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn with_response_content_security_policy(mut self, response: Self) -> Self {
|
|
if response.sandboxes_document_domain {
|
|
self.forces_opaque_origin |= response.forces_opaque_origin;
|
|
self.allows_scripts &= response.allows_scripts;
|
|
self.allows_modals &= response.allows_modals;
|
|
self.allows_top_navigation &= response.allows_top_navigation;
|
|
self.allows_popups_to_escape = if self.sandboxes_document_domain {
|
|
self.allows_popups_to_escape && response.allows_popups_to_escape
|
|
} else {
|
|
response.allows_popups_to_escape
|
|
};
|
|
}
|
|
self.sandboxes_document_domain |= response.sandboxes_document_domain;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(super) struct DocumentRuntime {
|
|
dom_host: LiveRuntimeDomHost,
|
|
parser_reentry: ParserReentryState,
|
|
pending_parser_post_step_runtime_work: ParserPostStepRuntimeWork,
|
|
/// Used when temporarily parking live shadow-root bindings across compat replacement paths.
|
|
#[cfg(test)]
|
|
parked_live_shadow_root_bindings: Option<Vec<ShadowRootBindingSnapshot>>,
|
|
selector_engine: QueryEngine,
|
|
selector_debug: SelectorDebugCounters,
|
|
document: HostDocumentState,
|
|
timing: timing::DocumentTiming,
|
|
script_execution_control: crate::script_execution_control::RendererScriptExecutionControl,
|
|
author_styles_disabled: bool,
|
|
bypass_content_security_policy: bool,
|
|
policy_container: DocumentPolicyContainer,
|
|
delivered_meta_content_security_policies:
|
|
Arc<parking_lot::RwLock<HashMap<DomHandle, Vec<ContentSecurityPolicy>>>>,
|
|
processed_meta_content_security_policy_handles: RefCell<HashSet<(DomHandle, DomHandle)>>,
|
|
resource_loader_binding: Option<DocumentResourceLoaderBinding>,
|
|
script_context_stack: Vec<CurrentScriptContext>,
|
|
root_document_parser: Option<DocumentParserSession>,
|
|
post_parse_schedule_invalidated: bool,
|
|
stylesheet_lifecycle: StylesheetLifecycleState,
|
|
main_parser_continuation: main_parser_continuation::MainParserContinuationState,
|
|
pending_stylesheet_source_css_projection_owners: Vec<DomHandle>,
|
|
pending_connected_style_load_prime_result: ConnectedStyleLoadPrimeResult,
|
|
initial_connected_style_loads_queued: bool,
|
|
late_preload_stylesheet_handles: HashSet<DomHandle>,
|
|
in_document_image_priority_boost_count: usize,
|
|
parser_discovered_modulepreloads: HashSet<ModuleMapKey>,
|
|
modulepreload_invalid_as_link_errors: HashSet<DomHandle>,
|
|
style_source_document_sync_pending: bool,
|
|
pending_devtools_dom_mutations: Vec<devtools_mutations::DevToolsDomMutationFact>,
|
|
#[cfg(test)]
|
|
pending_runtime_binding_calls: Vec<native_bridge::PendingRuntimeBindingCall>,
|
|
pending_inspector_issues: Vec<inspector_issues::PendingInspectorIssue>,
|
|
quirks_mode_issue_reported: bool,
|
|
script_lifecycle: DocumentScriptLifecycle,
|
|
parser_script_start_positions: HashMap<DomHandle, ParserScriptStartPosition>,
|
|
timeouts: HostTimeoutScheduler,
|
|
events: HostEventTargetRegistry,
|
|
mutations: MutationCoordinator,
|
|
meta_refresh_scheduler: crate::meta_refresh_scheduler::MetaRefreshScheduler,
|
|
custom_element_reaction_depth: usize,
|
|
structural_mutation_depth: usize,
|
|
dom_content_loaded_dispatched: bool,
|
|
document_incarnation: DocumentRuntimeIncarnationIdentity,
|
|
document_input_stream_opened: bool,
|
|
next_document_write_external_script_load_id: u64,
|
|
document_write_script_preload_scanner:
|
|
Option<Box<crate::runtime::IncrementalBufferedScriptPreloadScanner>>,
|
|
/// In-flight classic script loads started by the main-document scanner.
|
|
/// The Document shares this resource residence with parser re-entry while
|
|
/// scanner/tokenizer state remains owned by phase one.
|
|
main_document_script_preloads: crate::runtime::DocumentScriptPreloadStore,
|
|
document_write_script_preloads:
|
|
HashMap<crate::runtime::BufferedScriptPreloadKey, DocumentWriteScriptPreload>,
|
|
pending_parser_blocking_work: Option<PendingParserBlockingWork>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct DocumentResourceLoaderBinding {
|
|
registry: crate::network::context::DocumentResourceLoaderRegistry,
|
|
owner: crate::native_bridge::WindowDocumentOwner,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub(crate) struct ParserScriptStartPosition {
|
|
pub(crate) line: u64,
|
|
pub(crate) column: u64,
|
|
}
|
|
|
|
/// Wraps the runtime-owned live `DomHost`.
|
|
///
|
|
/// The runtime always owns the `DomHost`; parser steps do not move it out and
|
|
/// do not put the runtime into a separate DOM-access mode. Parser callbacks
|
|
/// call renderer-owned sinks, and each sink short-borrows this same native DOM
|
|
/// object graph through `DocumentRuntime`.
|
|
///
|
|
/// The live DOM also owns the canonical media settings, shared by native parser
|
|
/// reads and Window bindings. The wrapper centralizes direct `DomHost` borrowing
|
|
/// so the parser path avoids carrying a `DomHost` pointer or borrow token through
|
|
/// helper layers.
|
|
#[derive(Debug)]
|
|
struct LiveRuntimeDomHost {
|
|
dom_host: DomHost,
|
|
emulated_media: crate::protocol_types::EmulatedMediaOverrides,
|
|
viewport_surface: Option<crate::protocol_types::ViewportSurface>,
|
|
}
|
|
|
|
impl LiveRuntimeDomHost {
|
|
fn style_viewport(&self) -> crate::style_engine::StyleViewport {
|
|
self.viewport_surface
|
|
.map(crate::style_engine::StyleViewport::from_viewport_surface)
|
|
.unwrap_or_else(|| {
|
|
crate::style_engine::StyleViewport::new(
|
|
Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.inner_width),
|
|
Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.inner_height),
|
|
)
|
|
.with_screen_size(
|
|
Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.screen_width),
|
|
Some(moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE.screen_height),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn from_dom_host(dom_host: DomHost) -> Self {
|
|
Self {
|
|
dom_host,
|
|
emulated_media: Default::default(),
|
|
viewport_surface: None,
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
fn borrow(&self) -> &DomHost {
|
|
&self.dom_host
|
|
}
|
|
|
|
#[track_caller]
|
|
fn borrow_mut(&mut self) -> &mut DomHost {
|
|
&mut self.dom_host
|
|
}
|
|
|
|
fn into_dom_host(self) -> DomHost {
|
|
self.dom_host
|
|
}
|
|
}
|
|
|
|
impl DocumentRuntime {
|
|
#[cfg(test)]
|
|
/// Exposes the synchronous parser-blocking execution depth to contract
|
|
/// tests; it is not a general measure of parser ownership or activity.
|
|
pub(crate) fn parser_script_nesting_level(&self) -> usize {
|
|
self.parser_reentry.script_nesting_level
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn parser_pause_depth(&self) -> usize {
|
|
self.parser_reentry.pause_depth
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn is_parser_paused_for_reentry(&self) -> bool {
|
|
self.parser_reentry.pause_depth > 0
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn parser_insertion_session_depth(&self) -> usize {
|
|
self.parser_reentry.insertion_session_depth
|
|
}
|
|
|
|
pub(crate) fn is_parser_insertion_session_active(&self) -> bool {
|
|
self.parser_reentry.insertion_session_depth > 0
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn active_parser_step_document_incarnation(
|
|
&self,
|
|
) -> Option<DocumentRuntimeIncarnationIdentity> {
|
|
self.parser_reentry
|
|
.active_step_owner_guard
|
|
.as_ref()
|
|
.map(|guard| guard.document_incarnation.clone())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn active_parser_step_depth(&self) -> Option<usize> {
|
|
self.parser_reentry
|
|
.active_step_owner_guard
|
|
.as_ref()
|
|
.map(|guard| guard.depth)
|
|
}
|
|
|
|
pub(crate) fn should_checkpoint_before_parser_custom_element_constructor(&self) -> bool {
|
|
self.parser_reentry.script_nesting_level == 0
|
|
}
|
|
|
|
pub(crate) fn enter_parser_script_nesting(&mut self) -> ParserScriptNestingGuard {
|
|
let parser_session = self
|
|
.current_parser_bridge()
|
|
.map(|bridge| bridge.enter_parser_script_nesting());
|
|
self.parser_reentry.script_nesting_level = self
|
|
.parser_reentry
|
|
.script_nesting_level
|
|
.checked_add(1)
|
|
.expect("parser script nesting level overflow");
|
|
ParserScriptNestingGuard {
|
|
runtime: self as *mut DocumentRuntime,
|
|
_parser_session: parser_session,
|
|
}
|
|
}
|
|
|
|
fn exit_parser_script_nesting(&mut self) {
|
|
assert!(
|
|
self.parser_reentry.script_nesting_level > 0,
|
|
"parser script nesting guard exited without matching enter"
|
|
);
|
|
self.parser_reentry.script_nesting_level -= 1;
|
|
}
|
|
|
|
pub(crate) fn enter_parser_pause(&mut self) -> ParserPauseGuard {
|
|
self.parser_reentry.pause_depth = self
|
|
.parser_reentry
|
|
.pause_depth
|
|
.checked_add(1)
|
|
.expect("parser pause depth overflow");
|
|
ParserPauseGuard {
|
|
runtime: self as *mut DocumentRuntime,
|
|
}
|
|
}
|
|
|
|
fn exit_parser_pause(&mut self) {
|
|
assert!(
|
|
self.parser_reentry.pause_depth > 0,
|
|
"parser pause guard exited without matching enter"
|
|
);
|
|
self.parser_reentry.pause_depth -= 1;
|
|
}
|
|
|
|
pub(crate) fn enter_parser_insertion_session(&mut self) -> ParserInsertionSessionGuard {
|
|
self.parser_reentry.insertion_session_depth = self
|
|
.parser_reentry
|
|
.insertion_session_depth
|
|
.checked_add(1)
|
|
.expect("parser insertion session depth overflow");
|
|
ParserInsertionSessionGuard {
|
|
runtime: self as *mut DocumentRuntime,
|
|
}
|
|
}
|
|
|
|
fn exit_parser_insertion_session(&mut self) {
|
|
assert!(
|
|
self.parser_reentry.insertion_session_depth > 0,
|
|
"parser insertion session guard exited without matching enter"
|
|
);
|
|
self.parser_reentry.insertion_session_depth -= 1;
|
|
}
|
|
|
|
pub(crate) fn enter_throw_on_dynamic_markup_insertion(&mut self, document: DomHandle) {
|
|
*self
|
|
.parser_reentry
|
|
.dynamic_markup_insertion_counters
|
|
.entry(document)
|
|
.or_insert(0) += 1;
|
|
}
|
|
|
|
pub(crate) fn exit_throw_on_dynamic_markup_insertion(&mut self, document: DomHandle) {
|
|
let Some(counter) = self
|
|
.parser_reentry
|
|
.dynamic_markup_insertion_counters
|
|
.get_mut(&document)
|
|
else {
|
|
return;
|
|
};
|
|
*counter = counter.saturating_sub(1);
|
|
if *counter == 0 {
|
|
self.parser_reentry
|
|
.dynamic_markup_insertion_counters
|
|
.remove(&document);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn has_throw_on_dynamic_markup_insertion_counter(
|
|
&self,
|
|
document: DomHandle,
|
|
) -> bool {
|
|
self.parser_reentry
|
|
.dynamic_markup_insertion_counters
|
|
.get(&document)
|
|
.is_some_and(|counter| *counter > 0)
|
|
}
|
|
|
|
/// Runs a synchronous parser step against the runtime-owned live DOM.
|
|
///
|
|
/// This keeps the active Document-owner/depth guard owned by `DocumentRuntime`
|
|
/// and guarantees the guard is released when the step returns or unwinds.
|
|
#[track_caller]
|
|
pub(crate) fn with_dom_host_parse_step<R>(&mut self, step: impl FnOnce(&mut Self) -> R) -> R {
|
|
struct FinishParserStepOnDrop<'a> {
|
|
runtime: &'a mut DocumentRuntime,
|
|
}
|
|
|
|
impl Drop for FinishParserStepOnDrop<'_> {
|
|
fn drop(&mut self) {
|
|
self.runtime.finish_dom_host_parse_step();
|
|
}
|
|
}
|
|
|
|
self.begin_dom_host_parse_step();
|
|
let guard = FinishParserStepOnDrop { runtime: self };
|
|
step(&mut *guard.runtime)
|
|
}
|
|
|
|
/// Starts one synchronous parser step against the runtime-owned live DOM.
|
|
///
|
|
/// Parser sinks do not receive this owner identity. They call back into
|
|
/// `DocumentRuntime`, and each callback short-borrows the current DomHost
|
|
/// after this runtime-private active step guard rejects a replaced
|
|
/// Document incarnation.
|
|
#[track_caller]
|
|
pub(crate) fn begin_dom_host_parse_step(&mut self) {
|
|
let document_incarnation = self.document_incarnation.clone();
|
|
if let Some(active) = self.parser_reentry.active_step_owner_guard.as_mut() {
|
|
assert_eq!(
|
|
active.document_incarnation, document_incarnation,
|
|
"nested parser steps must target the current Document incarnation"
|
|
);
|
|
active.depth = active
|
|
.depth
|
|
.checked_add(1)
|
|
.expect("parser step depth overflow");
|
|
return;
|
|
}
|
|
self.parser_reentry.active_step_owner_guard = Some(ParserStepOwnerGuard {
|
|
document_incarnation,
|
|
depth: 1,
|
|
});
|
|
}
|
|
|
|
/// Finishes one synchronous parser step and marks parser-discovered work dirty.
|
|
#[track_caller]
|
|
pub(crate) fn finish_dom_host_parse_step(&mut self) {
|
|
let guard = self
|
|
.parser_reentry
|
|
.active_step_owner_guard
|
|
.as_mut()
|
|
.expect("parser step must be active before finishing");
|
|
assert!(
|
|
guard.depth > 0,
|
|
"parser step must be active before finishing"
|
|
);
|
|
guard.depth -= 1;
|
|
if guard.depth == 0 {
|
|
self.parser_reentry.active_step_owner_guard.take();
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
fn assert_active_parser_document_incarnation(&self) {
|
|
let active = self
|
|
.parser_reentry
|
|
.active_step_owner_guard
|
|
.as_ref()
|
|
.expect("parser step must be active");
|
|
assert_eq!(
|
|
active.document_incarnation, self.document_incarnation,
|
|
"parser step must target the current Document incarnation"
|
|
);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn dom_host_mut_for_active_parser_step(&mut self) -> &mut DomHost {
|
|
self.assert_active_parser_document_incarnation();
|
|
self.dom_host.borrow_mut()
|
|
}
|
|
|
|
fn enter_structural_mutation(&mut self) -> StructuralMutationGuard {
|
|
self.structural_mutation_depth = self
|
|
.structural_mutation_depth
|
|
.checked_add(1)
|
|
.expect("structural mutation depth overflow");
|
|
StructuralMutationGuard {
|
|
runtime: self as *mut DocumentRuntime,
|
|
}
|
|
}
|
|
|
|
fn exit_structural_mutation(&mut self) {
|
|
assert!(
|
|
self.structural_mutation_depth > 0,
|
|
"structural mutation guard exited without matching enter"
|
|
);
|
|
self.structural_mutation_depth -= 1;
|
|
}
|
|
|
|
pub(crate) fn is_structural_mutation_active(&self) -> bool {
|
|
self.structural_mutation_depth > 0
|
|
}
|
|
|
|
pub(crate) fn debug_assert_not_in_structural_mutation(&self, action: &str) {
|
|
debug_assert!(
|
|
!self.is_structural_mutation_active(),
|
|
"{action} must run after structural mutation scope exits"
|
|
);
|
|
}
|
|
|
|
#[track_caller]
|
|
fn append_child_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
) -> DomMutationEffects {
|
|
let _guard = self.enter_structural_mutation();
|
|
self.dom_host.append_child_effects(parent, child)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn insert_before_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
reference_child: Option<DomHandle>,
|
|
) -> DomMutationEffects {
|
|
self.insert_before_effects_in_structural_scope_with_state_preservation(
|
|
parent,
|
|
child,
|
|
reference_child,
|
|
false,
|
|
)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn insert_before_effects_in_structural_scope_with_state_preservation(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
reference_child: Option<DomHandle>,
|
|
is_atomic_move: bool,
|
|
) -> DomMutationEffects {
|
|
let _guard = self.enter_structural_mutation();
|
|
if is_atomic_move {
|
|
self.dom_host
|
|
.insert_before_preserving_state_effects(parent, child, reference_child)
|
|
} else {
|
|
self.dom_host
|
|
.insert_before_effects(parent, child, reference_child)
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
fn remove_child_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
) -> DomMutationEffects {
|
|
self.remove_child_effects_in_structural_scope_with_state_preservation(parent, child, false)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn remove_child_effects_in_structural_scope_with_state_preservation(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
is_atomic_move: bool,
|
|
) -> DomMutationEffects {
|
|
let _guard = self.enter_structural_mutation();
|
|
if is_atomic_move {
|
|
self.dom_host
|
|
.remove_child_preserving_state_effects(parent, child)
|
|
} else {
|
|
self.dom_host.remove_child_effects(parent, child)
|
|
}
|
|
}
|
|
|
|
#[track_caller]
|
|
fn replace_child_with_self_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
old_child: DomHandle,
|
|
) -> DomMutationEffects {
|
|
let _guard = self.enter_structural_mutation();
|
|
self.dom_host
|
|
.replace_child_with_self_effects(parent, old_child)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn parser_append_child_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
) -> DomMutationEffects {
|
|
self.assert_active_parser_document_incarnation();
|
|
self.append_child_effects_in_structural_scope(parent, child)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn parser_insert_before_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
reference_child: Option<DomHandle>,
|
|
) -> DomMutationEffects {
|
|
self.assert_active_parser_document_incarnation();
|
|
self.insert_before_effects_in_structural_scope(parent, child, reference_child)
|
|
}
|
|
|
|
#[track_caller]
|
|
fn parser_remove_child_effects_in_structural_scope(
|
|
&mut self,
|
|
parent: DomHandle,
|
|
child: DomHandle,
|
|
) -> DomMutationEffects {
|
|
self.assert_active_parser_document_incarnation();
|
|
self.remove_child_effects_in_structural_scope(parent, child)
|
|
}
|
|
|
|
pub(crate) fn node_is_connected_for_web_api(&self, handle: DomHandle) -> bool {
|
|
custom_elements::is_shadow_including_rooted_in_document(self.dom_host.borrow(), handle)
|
|
}
|
|
}
|
|
|
|
impl std::ops::Deref for LiveRuntimeDomHost {
|
|
type Target = DomHost;
|
|
|
|
#[track_caller]
|
|
fn deref(&self) -> &Self::Target {
|
|
self.borrow()
|
|
}
|
|
}
|
|
|
|
impl std::ops::DerefMut for LiveRuntimeDomHost {
|
|
#[track_caller]
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
self.borrow_mut()
|
|
}
|
|
}
|
|
|
|
impl super::planning::ParserPlanningReadView for LiveRuntimeDomHost {
|
|
fn parser_script_read(
|
|
&self,
|
|
node_id: NativeNodeId,
|
|
) -> Option<super::planning::ParserScriptRead> {
|
|
self.borrow().parser_script_read(node_id)
|
|
}
|
|
|
|
fn is_connected(&self, node_id: NativeNodeId) -> bool {
|
|
self.borrow().is_connected(node_id)
|
|
}
|
|
|
|
fn script_handles(&self) -> Vec<NativeNodeId> {
|
|
self.borrow().script_handles()
|
|
}
|
|
|
|
fn document_order_script_handles(&self) -> Vec<NativeNodeId> {
|
|
self.borrow().document_order_script_handles()
|
|
}
|
|
|
|
fn document_order_position(&self, node_id: NativeNodeId) -> Option<usize> {
|
|
self.borrow().document_order_position(node_id)
|
|
}
|
|
|
|
fn final_url_clone(&self) -> Option<Url> {
|
|
self.borrow().final_url_clone()
|
|
}
|
|
|
|
fn document_base_url_clone(&self) -> Option<Url> {
|
|
<DomHost as super::planning::ParserPlanningReadView>::document_base_url_clone(self.borrow())
|
|
}
|
|
}
|
|
|
|
impl super::stylesheet_blocking::StylesheetBlockingReadView for LiveRuntimeDomHost {
|
|
fn media_query_matches(&self, media: &str) -> bool {
|
|
let document = self
|
|
.borrow()
|
|
.node(self.borrow().document_handle())
|
|
.and_then(Node::as_document)
|
|
.expect("live Document");
|
|
crate::style_engine::media_list::evaluate_media_query_list_with_context(
|
|
media,
|
|
Some(&self.emulated_media),
|
|
self.style_viewport(),
|
|
document.base_url(),
|
|
document.quirks_mode(),
|
|
)
|
|
}
|
|
fn stylesheet_element(
|
|
&self,
|
|
node_id: NativeNodeId,
|
|
) -> Option<super::stylesheet_blocking::StylesheetElementRead> {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::stylesheet_element(
|
|
self.borrow(),
|
|
node_id,
|
|
)
|
|
}
|
|
|
|
fn child_ids(&self, node_id: NativeNodeId) -> Vec<NativeNodeId> {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::child_ids(
|
|
self.borrow(),
|
|
node_id,
|
|
)
|
|
}
|
|
|
|
fn text_content(&self, node_id: NativeNodeId) -> Option<String> {
|
|
self.borrow().text_content(node_id)
|
|
}
|
|
|
|
fn final_url_clone(&self) -> Option<Url> {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::final_url_clone(
|
|
self.borrow(),
|
|
)
|
|
}
|
|
|
|
fn document_base_url_clone(&self) -> Option<Url> {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::document_base_url_clone(
|
|
self.borrow(),
|
|
)
|
|
}
|
|
|
|
fn stylesheet_base_url_clone(&self, node_id: NativeNodeId) -> Option<Url> {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::stylesheet_base_url_clone(self.borrow(), node_id)
|
|
}
|
|
|
|
fn document_node_id(&self) -> NativeNodeId {
|
|
self.borrow().document_node_id()
|
|
}
|
|
|
|
fn document_is_quirks_mode(&self) -> bool {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::document_is_quirks_mode(
|
|
self.borrow(),
|
|
)
|
|
}
|
|
|
|
fn document_order_stylesheet_candidate_ids_before(
|
|
&self,
|
|
target_node_id: Option<NodeId>,
|
|
) -> Vec<NativeNodeId> {
|
|
<DomHost as super::stylesheet_blocking::StylesheetBlockingReadView>::document_order_stylesheet_candidate_ids_before(
|
|
self.borrow(),
|
|
target_node_id,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::{collections::HashSet, sync::Arc};
|
|
|
|
use url::Url;
|
|
|
|
use crate::stylesheet_blocking::StylesheetFetchOptions;
|
|
use crate::{
|
|
dom::{
|
|
NodeId,
|
|
native::{Element, Node},
|
|
},
|
|
network::ResourceRequestClient,
|
|
parser::HtmlParser,
|
|
types::SubresourceResourceType,
|
|
{
|
|
document_runtime::DocumentProcessingWakeObservation,
|
|
document_script_scheduler::{DocumentScriptExecutionLane, PageOwnedDocumentScriptWork},
|
|
native_bridge::PendingRuntimeBindingCall,
|
|
page_task_queue::{PageTask, PostParsePageOwnedWork},
|
|
},
|
|
};
|
|
use moli_fetch::FetchConfig;
|
|
|
|
use super::{
|
|
ConnectedLinkReadinessFetchOptions, ConnectedLoadCompletion, ConnectedLoadNetworkResult,
|
|
ConnectedLoadOperation, ConnectedLoadParameters, ConnectedStyleEventElementKind,
|
|
DocumentRuntime, DomHandle, DomHost, LiveRuntimeDomHost, ParseTimeWakeObservation,
|
|
ParseTimeWakeSource,
|
|
};
|
|
|
|
fn first_element_handle(document: &crate::dom::native::NativeDom, tag_name: &str) -> DomHandle {
|
|
let mut stack = vec![document.document_node_id()];
|
|
while let Some(handle) = stack.pop() {
|
|
if document
|
|
.node(handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element(tag_name))
|
|
{
|
|
return handle;
|
|
}
|
|
let mut children = document.child_nodes(handle).unwrap_or_default();
|
|
children.reverse();
|
|
stack.extend(children);
|
|
}
|
|
panic!("expected {tag_name} element in test document")
|
|
}
|
|
|
|
fn parse_document_with_stylesheet_inputs(
|
|
final_url: Url,
|
|
html: impl Into<String>,
|
|
) -> (
|
|
crate::dom::native::NativeDom,
|
|
Vec<crate::DocumentOwnedStylesheetDiscoveryInput>,
|
|
) {
|
|
let (document, _, inputs) =
|
|
crate::parse_html_test_fixture_with_parser_outputs(final_url, html.into());
|
|
(document, inputs)
|
|
}
|
|
|
|
fn preload_load_parameters(url: Url) -> ConnectedLoadParameters {
|
|
ConnectedLoadParameters::PreloadLikeLink {
|
|
url,
|
|
options: Arc::new(ConnectedLinkReadinessFetchOptions {
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
request_resource_type: Some(moli_fetch::RequestResourceType::CssStyleSheet),
|
|
script_fetch_metadata: None,
|
|
request_mode: moli_fetch::RequestMode::NoCors,
|
|
credentials_mode: moli_fetch::RequestCredentialsMode::Include,
|
|
fetch_priority_hint: None,
|
|
link_preload: true,
|
|
link_fetch_options: StylesheetFetchOptions::default(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn live_runtime_dom_host_forwards_the_canonical_document_base_url() {
|
|
let document = HtmlParser::SCRIPTING_ENABLED.parse(
|
|
Url::parse("https://example.test/path/page.html").unwrap(),
|
|
"<!doctype html><html><head><base href=\"/assets/\"></head></html>".to_owned(),
|
|
);
|
|
let host = LiveRuntimeDomHost::from_dom_host(DomHost::from_dom(document));
|
|
let expected = Url::parse("https://example.test/assets/").unwrap();
|
|
|
|
assert_eq!(
|
|
<LiveRuntimeDomHost as crate::planning::ParserPlanningReadView>::document_base_url_clone(
|
|
&host,
|
|
),
|
|
Some(expected.clone())
|
|
);
|
|
assert_eq!(
|
|
<LiveRuntimeDomHost as crate::stylesheet_blocking::StylesheetBlockingReadView>::document_base_url_clone(
|
|
&host,
|
|
),
|
|
Some(expected)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stylesheet_discovery_reads_the_canonical_document_media_environment() {
|
|
let document = HtmlParser::SCRIPTING_ENABLED.parse(
|
|
Url::parse("https://example.test/media").unwrap(),
|
|
"<!doctype html><link id=sheet rel=stylesheet href=app.css media='(min-width: 700px)'>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let mut runtime = DocumentRuntime::from_document(document);
|
|
let blocks = |runtime: &DocumentRuntime| {
|
|
crate::stylesheet_blocking::stylesheet_link_disposition(
|
|
&runtime.dom_host,
|
|
NodeId::new(link.index()),
|
|
)
|
|
.expect("a nonmatching stylesheet must still have a load operation")
|
|
.is_blocking()
|
|
};
|
|
assert!(blocks(&runtime));
|
|
runtime.set_viewport_surface(Some(crate::protocol_types::ViewportSurface {
|
|
inner_width: 320,
|
|
inner_height: 200,
|
|
device_pixel_ratio: 2.0,
|
|
..Default::default()
|
|
}));
|
|
assert!(!blocks(&runtime));
|
|
runtime.dom_host.set_attribute(
|
|
link,
|
|
"media",
|
|
"screen and (width: 320px) and (device-width: 1920px) and (resolution: 2dppx)",
|
|
);
|
|
assert!(blocks(&runtime));
|
|
runtime
|
|
.dom_host
|
|
.set_attribute(link, "media", "print and (prefers-color-scheme: dark)");
|
|
assert!(!blocks(&runtime));
|
|
runtime.set_emulated_media(&crate::protocol_types::EmulatedMediaOverrides {
|
|
media: Some("print".into()),
|
|
color_scheme: Some("dark".into()),
|
|
..Default::default()
|
|
});
|
|
assert!(blocks(&runtime));
|
|
runtime.set_emulated_media(&Default::default());
|
|
assert!(!blocks(&runtime));
|
|
}
|
|
|
|
#[test]
|
|
fn live_runtime_dom_host_forwards_quirks_mode_for_stylesheet_processing() {
|
|
let document = HtmlParser::SCRIPTING_ENABLED.parse(
|
|
Url::parse("https://example.test/page.html").unwrap(),
|
|
"<html><head><link rel=stylesheet href=app.css></head></html>".to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let host = LiveRuntimeDomHost::from_dom_host(DomHost::from_dom(document));
|
|
|
|
let disposition = crate::stylesheet_blocking::stylesheet_link_disposition(
|
|
&host,
|
|
NodeId::new(link.index()),
|
|
)
|
|
.expect("stylesheet disposition");
|
|
|
|
assert!(disposition.options().quirks_mode_mime_compatibility());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_builds_from_parsed_document_and_updates_text_content() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main>seed</main></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let div_handle = host.create_element("div");
|
|
|
|
assert!(host.set_attribute(div_handle, "data-kind", "native"));
|
|
assert!(host.set_text_content(div_handle, "child"));
|
|
assert!(host.append_child(body_handle, div_handle));
|
|
assert_eq!(
|
|
host.get_attribute(div_handle, "data-kind"),
|
|
Some("native".to_owned())
|
|
);
|
|
assert_eq!(host.text_content(body_handle).as_deref(), Some("seedchild"));
|
|
}
|
|
|
|
#[test]
|
|
fn live_runtime_dom_host_direct_borrow_does_not_block_runtime_access() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main>seed</main></body></html>".to_owned(),
|
|
);
|
|
let mut live = LiveRuntimeDomHost::from_dom_host(DomHost::from_dom(document));
|
|
let document_handle = live.document_handle();
|
|
|
|
let child = live.create_element("div");
|
|
|
|
assert!(live.append_child(document_handle, child));
|
|
assert_eq!(live.borrow_mut().document_handle(), document_handle);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_reentry_state_tracks_script_pause_insertion_and_dynamic_markup() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::from_document(document);
|
|
let document_handle = runtime.document_handle();
|
|
|
|
assert_eq!(runtime.parser_script_nesting_level(), 0);
|
|
assert_eq!(runtime.parser_pause_depth(), 0);
|
|
assert_eq!(runtime.parser_insertion_session_depth(), 0);
|
|
assert!(!runtime.is_parser_paused_for_reentry());
|
|
assert!(!runtime.is_parser_insertion_session_active());
|
|
assert!(!runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle));
|
|
assert!(runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
{
|
|
let _outer = runtime.enter_parser_script_nesting();
|
|
assert_eq!(runtime.parser_script_nesting_level(), 1);
|
|
assert!(!runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
{
|
|
let _inner = runtime.enter_parser_script_nesting();
|
|
assert_eq!(runtime.parser_script_nesting_level(), 2);
|
|
assert!(!runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
}
|
|
assert_eq!(runtime.parser_script_nesting_level(), 1);
|
|
assert!(!runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
}
|
|
assert_eq!(runtime.parser_script_nesting_level(), 0);
|
|
assert!(runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
|
|
{
|
|
let _pause = runtime.enter_parser_pause();
|
|
assert_eq!(runtime.parser_pause_depth(), 1);
|
|
assert!(runtime.is_parser_paused_for_reentry());
|
|
assert!(runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
{
|
|
let _nested_pause = runtime.enter_parser_pause();
|
|
assert_eq!(runtime.parser_pause_depth(), 2);
|
|
assert!(runtime.is_parser_paused_for_reentry());
|
|
}
|
|
assert_eq!(runtime.parser_pause_depth(), 1);
|
|
}
|
|
assert_eq!(runtime.parser_pause_depth(), 0);
|
|
assert!(!runtime.is_parser_paused_for_reentry());
|
|
|
|
{
|
|
let _session = runtime.enter_parser_insertion_session();
|
|
assert_eq!(runtime.parser_insertion_session_depth(), 1);
|
|
assert!(runtime.is_parser_insertion_session_active());
|
|
assert!(runtime.should_checkpoint_before_parser_custom_element_constructor());
|
|
{
|
|
let _nested_session = runtime.enter_parser_insertion_session();
|
|
assert_eq!(runtime.parser_insertion_session_depth(), 2);
|
|
assert!(runtime.is_parser_insertion_session_active());
|
|
}
|
|
assert_eq!(runtime.parser_insertion_session_depth(), 1);
|
|
}
|
|
assert_eq!(runtime.parser_insertion_session_depth(), 0);
|
|
assert!(!runtime.is_parser_insertion_session_active());
|
|
|
|
runtime.enter_throw_on_dynamic_markup_insertion(document_handle);
|
|
assert!(runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle));
|
|
runtime.enter_throw_on_dynamic_markup_insertion(document_handle);
|
|
assert!(runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle));
|
|
runtime.exit_throw_on_dynamic_markup_insertion(document_handle);
|
|
assert!(runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle));
|
|
runtime.exit_throw_on_dynamic_markup_insertion(document_handle);
|
|
assert!(!runtime.has_throw_on_dynamic_markup_insertion_counter(document_handle));
|
|
}
|
|
|
|
#[test]
|
|
fn structural_mutation_guard_tracks_nested_raw_tree_splices() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::from_document(document);
|
|
|
|
assert!(!runtime.is_structural_mutation_active());
|
|
{
|
|
let _outer = runtime.enter_structural_mutation();
|
|
assert!(runtime.is_structural_mutation_active());
|
|
{
|
|
let _inner = runtime.enter_structural_mutation();
|
|
assert!(runtime.is_structural_mutation_active());
|
|
}
|
|
assert!(runtime.is_structural_mutation_active());
|
|
}
|
|
assert!(!runtime.is_structural_mutation_active());
|
|
|
|
let body = runtime
|
|
.dom_host
|
|
.document_body_handle()
|
|
.expect("body handle");
|
|
let child = runtime.dom_host_mut().create_element("div");
|
|
let effects = runtime.append_child_effects_in_structural_scope(body, child);
|
|
assert!(effects.did_change());
|
|
assert!(!runtime.is_structural_mutation_active());
|
|
}
|
|
|
|
#[test]
|
|
fn parser_runtime_dom_owner_allows_nested_steps_on_same_document_incarnation() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main>seed</main></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::from_document(document);
|
|
|
|
runtime.begin_dom_host_parse_step();
|
|
let outer_incarnation = runtime.active_parser_step_document_incarnation();
|
|
runtime.begin_dom_host_parse_step();
|
|
assert_eq!(
|
|
runtime.active_parser_step_document_incarnation(),
|
|
outer_incarnation
|
|
);
|
|
assert_eq!(runtime.active_parser_step_depth(), Some(2));
|
|
|
|
runtime.finish_dom_host_parse_step();
|
|
assert_eq!(
|
|
runtime.active_parser_step_document_incarnation(),
|
|
outer_incarnation
|
|
);
|
|
assert_eq!(runtime.active_parser_step_depth(), Some(1));
|
|
runtime.finish_dom_host_parse_step();
|
|
|
|
assert_eq!(runtime.active_parser_step_document_incarnation(), None);
|
|
assert_eq!(runtime.active_parser_step_depth(), None);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic(expected = "current Document incarnation")]
|
|
fn parser_runtime_dom_owner_rejects_document_replacement() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main>seed</main></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::from_document(document);
|
|
|
|
runtime.begin_dom_host_parse_step();
|
|
runtime.open_document();
|
|
runtime.parser_runtime_dom_node_exists(runtime.document_handle());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_updates_tree_navigation_and_contains_relations() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main id=one>one</main><main id=two>two</main></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let children = host.child_nodes(body_handle).expect("body children");
|
|
let first = children[0];
|
|
let second = children[1];
|
|
let inserted = host.create_element("aside");
|
|
|
|
assert_eq!(
|
|
host.node(body_handle).and_then(Node::owner_document),
|
|
Some(host.document_handle())
|
|
);
|
|
assert_eq!(
|
|
host.node(first).and_then(Node::parent_node),
|
|
Some(body_handle)
|
|
);
|
|
assert_eq!(
|
|
host.node(body_handle).and_then(Node::first_child),
|
|
Some(first)
|
|
);
|
|
assert_eq!(
|
|
host.node(body_handle).and_then(Node::last_child),
|
|
Some(second)
|
|
);
|
|
assert_eq!(host.node(first).and_then(Node::next_sibling), Some(second));
|
|
assert_eq!(host.node(second).and_then(Node::prev_sibling), Some(first));
|
|
assert!(
|
|
host.node(body_handle)
|
|
.is_some_and(|node| node.contains(host.dom(), second))
|
|
);
|
|
assert!(
|
|
host.node(first)
|
|
.is_some_and(|node| node.contains(host.dom(), first))
|
|
);
|
|
|
|
assert!(host.insert_before(body_handle, inserted, Some(second)));
|
|
assert_eq!(
|
|
host.node(first).and_then(Node::next_sibling),
|
|
Some(inserted)
|
|
);
|
|
assert_eq!(
|
|
host.node(second).and_then(Node::prev_sibling),
|
|
Some(inserted)
|
|
);
|
|
assert_eq!(
|
|
host.child_nodes(body_handle)
|
|
.expect("updated body children"),
|
|
vec![first, inserted, second]
|
|
);
|
|
|
|
assert!(host.remove_child(body_handle, inserted));
|
|
assert_eq!(
|
|
host.child_nodes(body_handle)
|
|
.expect("body children after removal"),
|
|
vec![first, second]
|
|
);
|
|
assert_eq!(host.node(inserted).and_then(Node::parent_node), None);
|
|
assert!(
|
|
!host
|
|
.node(inserted)
|
|
.is_some_and(|node| node.contains(host.dom(), first))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_reports_unified_mutation_effects() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let fragment = host.create_document_fragment();
|
|
let script = host.create_element("script");
|
|
|
|
let insert_script = host.append_child_effects(fragment, script);
|
|
assert!(insert_script.did_change());
|
|
assert!(insert_script.scripts().connected_roots().is_empty());
|
|
assert_eq!(insert_script.scripts().updated_nodes(), &[script]);
|
|
assert_eq!(insert_script.scripts().prepare_triggers().len(), 1);
|
|
assert_eq!(
|
|
insert_script.scripts().prepare_triggers()[0].handle(),
|
|
script
|
|
);
|
|
assert!(insert_script.tree().disconnected_roots().is_empty());
|
|
|
|
let connect_fragment = host.append_child_effects(body_handle, fragment);
|
|
assert!(connect_fragment.did_change());
|
|
assert!(connect_fragment.scripts().connected_roots().is_empty());
|
|
assert_eq!(connect_fragment.scripts().updated_nodes(), &[script]);
|
|
assert_eq!(connect_fragment.scripts().prepare_triggers().len(), 1);
|
|
assert_eq!(
|
|
connect_fragment.scripts().prepare_triggers()[0].handle(),
|
|
script
|
|
);
|
|
assert!(connect_fragment.tree().disconnected_roots().is_empty());
|
|
|
|
let update_script = host.set_attribute_effects(script, "src", "/dynamic.js");
|
|
assert!(update_script.did_change());
|
|
assert!(update_script.scripts().connected_roots().is_empty());
|
|
assert_eq!(update_script.scripts().updated_nodes(), &[script]);
|
|
assert_eq!(update_script.scripts().prepare_triggers().len(), 1);
|
|
assert_eq!(
|
|
update_script.scripts().prepare_triggers()[0].handle(),
|
|
script
|
|
);
|
|
|
|
let disconnect_script = host.remove_child_effects(body_handle, script);
|
|
assert!(disconnect_script.did_change());
|
|
assert!(disconnect_script.scripts().connected_roots().is_empty());
|
|
assert!(disconnect_script.scripts().updated_nodes().is_empty());
|
|
assert!(disconnect_script.scripts().prepare_triggers().is_empty());
|
|
assert_eq!(disconnect_script.tree().disconnected_roots(), &[script]);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_uses_connected_script_roots_only_for_non_script_subtrees() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let wrapper = host.create_element("div");
|
|
let script = host.create_element("script");
|
|
|
|
let insert_script = host.append_child_effects(wrapper, script);
|
|
assert!(insert_script.scripts().connected_roots().is_empty());
|
|
assert_eq!(insert_script.scripts().prepare_triggers().len(), 1);
|
|
assert_eq!(
|
|
insert_script.scripts().prepare_triggers()[0].handle(),
|
|
script
|
|
);
|
|
|
|
let connect_wrapper = host.append_child_effects(body_handle, wrapper);
|
|
assert_eq!(connect_wrapper.scripts().connected_roots(), &[wrapper]);
|
|
assert!(connect_wrapper.scripts().prepare_triggers().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_scans_connected_shadow_trees_for_scripts() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let shadow_host = host.create_element("div");
|
|
let shadow_root = host
|
|
.attach_shadow_root(shadow_host, "open")
|
|
.expect("shadow root");
|
|
let script = host.create_element("script");
|
|
|
|
let insert_script = host.append_child_effects(shadow_root, script);
|
|
assert!(insert_script.did_change());
|
|
assert_eq!(insert_script.scripts().prepare_triggers().len(), 1);
|
|
assert_eq!(
|
|
insert_script.scripts().prepare_triggers()[0].handle(),
|
|
script
|
|
);
|
|
|
|
let connect_host = host.append_child_effects(body_handle, shadow_host);
|
|
assert_eq!(connect_host.scripts().connected_roots(), &[shadow_host]);
|
|
assert_eq!(host.connected_script_handles(shadow_host), vec![script]);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_flattens_nested_slot_fallback_without_losing_siblings() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let shadow_host = host.create_element("section");
|
|
assert!(host.append_child(body_handle, shadow_host));
|
|
|
|
let shadow_root = host
|
|
.attach_shadow_root(shadow_host, "open")
|
|
.expect("shadow root");
|
|
let outer_slot = host.create_element("slot");
|
|
let inner_slot = host.create_element("slot");
|
|
let fallback_text = host.create_text_node("hello");
|
|
let fallback_span = host.create_element("span");
|
|
|
|
assert!(host.append_child(shadow_root, outer_slot));
|
|
assert!(host.append_child(outer_slot, inner_slot));
|
|
assert!(host.append_child(inner_slot, fallback_text));
|
|
assert!(host.append_child(outer_slot, fallback_span));
|
|
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(outer_slot, false),
|
|
Vec::new()
|
|
);
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(outer_slot, true),
|
|
vec![fallback_text, fallback_span]
|
|
);
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(inner_slot, false),
|
|
Vec::new()
|
|
);
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(inner_slot, true),
|
|
vec![fallback_text]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_slot_mutation_effects_mark_old_and_new_slots() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let shadow_host = host.create_element("section");
|
|
assert!(host.append_child(body_handle, shadow_host));
|
|
|
|
let shadow_root = host
|
|
.attach_shadow_root(shadow_host, "open")
|
|
.expect("shadow root");
|
|
let slot_a = host.create_element("slot");
|
|
let slot_b = host.create_element("slot");
|
|
let child = host.create_element("div");
|
|
|
|
assert!(host.set_attribute(slot_a, "name", "a"));
|
|
assert!(host.set_attribute(slot_b, "name", "b"));
|
|
assert!(host.append_child(shadow_root, slot_a));
|
|
assert!(host.append_child(shadow_root, slot_b));
|
|
assert!(host.set_attribute(child, "slot", "a"));
|
|
|
|
let insert_effects = host.append_child_effects(shadow_host, child);
|
|
assert_eq!(insert_effects.slots().changed_slots(), &[slot_a]);
|
|
assert_eq!(host.assigned_slot_for_node(child), Some(slot_a));
|
|
|
|
let retarget_effects = host.set_attribute_effects(child, "slot", "b");
|
|
let retargeted: HashSet<_> = retarget_effects
|
|
.slots()
|
|
.changed_slots()
|
|
.iter()
|
|
.map(|handle| handle.index())
|
|
.collect();
|
|
assert_eq!(retargeted, HashSet::from([slot_a.index(), slot_b.index()]));
|
|
assert_eq!(host.assigned_slot_for_node(child), Some(slot_b));
|
|
|
|
let remove_effects = host.remove_child_effects(shadow_host, child);
|
|
assert_eq!(remove_effects.slots().changed_slots(), &[slot_b]);
|
|
assert_eq!(host.assigned_slot_for_node(child), None);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_move_effects_mark_shadow_slot_changes() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let shadow_host = host.create_element("section");
|
|
assert!(host.append_child(body_handle, shadow_host));
|
|
|
|
let shadow_root = host
|
|
.attach_shadow_root(shadow_host, "open")
|
|
.expect("shadow root");
|
|
let slot = host.create_element("slot");
|
|
let slottable = host.create_element("p");
|
|
assert!(host.set_attribute(slot, "name", "content"));
|
|
assert!(host.set_attribute(slottable, "slot", "content"));
|
|
assert!(host.append_child(shadow_root, slot));
|
|
assert!(host.append_child(shadow_host, slottable));
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(slot, false),
|
|
vec![slottable]
|
|
);
|
|
|
|
let move_out_effects = host.insert_before_effects(body_handle, slottable, None);
|
|
assert_eq!(move_out_effects.slots().changed_slots(), &[slot]);
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(slot, false),
|
|
Vec::new()
|
|
);
|
|
|
|
let move_in_effects = host.insert_before_effects(shadow_host, slottable, None);
|
|
assert_eq!(move_in_effects.slots().changed_slots(), &[slot]);
|
|
assert_eq!(
|
|
host.assigned_nodes_for_slot_with_options(slot, false),
|
|
vec![slottable]
|
|
);
|
|
|
|
let fallback = host.create_element("span");
|
|
assert!(host.append_child(shadow_root, fallback));
|
|
let fallback_in_effects = host.insert_before_effects(slot, fallback, None);
|
|
assert_eq!(fallback_in_effects.slots().changed_slots(), &[slot]);
|
|
|
|
let fallback_out_effects = host.insert_before_effects(shadow_root, fallback, None);
|
|
assert_eq!(fallback_out_effects.slots().changed_slots(), &[slot]);
|
|
|
|
let slot_out_effects = host.insert_before_effects(body_handle, slot, None);
|
|
assert_eq!(slot_out_effects.slots().changed_slots(), &[slot]);
|
|
|
|
let slot_in_effects = host.insert_before_effects(shadow_root, slot, None);
|
|
assert_eq!(slot_in_effects.slots().changed_slots(), &[slot]);
|
|
}
|
|
|
|
#[test]
|
|
fn document_runtime_replace_live_document_preserves_shadow_root_bindings() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let shadow_host = runtime.dom_host_mut().create_element("section");
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.append_child(body_handle, shadow_host)
|
|
);
|
|
|
|
let shadow_root = runtime
|
|
.dom_host_mut()
|
|
.attach_shadow_root(shadow_host, "open")
|
|
.expect("shadow root");
|
|
let shadow_child = runtime.dom_host_mut().create_element("span");
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.append_child(shadow_root, shadow_child)
|
|
);
|
|
|
|
let parser_snapshot = runtime.snapshot_document();
|
|
runtime.replace_live_document(&parser_snapshot);
|
|
|
|
assert_eq!(
|
|
runtime.dom_host().shadow_root_handle(shadow_host),
|
|
Some(shadow_root)
|
|
);
|
|
assert_eq!(
|
|
runtime
|
|
.dom_host()
|
|
.child_handles(shadow_root)
|
|
.collect::<Vec<_>>(),
|
|
vec![shadow_child]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn document_runtime_replace_live_document_preserves_script_already_started() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><script src=\"/app.js\"></script></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let script = document
|
|
.script_handles()
|
|
.first()
|
|
.copied()
|
|
.expect("parser-created script handle");
|
|
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.set_script_already_started(script, true),
|
|
"live runtime should allow marking parser-created script as already-started"
|
|
);
|
|
let replacement_document = document.clone();
|
|
runtime.replace_live_document(&replacement_document);
|
|
|
|
assert!(
|
|
runtime
|
|
.snapshot_document()
|
|
.node(script)
|
|
.and_then(|node| node.as_element())
|
|
.is_some_and(|element| element.script_already_started()),
|
|
"document replacement should preserve runtime-owned already-started state"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn document_runtime_can_mark_script_already_started_by_node_id() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><script src=\"/app.js\"></script></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let script = document
|
|
.script_handles()
|
|
.first()
|
|
.copied()
|
|
.expect("parser-created script handle");
|
|
let node_id = NodeId::new(script.index());
|
|
|
|
assert!(
|
|
runtime.mark_script_already_started_by_node_id(node_id),
|
|
"marking by node id should resolve the live runtime script handle"
|
|
);
|
|
assert!(
|
|
runtime
|
|
.snapshot_document()
|
|
.node(script)
|
|
.and_then(|node| node.as_element())
|
|
.is_some_and(|element| element.script_already_started()),
|
|
"marking by node id should project into the live DOM snapshot"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn document_runtime_sets_parser_script_force_async_like_chrome() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><script></script><script async></script></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let parser_scripts = document.script_handles();
|
|
let parser_script = parser_scripts
|
|
.first()
|
|
.copied()
|
|
.expect("parser-created script handle");
|
|
let parser_async_script = parser_scripts
|
|
.get(1)
|
|
.copied()
|
|
.expect("parser-created async script handle");
|
|
let dynamic_script = runtime.dom_host_mut().create_element("script");
|
|
|
|
assert!(
|
|
!runtime
|
|
.dom_host()
|
|
.node(parser_script)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(Element::script_async)
|
|
);
|
|
assert!(
|
|
runtime
|
|
.dom_host()
|
|
.node(parser_async_script)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(Element::script_async)
|
|
);
|
|
assert!(
|
|
runtime
|
|
.dom_host()
|
|
.node(dynamic_script)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(Element::script_async)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn document_runtime_snapshot_projects_live_dom_without_reparse() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head></head><body><main id=seed>seed</main></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let script = runtime.dom_host_mut().create_element("script");
|
|
let text = runtime.dom_host_mut().create_text_node("console.log('ok')");
|
|
let section = runtime.dom_host_mut().create_element("section");
|
|
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.set_attribute(section, "data-state", "live")
|
|
);
|
|
assert!(runtime.dom_host_mut().set_text_content(section, "fresh"));
|
|
assert!(runtime.dom_host_mut().append_child(body_handle, section));
|
|
assert!(runtime.dom_host_mut().append_child(script, text));
|
|
assert!(runtime.dom_host_mut().append_child(body_handle, script));
|
|
|
|
let snapshot = runtime.snapshot_document();
|
|
let body_id = snapshot.body_node_id().expect("snapshot body");
|
|
let body_html = snapshot.outer_html(body_id).expect("snapshot body html");
|
|
|
|
assert!(snapshot.parse_errors().is_empty());
|
|
assert!(body_html.contains("<section data-state=\"live\">fresh</section>"));
|
|
assert!(body_html.contains("<script>console.log('ok')</script>"));
|
|
assert_eq!(snapshot.script_node_ids().len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_roundtrip_through_live_runtime_keeps_style_import_text_before_later_script() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let stream = parser.start_document(Url::parse("https://example.com/").unwrap());
|
|
let html = "<!doctype html><html><head><script>window.start = 1;</script><style>@import url('/slow.css');</style><script>window.afterStyle = 1;</script><script src='/blocking.js'></script></head><body><div id='late'>late</div></body></html>";
|
|
|
|
let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(_)) =
|
|
stream.pump_parser_step(html).result
|
|
else {
|
|
panic!("expected first script handoff");
|
|
};
|
|
let first_document = stream.take_parser_stream_dom_host().into_dom();
|
|
let mut runtime = DocumentRuntime::new(&first_document);
|
|
runtime.replace_live_document_with_document(first_document);
|
|
stream.restore_parser_stream_dom_host(DomHost::from_dom(runtime.snapshot_document()));
|
|
|
|
let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(_)) =
|
|
stream.pump_parser_step("").result
|
|
else {
|
|
panic!("expected second script handoff");
|
|
};
|
|
let second_document = stream.take_parser_stream_dom_host().into_dom();
|
|
runtime.replace_live_document_with_document(second_document);
|
|
stream.restore_parser_stream_dom_host(DomHost::from_dom(runtime.snapshot_document()));
|
|
|
|
let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(third_handoff)) =
|
|
stream.pump_parser_step("").result
|
|
else {
|
|
panic!("expected third script handoff");
|
|
};
|
|
let third_handoff = third_handoff.node_id();
|
|
let third_snapshot = stream.snapshot_parser_stream_document();
|
|
let style_node_id = third_snapshot
|
|
.nodes()
|
|
.iter()
|
|
.find(|node| node.is_html_element_named("style"))
|
|
.map(|node| node.id())
|
|
.expect("expected style element");
|
|
|
|
assert_eq!(
|
|
third_snapshot.text_content(style_node_id).as_deref(),
|
|
Some("@import url('/slow.css');")
|
|
);
|
|
assert_eq!(
|
|
third_snapshot.script_src(third_handoff),
|
|
Some("/blocking.js")
|
|
);
|
|
assert!(
|
|
third_snapshot
|
|
.node(third_handoff)
|
|
.is_some_and(|node| node.flags().parser_created()),
|
|
"external script should remain parser-created after live roundtrip"
|
|
);
|
|
assert!(
|
|
third_snapshot.document_body_handle().is_none(),
|
|
"later body content should still be hidden at the external script handoff"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parser_roundtrip_after_live_dom_mutation_keeps_later_body_hidden() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let stream = parser.start_document(Url::parse("https://example.com/").unwrap());
|
|
let html = "<!doctype html><html><head><script>window.start = 1;</script><script>window.after = 1;</script></head><body><div id='late'>late</div></body></html>";
|
|
|
|
let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(_)) =
|
|
stream.pump_parser_step(html).result
|
|
else {
|
|
panic!("expected first script handoff");
|
|
};
|
|
let first_document = stream.take_parser_stream_dom_host().into_dom();
|
|
let mut runtime = DocumentRuntime::new(&first_document);
|
|
runtime.replace_live_document_with_document(first_document);
|
|
|
|
let head = runtime
|
|
.dom_host()
|
|
.document_head_handle()
|
|
.expect("head should exist before runtime mutation");
|
|
let link = runtime.dom_host_mut().create_element("link");
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.set_attribute(link, "rel", "stylesheet")
|
|
);
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.set_attribute(link, "href", "/runtime.css")
|
|
);
|
|
assert!(runtime.dom_host_mut().append_child(head, link));
|
|
|
|
stream.restore_parser_stream_dom_host(DomHost::from_dom(runtime.snapshot_document()));
|
|
|
|
let crate::parser::ParserPumpStep::Yield(crate::parser::ParserYield::Script(
|
|
second_handoff,
|
|
)) = stream.pump_parser_step("").result
|
|
else {
|
|
panic!("expected second script handoff");
|
|
};
|
|
let second_handoff = second_handoff.node_id();
|
|
let second_snapshot = stream.snapshot_parser_stream_document();
|
|
assert_eq!(
|
|
second_snapshot.script_text(second_handoff).as_deref(),
|
|
Some("window.after = 1;")
|
|
);
|
|
assert!(
|
|
second_snapshot.document_body_handle().is_none(),
|
|
"later body content should remain hidden after live DOM mutation roundtrip"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn precedes_in_document_order_handles_disconnected_roots_without_underflow() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><div id=live></div></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let live = runtime
|
|
.dom_host()
|
|
.element_handle_by_id("live")
|
|
.expect("live node in document");
|
|
let fragment = runtime.dom_host_mut().create_document_fragment();
|
|
let detached = runtime.dom_host_mut().create_element("div");
|
|
|
|
assert!(runtime.dom_host_mut().append_child(fragment, detached));
|
|
assert!(!runtime.precedes_in_document_order(live, detached));
|
|
assert!(!runtime.precedes_in_document_order(detached, live));
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_import_and_adopt_node_preserve_owner_document_and_parentage() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><div id=container><span id=seed>seed</span></div></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let document_handle = document.document_node_id();
|
|
let container = host.element_handle_by_id("container").expect("container");
|
|
let seed = host.element_handle_by_id("seed").expect("seed");
|
|
|
|
let imported = host
|
|
.import_node(document_handle, container, true)
|
|
.expect("imported clone");
|
|
assert_ne!(imported, container);
|
|
assert_eq!(
|
|
host.node(imported).and_then(Node::owner_document),
|
|
Some(document_handle)
|
|
);
|
|
assert_eq!(host.node(imported).and_then(Node::parent_node), None);
|
|
let imported_children = host.child_nodes(imported).expect("imported children");
|
|
assert_eq!(imported_children.len(), 1);
|
|
assert_ne!(imported_children[0], seed);
|
|
assert_eq!(
|
|
host.node(imported_children[0])
|
|
.and_then(Node::owner_document),
|
|
Some(document_handle)
|
|
);
|
|
|
|
assert!(host.remove_child(container, seed));
|
|
let adopted = host
|
|
.adopt_node(document_handle, seed)
|
|
.expect("adopted node");
|
|
assert_eq!(adopted, seed);
|
|
assert_eq!(host.node(seed).and_then(Node::parent_node), None);
|
|
assert_eq!(
|
|
host.node(seed).and_then(Node::owner_document),
|
|
Some(document_handle)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_rebuilds_id_lookup_after_id_attribute_mutation() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main id=one>one</main><main>two</main></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let children = host.child_nodes(body_handle).expect("body children");
|
|
let first = children[0];
|
|
let second = children[1];
|
|
|
|
assert_eq!(host.element_handle_by_id("one"), Some(first));
|
|
|
|
assert!(host.set_attribute(second, "id", "two"));
|
|
assert_eq!(host.element_handle_by_id("two"), Some(second));
|
|
|
|
assert!(host.remove_attribute(first, "id"));
|
|
assert_eq!(host.element_handle_by_id("one"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_invalidates_live_collection_cache_on_tree_and_attribute_mutations() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><main></main></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let body_handle = document.document_body_handle().expect("body handle");
|
|
let main = host
|
|
.child_nodes(body_handle)
|
|
.expect("body children")
|
|
.into_iter()
|
|
.next()
|
|
.expect("seed child");
|
|
|
|
assert_eq!(
|
|
host.resolve_live_collection(body_handle, "children", None, false)
|
|
.expect("children collection")
|
|
.len(),
|
|
1
|
|
);
|
|
|
|
let aside = host.create_element("aside");
|
|
assert!(host.append_child(body_handle, aside));
|
|
assert_eq!(
|
|
host.resolve_live_collection(body_handle, "children", None, false)
|
|
.expect("children collection after append")
|
|
.len(),
|
|
2
|
|
);
|
|
|
|
assert_eq!(
|
|
host.resolve_live_collection(body_handle, "className", Some("active"), false)
|
|
.expect("class collection before attr change")
|
|
.len(),
|
|
0
|
|
);
|
|
assert!(host.set_attribute(main, "class", "active"));
|
|
assert_eq!(
|
|
host.resolve_live_collection(body_handle, "className", Some("active"), false)
|
|
.expect("class collection after attr change")
|
|
.len(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dom_host_normalize_merges_adjacent_text_nodes_recursively() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><div id=container></div></body></html>".to_owned(),
|
|
);
|
|
let mut host = DomHost::from_dom(document.clone());
|
|
let container = host.element_handle_by_id("container").expect("container");
|
|
|
|
let first = host.create_text_node("a");
|
|
let empty = host.create_text_node("");
|
|
let second = host.create_text_node("b");
|
|
let nested = host.create_element("section");
|
|
let nested_first = host.create_text_node("x");
|
|
let nested_empty = host.create_text_node("");
|
|
let nested_second = host.create_text_node("y");
|
|
|
|
assert!(host.append_child(container, first));
|
|
assert!(host.append_child(container, empty));
|
|
assert!(host.append_child(container, second));
|
|
assert!(host.append_child(container, nested));
|
|
assert!(host.append_child(nested, nested_first));
|
|
assert!(host.append_child(nested, nested_empty));
|
|
assert!(host.append_child(nested, nested_second));
|
|
|
|
let effects = host.normalize_effects(container);
|
|
assert!(effects.did_change());
|
|
|
|
let children = host.child_nodes(container).expect("container children");
|
|
assert_eq!(children.len(), 2);
|
|
assert_eq!(host.text_content(children[0]).as_deref(), Some("ab"));
|
|
let nested_children = host.child_nodes(children[1]).expect("nested children");
|
|
assert_eq!(nested_children.len(), 1);
|
|
assert_eq!(host.text_content(nested_children[0]).as_deref(), Some("xy"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stylesheet_typed_task_is_ready_for_document_processing() {
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=\"data:text/css,body%7B%7D\"></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new();
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
let link = first_element_handle(&document, "link");
|
|
let stylesheet_url = Url::parse("data:text/css,body%7B%7D").unwrap();
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.enqueue_completion_for_testing(
|
|
crate::dom::NodeId::new(link.index()),
|
|
stylesheet_url,
|
|
true,
|
|
);
|
|
|
|
assert_eq!(
|
|
runtime
|
|
.observe_document_processing_wake(&mut task_queue)
|
|
.await,
|
|
DocumentProcessingWakeObservation::ReadyNow,
|
|
"a stylesheet completion published before observation must remain visible as typed Networking work"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn injected_page_task_can_wake_parse_time_turn_without_timeout_fallback() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body></body></html>".to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new();
|
|
let sender = task_queue.parser_boundary_sender();
|
|
|
|
tokio::spawn(async move {
|
|
let _ = sender.send(PageTask::DispatchDomContentLoaded);
|
|
});
|
|
|
|
assert_eq!(
|
|
runtime
|
|
.wait_for_parse_time_turn_arrival(&mut task_queue)
|
|
.await,
|
|
ParseTimeWakeObservation::Arrived(ParseTimeWakeSource::InjectedPageTask)
|
|
);
|
|
task_queue.accept_ready_parse_time_wakes();
|
|
assert!(matches!(
|
|
task_queue.parse_time_pop_front(),
|
|
Some(PageTask::DispatchDomContentLoaded)
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stylesheet_completion_with_link_owner_surfaces_ready_parse_time_work() {
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=\"data:text/css,body%7B%7D\"></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new();
|
|
let head = document.document_head_handle().expect("head handle");
|
|
let link = document
|
|
.child_nodes(head)
|
|
.expect("head children")
|
|
.into_iter()
|
|
.find(|handle| {
|
|
document
|
|
.node(*handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element("link"))
|
|
})
|
|
.expect("stylesheet link");
|
|
let link_node_id = crate::dom::NodeId::new(link.index());
|
|
let stylesheet_url = Url::parse("data:text/css,body%7B%7D").unwrap();
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.enqueue_completion_for_testing(link_node_id, stylesheet_url, true);
|
|
|
|
assert_eq!(
|
|
runtime
|
|
.wait_for_parse_time_turn_arrival(&mut task_queue)
|
|
.await,
|
|
ParseTimeWakeObservation::ReadyNow,
|
|
"the resource wake is not a parse-time source, but draining it makes the link event ready"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn draining_blocking_stylesheet_completion_persists_resolved_state() {
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/app.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
assert!(!runtime.has_all_blocking_stylesheets_resolved());
|
|
|
|
let node_id = NodeId::new(first_element_handle(&document, "link").index());
|
|
let stylesheet_url = Url::parse("https://example.com/app.css").unwrap();
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.enqueue_completion_for_testing(node_id, stylesheet_url, true);
|
|
|
|
runtime.drain_blocking_stylesheet_completions();
|
|
|
|
assert!(
|
|
runtime.has_all_blocking_stylesheets_resolved(),
|
|
"drained blocking stylesheet completions should persist as resolved state"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn response_csp_blocks_ownerless_stylesheet_admission() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head></head><body></body></html>".to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
runtime.set_response_content_security_policies(&["style-src 'none'".to_owned()]);
|
|
|
|
assert!(
|
|
runtime
|
|
.preload_stylesheet(
|
|
Url::parse("https://example.com/speculative.css").unwrap(),
|
|
crate::stylesheet_blocking::StylesheetFetchOptions::default(),
|
|
)
|
|
.is_none(),
|
|
"response policy admission must run before creating an ownerless resource"
|
|
);
|
|
assert!(runtime.take_ready_stylesheet_network_results().is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn typed_stylesheet_claim_preserves_blocking_resolution_state() {
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/app.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
|
|
let node_id = NodeId::new(first_element_handle(&document, "link").index());
|
|
let stylesheet_url = Url::parse("https://example.com/app.css").unwrap();
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.enqueue_completion_for_testing(node_id, stylesheet_url, true);
|
|
|
|
assert!(
|
|
runtime.wait_for_stylesheet_networking_task_for_test().await,
|
|
"the exact blocking terminal must remain resident in the typed Networking source"
|
|
);
|
|
assert!(
|
|
runtime.apply_next_stylesheet_networking_task_for_test(),
|
|
"the test must claim the same typed task consumed by a live Page"
|
|
);
|
|
|
|
assert!(
|
|
runtime.has_all_blocking_stylesheets_resolved(),
|
|
"claiming the typed task must persist the canonical blocking resolution"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stale_connected_stylesheet_network_result_releases_source_owner() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/current.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let stale_url = Url::parse("https://example.com/old.css").unwrap();
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.push_back(ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: None,
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document.final_url().unwrap().clone(),
|
|
request_url: stale_url.clone(),
|
|
source_owners: vec![link],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("stale connected stylesheet result".to_owned()),
|
|
});
|
|
|
|
let results = runtime.take_ready_stylesheet_network_results();
|
|
|
|
assert_eq!(results.len(), 1);
|
|
let result = results.into_iter().next().unwrap();
|
|
assert_eq!(result.request_url, stale_url);
|
|
assert!(
|
|
result.source_owners.is_empty(),
|
|
"stale connected stylesheet results must not retain a source owner"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn active_link_load_keeps_physical_observation_separate_from_client_terminal() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/current.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let current_url = Url::parse("https://example.com/current.css").unwrap();
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.queue_initial_connected_style_loads();
|
|
runtime.prime_document_lifecycle_processing();
|
|
let active_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("stylesheet processing should create an owner-bound load");
|
|
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.push_back(ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: Some(active_load.fetch().clone()),
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document.final_url().unwrap().clone(),
|
|
request_url: current_url.clone(),
|
|
source_owners: vec![link],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("current connected stylesheet result".to_owned()),
|
|
});
|
|
|
|
let results = runtime.take_ready_stylesheet_network_results();
|
|
|
|
assert_eq!(results.len(), 1);
|
|
let result = results.into_iter().next().unwrap();
|
|
assert_eq!(result.request_url, current_url);
|
|
assert!(
|
|
result.source_owners.is_empty(),
|
|
"typed physical observations must not double as stylesheet client delivery"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn removed_link_rejects_its_detached_load_network_source_owner() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/current.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.queue_initial_connected_style_loads();
|
|
runtime.prime_document_lifecycle_processing();
|
|
let detached_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("active stylesheet load");
|
|
let parent = runtime
|
|
.dom_host()
|
|
.parent_node(link)
|
|
.expect("link should have a parent");
|
|
assert!(runtime.dom_host_mut().remove_child(parent, link));
|
|
drop(runtime.invalidate_style_related_state(link));
|
|
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.push_back(ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: Some(detached_load.fetch().clone()),
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: detached_load.fetch().document_url().clone(),
|
|
request_url: detached_load.request_url().clone(),
|
|
source_owners: vec![link],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("detached load result".to_owned()),
|
|
});
|
|
|
|
let result = runtime
|
|
.take_ready_stylesheet_network_results()
|
|
.into_iter()
|
|
.next()
|
|
.expect("resource observation result");
|
|
assert!(result.source_owners.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disabled_link_rejects_its_detached_load_network_source_owner() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/current.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.queue_initial_connected_style_loads();
|
|
runtime.prime_document_lifecycle_processing();
|
|
let detached_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("active stylesheet load");
|
|
assert!(runtime.dom_host_mut().set_attribute(link, "disabled", ""));
|
|
drop(runtime.invalidate_style_related_state(link));
|
|
runtime.queue_connected_style_loads(link);
|
|
runtime.prime_document_lifecycle_processing();
|
|
assert!(
|
|
runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.is_none()
|
|
);
|
|
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.push_back(ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: Some(detached_load.fetch().clone()),
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: detached_load.fetch().document_url().clone(),
|
|
request_url: detached_load.request_url().clone(),
|
|
source_owners: vec![link],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("disabled load result".to_owned()),
|
|
});
|
|
|
|
let result = runtime
|
|
.take_ready_stylesheet_network_results()
|
|
.into_iter()
|
|
.next()
|
|
.expect("resource observation result");
|
|
assert!(result.source_owners.is_empty());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_url_reprocess_rejects_the_detached_owner_bound_load_object() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/a.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.queue_initial_connected_style_loads();
|
|
runtime.prime_document_lifecycle_processing();
|
|
let first_a_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("first A processing load");
|
|
|
|
drop(runtime.invalidate_style_related_state(link));
|
|
assert!(runtime.dom_host_mut().set_attribute(link, "href", "/b.css"));
|
|
runtime.queue_connected_style_loads(link);
|
|
runtime.prime_document_lifecycle_processing();
|
|
let b_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("B processing load");
|
|
|
|
drop(runtime.invalidate_style_related_state(link));
|
|
assert!(runtime.dom_host_mut().set_attribute(link, "href", "/a.css"));
|
|
runtime.queue_connected_style_loads(link);
|
|
runtime.prime_document_lifecycle_processing();
|
|
let second_a_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("second A processing load");
|
|
|
|
assert!(!super::stylesheet_runtime::StylesheetLinkClient::ptr_eq(
|
|
&first_a_load,
|
|
&b_load
|
|
));
|
|
assert!(!super::stylesheet_runtime::StylesheetLinkClient::ptr_eq(
|
|
&first_a_load,
|
|
&second_a_load
|
|
));
|
|
assert_eq!(first_a_load.request_url(), second_a_load.request_url());
|
|
assert!(
|
|
first_a_load.fetch().ptr_eq(second_a_load.fetch()),
|
|
"a new owner processing client should reuse the compatible document resource"
|
|
);
|
|
assert!(
|
|
!runtime.accept_stylesheet_link_client_completion_for_test(link, &first_a_load, true,),
|
|
"an old A load object must not complete the new A processing"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn linked_stylesheet_request_attribute_reprocess_captures_new_fetch_options() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
Url::parse("https://example.com/page").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href='http://127.0.0.1:9/a.css' crossorigin=anonymous integrity=sha256-first referrerpolicy=no-referrer fetchpriority=low></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let link = first_element_handle(&document, "link");
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.queue_initial_connected_style_loads();
|
|
runtime.prime_document_lifecycle_processing();
|
|
let first_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("first processing load");
|
|
assert_eq!(
|
|
first_load.fetch().options().cross_origin(),
|
|
Some("anonymous")
|
|
);
|
|
assert_eq!(
|
|
first_load.fetch().options().integrity(),
|
|
Some("sha256-first")
|
|
);
|
|
|
|
drop(runtime.invalidate_style_related_state(link));
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.set_attribute(link, "crossorigin", "use-credentials")
|
|
);
|
|
assert!(
|
|
runtime
|
|
.dom_host_mut()
|
|
.set_attribute(link, "integrity", "sha256-second")
|
|
);
|
|
runtime.queue_connected_style_loads(link);
|
|
runtime.prime_document_lifecycle_processing();
|
|
let second_load = runtime
|
|
.active_stylesheet_link_client_for_test(link)
|
|
.expect("reprocessed load");
|
|
|
|
assert!(!super::stylesheet_runtime::StylesheetLinkClient::ptr_eq(
|
|
&first_load,
|
|
&second_load
|
|
));
|
|
assert!(!first_load.fetch().ptr_eq(second_load.fetch()));
|
|
assert_eq!(
|
|
second_load.fetch().options().cross_origin(),
|
|
Some("use-credentials")
|
|
);
|
|
assert_eq!(
|
|
second_load.fetch().options().integrity(),
|
|
Some("sha256-second")
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn link_blocking_stylesheet_fetch_is_shared_across_document_lifecycle_state() {
|
|
use std::sync::{
|
|
Arc,
|
|
atomic::{AtomicUsize, Ordering},
|
|
};
|
|
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpListener,
|
|
time::{Duration, timeout},
|
|
};
|
|
|
|
let hits = Arc::new(AtomicUsize::new(0));
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server_hits = Arc::clone(&hits);
|
|
let server = tokio::spawn(async move {
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut buffer = [0_u8; 1024];
|
|
let _ = socket.read(&mut buffer).await.unwrap();
|
|
server_hits.fetch_add(1, Ordering::SeqCst);
|
|
socket
|
|
.write_all(
|
|
b"HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nContent-Length: 20\r\nConnection: close\r\n\r\nbody { color: red; }",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse(&format!("http://{addr}/page")).unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href='/app.css'><script defer src='/app.js'></script></head><body></body></html>".to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
|
|
let node_id = NodeId::new(first_element_handle(&document, "link").index());
|
|
let stylesheet_url = Url::parse(&format!("http://{addr}/app.css")).unwrap();
|
|
|
|
timeout(Duration::from_secs(2), async {
|
|
loop {
|
|
runtime.drain_blocking_stylesheet_completions();
|
|
let shared_resolved = runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.status(node_id, &stylesheet_url)
|
|
.is_some();
|
|
if shared_resolved && runtime.has_all_blocking_stylesheets_resolved() {
|
|
break;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("stylesheet fetch should resolve");
|
|
|
|
assert_eq!(
|
|
hits.load(Ordering::SeqCst),
|
|
1,
|
|
"link stylesheet fetch should be shared between connected-style and document-owned blocker state"
|
|
);
|
|
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn link_stylesheet_fetch_respects_nosniff_mime_blocking() {
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpListener,
|
|
time::{Duration, timeout},
|
|
};
|
|
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
const BODY: &str = "body { color: red; }";
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut buffer = [0_u8; 1024];
|
|
let _ = socket.read(&mut buffer).await.unwrap();
|
|
socket
|
|
.write_all(
|
|
format!(
|
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Content-Type-Options: nosniff\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
BODY.len(),
|
|
BODY
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse(&format!("http://{addr}/page")).unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href='/app.css'><script defer src='/app.js'></script></head><body></body></html>".to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
|
|
let node_id = NodeId::new(first_element_handle(&document, "link").index());
|
|
let stylesheet_url = Url::parse(&format!("http://{addr}/app.css")).unwrap();
|
|
|
|
let status = timeout(Duration::from_secs(2), async {
|
|
loop {
|
|
runtime.drain_blocking_stylesheet_completions();
|
|
if let Some(status) = runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.status(node_id, &stylesheet_url)
|
|
{
|
|
break status;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("stylesheet fetch should resolve");
|
|
|
|
runtime.drain_blocking_stylesheet_completions();
|
|
assert!(
|
|
!status,
|
|
"nosniff text/html stylesheet should fail readiness"
|
|
);
|
|
assert!(
|
|
runtime.has_all_blocking_stylesheets_resolved(),
|
|
"failed stylesheet readiness must unblock script scheduling"
|
|
);
|
|
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn parser_created_style_import_network_results_are_drained() {
|
|
use std::sync::{
|
|
Arc,
|
|
atomic::{AtomicUsize, Ordering},
|
|
};
|
|
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpListener,
|
|
time::{Duration, timeout},
|
|
};
|
|
|
|
const STYLESHEET_BODY: &str = "body { color: blue; }";
|
|
|
|
let hits = Arc::new(AtomicUsize::new(0));
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server_hits = Arc::clone(&hits);
|
|
let server = tokio::spawn(async move {
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut buffer = [0_u8; 1024];
|
|
let _ = socket.read(&mut buffer).await.unwrap();
|
|
server_hits.fetch_add(1, Ordering::SeqCst);
|
|
socket
|
|
.write_all(
|
|
format!(
|
|
"HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
STYLESHEET_BODY.len(),
|
|
STYLESHEET_BODY
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
let document_url = Url::parse(&format!("http://{addr}/page")).unwrap();
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><style>@import url('/imported.css');</style><script src='/app.js'></script></head><body></body></html>".to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
let script_node_id = NodeId::new(document.script_handles()[0].index());
|
|
let inputs = moli_stylesheet_blocking::collect_document_owned_blocking_stylesheets_before(
|
|
&document,
|
|
script_node_id,
|
|
)
|
|
.into_iter()
|
|
.map(|blocker| crate::DocumentOwnedStylesheetDiscoveryInput::from(&blocker))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
inputs.len(),
|
|
1,
|
|
"expected one parser-created style import blocker"
|
|
);
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(inputs.iter());
|
|
|
|
let results = timeout(Duration::from_secs(2), async {
|
|
loop {
|
|
let results = runtime.take_ready_stylesheet_network_results();
|
|
if !results.is_empty() {
|
|
break results;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("parser-created style import should resolve");
|
|
|
|
assert_eq!(results.len(), 1);
|
|
let result = results.into_iter().next().unwrap();
|
|
let result_document_url = result.document_url;
|
|
let request_url = result.request_url;
|
|
let source_owners = result.source_owners;
|
|
let result = result.result;
|
|
assert_eq!(result_document_url, document_url);
|
|
assert_eq!(
|
|
request_url,
|
|
Url::parse(&format!("http://{addr}/imported.css")).unwrap()
|
|
);
|
|
assert!(
|
|
source_owners.is_empty(),
|
|
"physical parser-blocking terminals must not carry opportunistic live-owner authority"
|
|
);
|
|
let response = result.expect("stylesheet import should succeed");
|
|
assert_eq!(response.status, 200);
|
|
assert_eq!(response.body_text(), STYLESHEET_BODY);
|
|
assert_eq!(hits.load(Ordering::SeqCst), 1);
|
|
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn parser_created_style_import_respects_nosniff_mime_blocking() {
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpListener,
|
|
time::{Duration, timeout},
|
|
};
|
|
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
const BODY: &str = "body { color: blue; }";
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut buffer = [0_u8; 1024];
|
|
let _ = socket.read(&mut buffer).await.unwrap();
|
|
socket
|
|
.write_all(
|
|
format!(
|
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nX-Content-Type-Options: nosniff\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
BODY.len(),
|
|
BODY
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
let document_url = Url::parse(&format!("http://{addr}/page")).unwrap();
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><style>@import url('/imported.css');</style><script src='/app.js'></script></head><body></body></html>".to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
let script_node_id = NodeId::new(document.script_handles()[0].index());
|
|
let inputs = moli_stylesheet_blocking::collect_document_owned_blocking_stylesheets_before(
|
|
&document,
|
|
script_node_id,
|
|
)
|
|
.into_iter()
|
|
.map(|blocker| crate::DocumentOwnedStylesheetDiscoveryInput::from(&blocker))
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(
|
|
inputs.len(),
|
|
1,
|
|
"expected one parser-created style import blocker"
|
|
);
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(inputs.iter());
|
|
|
|
let results = timeout(Duration::from_secs(2), async {
|
|
loop {
|
|
let results = runtime.take_ready_stylesheet_network_results();
|
|
if !results.is_empty() {
|
|
break results;
|
|
}
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await
|
|
.expect("parser-created style import should resolve");
|
|
|
|
assert_eq!(results.len(), 1);
|
|
let result = results.into_iter().next().unwrap();
|
|
let blocking_operation = result
|
|
.blocking_operation
|
|
.expect("parser-created import should retain its blocking operation");
|
|
let request_url = result.request_url;
|
|
let source_owners = result.source_owners;
|
|
let result = result.result;
|
|
assert_eq!(
|
|
request_url,
|
|
Url::parse(&format!("http://{addr}/imported.css")).unwrap()
|
|
);
|
|
assert!(
|
|
source_owners.is_empty(),
|
|
"an unusable physical terminal must remain separate from its one-shot install authority"
|
|
);
|
|
let response = result.expect("the HTTP response remains a physical network success");
|
|
assert_eq!(response.status, 200);
|
|
assert_eq!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.status_for_blocking_operation(&blocking_operation),
|
|
Some(crate::stylesheet_blocking::StylesheetBlockingStatus::Failed),
|
|
"nosniff text/html must fail stylesheet usability while retaining the HTTP response"
|
|
);
|
|
assert!(
|
|
runtime.has_all_blocking_stylesheets_resolved(),
|
|
"failed import readiness must unblock script scheduling"
|
|
);
|
|
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn connected_style_import_failure_marks_style_load_error() {
|
|
use tokio::{
|
|
io::{AsyncReadExt, AsyncWriteExt},
|
|
net::TcpListener,
|
|
time::{Duration, timeout},
|
|
};
|
|
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
let (mut socket, _) = listener.accept().await.unwrap();
|
|
let mut buffer = [0_u8; 1024];
|
|
let _ = socket.read(&mut buffer).await.unwrap();
|
|
const BODY: &str = "missing";
|
|
socket
|
|
.write_all(
|
|
format!(
|
|
"HTTP/1.1 404 Not Found\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
BODY.len(),
|
|
BODY
|
|
)
|
|
.as_bytes(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
});
|
|
|
|
let document_url = Url::parse(&format!("http://{addr}/page")).unwrap();
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><style>@import url('/missing.css');</style></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let document_loader = crate::network::context::DocumentResourceLoader::new(
|
|
loader.clone(),
|
|
crate::network::RendererResourceTaskRunner::from_current_tokio()
|
|
.expect("stylesheet test runtime"),
|
|
crate::network::context::DocumentFetchContext::new(
|
|
crate::native_bridge::WindowDocumentOwner::Frame(
|
|
super::runtime_core::test_stylesheet_document_owner(),
|
|
),
|
|
document_url.clone(),
|
|
document_url.clone(),
|
|
moli_url::origin_ascii_serialization(&document_url),
|
|
),
|
|
);
|
|
runtime.install_standalone_document_resource_loader(&document_loader);
|
|
let style = document
|
|
.document_head_handle()
|
|
.and_then(|head| document.child_nodes(head))
|
|
.and_then(|children| {
|
|
children.into_iter().find(|handle| {
|
|
document
|
|
.node(*handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element("style"))
|
|
})
|
|
})
|
|
.expect("style handle");
|
|
|
|
runtime.queue_initial_connected_style_loads();
|
|
runtime.prime_document_lifecycle_processing();
|
|
assert!(
|
|
timeout(
|
|
Duration::from_secs(2),
|
|
runtime.wait_for_stylesheet_networking_task_for_test()
|
|
)
|
|
.await
|
|
.expect("connected style import should resolve"),
|
|
"the stylesheet Networking source must remain open"
|
|
);
|
|
assert!(
|
|
runtime.apply_ready_stylesheet_networking_tasks_for_test(),
|
|
"the completed import must publish typed Networking work"
|
|
);
|
|
let ready = runtime
|
|
.pop_ready_connected_style_load()
|
|
.expect("the import completion must publish its typed style event");
|
|
|
|
assert_eq!(ready.owner(), style);
|
|
assert!(!ready.successful());
|
|
let results = runtime.take_ready_stylesheet_network_results();
|
|
assert_eq!(results.len(), 1);
|
|
let result = results.into_iter().next().unwrap();
|
|
assert_eq!(
|
|
result.request_url,
|
|
Url::parse(&format!("http://{addr}/missing.css")).unwrap()
|
|
);
|
|
let response = result
|
|
.result
|
|
.expect("HTTP failure must retain its physical response");
|
|
assert_eq!(response.status, 404);
|
|
assert_eq!(response.body_text(), "missing");
|
|
|
|
server.abort();
|
|
}
|
|
|
|
#[test]
|
|
fn drained_preload_like_network_result_still_drives_load_event_result() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document_url = Url::parse("https://example.com/page").unwrap();
|
|
let request_url = Url::parse("https://example.com/chunk.js").unwrap();
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><link rel=preload as=script href=/chunk.js></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let link = document
|
|
.document_head_handle()
|
|
.and_then(|head| document.child_nodes(head))
|
|
.and_then(|children| {
|
|
children.into_iter().find(|handle| {
|
|
document
|
|
.node(*handle)
|
|
.and_then(Node::as_element)
|
|
.is_some_and(|element| element.is_html_element("link"))
|
|
})
|
|
})
|
|
.expect("link handle");
|
|
let operation = ConnectedLoadOperation::new_with_load_event_binding(
|
|
link,
|
|
crate::document_runtime::ConnectedStyleEventElementKind::Link,
|
|
preload_load_parameters(request_url.clone()),
|
|
None,
|
|
Some(
|
|
crate::frame_owner_model::MainDocumentStyleLoadEventBinding::unowned_for_document_runtime_test(
|
|
link,
|
|
),
|
|
),
|
|
);
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(&operation));
|
|
runtime.apply_connected_style_load_completion(ConnectedLoadCompletion {
|
|
operation,
|
|
successful: false,
|
|
network_results: vec![ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: None,
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document_url.clone(),
|
|
request_url: request_url.clone(),
|
|
source_owners: vec![link],
|
|
resource_type: SubresourceResourceType::Script,
|
|
start_unix_millis: None,
|
|
origin_clean: true,
|
|
result: Err("preload failed".to_owned()),
|
|
}],
|
|
});
|
|
|
|
runtime.prime_document_lifecycle_processing();
|
|
let ready = runtime
|
|
.pop_ready_connected_style_load()
|
|
.expect("failed preload event task");
|
|
assert_eq!(ready.owner(), link);
|
|
assert!(!ready.successful());
|
|
|
|
let results = runtime.take_ready_stylesheet_network_results();
|
|
assert_eq!(results.len(), 1);
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn same_parameter_connected_completion_cannot_cross_operation_identity() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document_url = Url::parse("https://example.com/page").unwrap();
|
|
let request_url = Url::parse("https://example.com/a.css").unwrap();
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><link rel=preload as=style href=/a.css></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let owner = first_element_handle(&document, "link");
|
|
let inline_source = Arc::new(crate::style_engine::OwnerStyleSheetSource::new(
|
|
owner,
|
|
"@import url(a.css);".to_owned(),
|
|
document_url.clone(),
|
|
));
|
|
|
|
for parameters in [
|
|
preload_load_parameters(request_url.clone()),
|
|
ConnectedLoadParameters::StyleImports {
|
|
source: super::stylesheet_runtime::InlineStyleImportSource::new(Arc::clone(
|
|
&inline_source,
|
|
)),
|
|
urls: vec![request_url.clone()],
|
|
roots: Vec::new(),
|
|
},
|
|
] {
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
let first_a = ConnectedLoadOperation::new_for_test(
|
|
owner,
|
|
ConnectedStyleEventElementKind::Link,
|
|
parameters.clone(),
|
|
None,
|
|
);
|
|
let middle_b = ConnectedLoadOperation::new_for_test(
|
|
owner,
|
|
ConnectedStyleEventElementKind::Link,
|
|
preload_load_parameters(Url::parse("https://example.com/b.css").unwrap()),
|
|
None,
|
|
);
|
|
let current_a = ConnectedLoadOperation::new_for_test(
|
|
owner,
|
|
ConnectedStyleEventElementKind::Link,
|
|
parameters,
|
|
None,
|
|
);
|
|
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(&first_a));
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(middle_b);
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(¤t_a));
|
|
runtime.apply_connected_style_load_completion(ConnectedLoadCompletion {
|
|
operation: first_a,
|
|
successful: true,
|
|
network_results: vec![ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: None,
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document_url.clone(),
|
|
request_url: request_url.clone(),
|
|
source_owners: vec![owner],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("stale A completion".to_owned()),
|
|
}],
|
|
});
|
|
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.pending_operation(owner)
|
|
.is_some_and(|pending| ConnectedLoadOperation::ptr_eq(pending, ¤t_a)),
|
|
"the new A processing object must remain pending"
|
|
);
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.injected_ready_connected_loads
|
|
.is_empty()
|
|
);
|
|
let stale_result = runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.pop_front()
|
|
.expect("stale completion still records network observation");
|
|
assert!(stale_result.source_owners.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn accepted_style_import_result_installs_only_for_its_exact_processing_operation() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document_url = Url::parse("https://example.com/page").unwrap();
|
|
let request_url = Url::parse("https://example.com/shared.css").unwrap();
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><style>@import url('/shared.css');</style></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let owner = first_element_handle(&document, "style");
|
|
let source = Arc::new(crate::style_engine::OwnerStyleSheetSource::new(
|
|
owner,
|
|
"@import url('/shared.css');".to_owned(),
|
|
document_url.clone(),
|
|
));
|
|
let operation = ConnectedLoadOperation::new_for_test(
|
|
owner,
|
|
ConnectedStyleEventElementKind::Style,
|
|
ConnectedLoadParameters::StyleImports {
|
|
source: super::stylesheet_runtime::InlineStyleImportSource::new(source),
|
|
urls: vec![request_url.clone()],
|
|
roots: Vec::new(),
|
|
},
|
|
None,
|
|
);
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(&operation));
|
|
runtime.apply_connected_style_load_completion(ConnectedLoadCompletion {
|
|
operation,
|
|
successful: true,
|
|
network_results: vec![ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: None,
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document_url.clone(),
|
|
request_url: request_url.clone(),
|
|
source_owners: vec![owner],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("accepted source observation".to_owned()),
|
|
}],
|
|
});
|
|
let result = runtime
|
|
.take_ready_stylesheet_network_results()
|
|
.into_iter()
|
|
.next()
|
|
.expect("accepted source result");
|
|
|
|
assert_eq!(result.source_owners, vec![owner]);
|
|
}
|
|
|
|
#[test]
|
|
fn accepted_style_import_result_loses_install_authority_after_same_url_aba() {
|
|
let parser = HtmlParser::SCRIPTING_ENABLED;
|
|
let document_url = Url::parse("https://example.com/page").unwrap();
|
|
let request_url = Url::parse("https://example.com/shared.css").unwrap();
|
|
let document = parser.parse(
|
|
document_url.clone(),
|
|
"<!doctype html><html><head><style>@import url('/shared.css');</style></head><body></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let owner = first_element_handle(&document, "style");
|
|
let operation_for = |css_text: &str| {
|
|
let source = Arc::new(crate::style_engine::OwnerStyleSheetSource::new(
|
|
owner,
|
|
css_text.to_owned(),
|
|
document_url.clone(),
|
|
));
|
|
ConnectedLoadOperation::new_for_test(
|
|
owner,
|
|
ConnectedStyleEventElementKind::Style,
|
|
ConnectedLoadParameters::StyleImports {
|
|
source: super::stylesheet_runtime::InlineStyleImportSource::new(source),
|
|
urls: vec![request_url.clone()],
|
|
roots: Vec::new(),
|
|
},
|
|
None,
|
|
)
|
|
};
|
|
let first_a = operation_for("@import url('/shared.css'); /* first A */");
|
|
let mut runtime = DocumentRuntime::new(&document);
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(&first_a));
|
|
runtime.apply_connected_style_load_completion(ConnectedLoadCompletion {
|
|
operation: first_a,
|
|
successful: true,
|
|
network_results: vec![ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: None,
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document_url.clone(),
|
|
request_url: request_url.clone(),
|
|
source_owners: vec![owner],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("first A source observation".to_owned()),
|
|
}],
|
|
});
|
|
|
|
drop(runtime.invalidate_style_related_state(owner));
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(operation_for(
|
|
"@import url('/middle.css'); /* B */ @import url('/shared.css');",
|
|
));
|
|
drop(runtime.invalidate_style_related_state(owner));
|
|
let current_a = operation_for("@import url('/shared.css'); /* current A */");
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(¤t_a));
|
|
|
|
let stale = runtime
|
|
.take_ready_stylesheet_network_results()
|
|
.into_iter()
|
|
.next()
|
|
.expect("stale first A observation remains reportable");
|
|
|
|
assert!(stale.source_owners.is_empty());
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.pending_operation(owner)
|
|
.is_some_and(|pending| ConnectedLoadOperation::ptr_eq(pending, ¤t_a)),
|
|
"the current A operation must retain its pending authority"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn blocked_front_page_task_does_not_count_as_ready_document_processing_wake() {
|
|
let (document, stylesheet_inputs) =
|
|
parse_document_with_stylesheet_inputs(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><head><link rel=stylesheet href=/app.css></head><body><script defer src=/app.js></script></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
let mut task_queue = crate::page_task_queue::PageTaskQueueTestHarness::new();
|
|
let script = document.script_handles()[0];
|
|
|
|
let script_work = PageOwnedDocumentScriptWork::script(
|
|
DocumentScriptExecutionLane::ClassicDefer,
|
|
crate::planning::PreparedScript {
|
|
position: 0,
|
|
node_id: crate::dom::NodeId::new(script.index()),
|
|
kind: crate::types::ScriptKind::Classic,
|
|
mode: crate::types::ScriptMode::Defer,
|
|
source_kind: crate::types::ScriptSourceKind::External,
|
|
fetch_metadata: crate::planning::ScriptFetchMetadata::default(),
|
|
source: crate::planning::ScriptSource::External,
|
|
initiator_url: Url::parse("https://example.com/app.js").unwrap(),
|
|
base_url: Url::parse("https://example.com/app.js").unwrap(),
|
|
url: Url::parse("https://example.com/app.js").unwrap(),
|
|
host_script_handle: None,
|
|
},
|
|
);
|
|
task_queue.extend_post_parse_work([
|
|
PostParsePageOwnedWork::document_script_work_with_blocking_signatures(
|
|
script_work,
|
|
std::collections::HashSet::from([
|
|
crate::DocumentBlockingStylesheetSignature::Link {
|
|
url: Url::parse("https://example.com/app.css").unwrap(),
|
|
options: crate::stylesheet_blocking::StylesheetFetchOptions::default(),
|
|
},
|
|
]),
|
|
),
|
|
]);
|
|
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
|
|
assert!(!runtime.has_ready_document_processing_wake(&mut task_queue));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn open_document_resets_document_owned_lifecycle_state() {
|
|
let (document, stylesheet_inputs) = parse_document_with_stylesheet_inputs(
|
|
Url::parse("https://example.com/").unwrap(),
|
|
"<!doctype html><html><body><link rel=stylesheet href=/app.css></body></html>"
|
|
.to_owned(),
|
|
);
|
|
let loader = ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
|
let mut runtime = DocumentRuntime::new_networked(&document, &loader);
|
|
let handle = first_element_handle(&document, "link");
|
|
runtime.note_discovered_document_owned_stylesheet_inputs(stylesheet_inputs.iter());
|
|
|
|
runtime.enqueue_pending_connected_style_load_for_test(handle);
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.injected_ready_connected_loads
|
|
.push_back(crate::document_runtime::ReadyConnectedStyleLoad::for_owner(
|
|
handle,
|
|
false,
|
|
crate::document_runtime::ConnectedStyleEventElementKind::Link,
|
|
));
|
|
let pending_operation = ConnectedLoadOperation::new_for_test(
|
|
handle,
|
|
ConnectedStyleEventElementKind::Link,
|
|
preload_load_parameters(Url::parse("https://example.com/app.css").unwrap()),
|
|
None,
|
|
);
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.owner_states
|
|
.install_pending_operation(Arc::clone(&pending_operation));
|
|
let node_id = crate::dom::NodeId::new(handle.index());
|
|
let stylesheet_url = Url::parse("https://example.com/app.css").unwrap();
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.enqueue_completion_for_testing(node_id, stylesheet_url.clone(), true);
|
|
let _ = runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.drain_ready_completions();
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.push_back(ConnectedLoadNetworkResult {
|
|
stylesheet_fetch: None,
|
|
blocking_operation: None,
|
|
source_operation: None,
|
|
import_roots: Vec::new(),
|
|
document_url: document.final_url().unwrap().clone(),
|
|
request_url: stylesheet_url.clone(),
|
|
source_owners: vec![handle],
|
|
resource_type: SubresourceResourceType::Stylesheet,
|
|
start_unix_millis: Some(1.0),
|
|
origin_clean: true,
|
|
result: Err("stale connected stylesheet result".to_owned()),
|
|
});
|
|
runtime.absorb_runtime_binding_calls(vec![PendingRuntimeBindingCall {
|
|
source: crate::protocol_types::RuntimeBindingCallSourceIdentity::new(1, 1),
|
|
name: "binding".to_owned(),
|
|
payload: "{}".to_owned(),
|
|
execution_context_id: 1,
|
|
}]);
|
|
|
|
runtime.open_document();
|
|
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.pending_connected_loads
|
|
.is_empty()
|
|
);
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.injected_ready_connected_loads
|
|
.is_empty()
|
|
);
|
|
assert!(runtime.stylesheet_lifecycle.owner_states.is_empty());
|
|
assert!(
|
|
!runtime
|
|
.stylesheet_lifecycle
|
|
.fetches
|
|
.has_any_pending_entries()
|
|
);
|
|
assert!(runtime.pop_ready_connected_style_load().is_none());
|
|
assert!(
|
|
runtime
|
|
.stylesheet_lifecycle
|
|
.ready_connected_load_network_results
|
|
.is_empty()
|
|
);
|
|
let stale_results = runtime.take_ready_stylesheet_network_results();
|
|
assert_eq!(
|
|
stale_results.len(),
|
|
1,
|
|
"the retired typed terminal remains a resource observation"
|
|
);
|
|
assert_eq!(stale_results[0].request_url, stylesheet_url);
|
|
assert!(
|
|
stale_results[0].source_owners.is_empty(),
|
|
"document.open() must revoke the retired terminal's stylesheet install authority"
|
|
);
|
|
assert_eq!(
|
|
runtime.pending_runtime_binding_call_count(),
|
|
1,
|
|
"document.open() must retain a Runtime binding observation accepted before Document replacement"
|
|
);
|
|
}
|
|
}
|