diff --git a/moli-protocol/src/domains/input/tests.rs b/moli-protocol/src/domains/input/tests.rs
index 6568ba86de..8195ef2809 100644
--- a/moli-protocol/src/domains/input/tests.rs
+++ b/moli-protocol/src/domains/input/tests.rs
@@ -1888,10 +1888,13 @@ async fn coordinate_drag_event_completes_through_pending_layout_dispatch() {
@@ -1933,7 +1936,12 @@ async fn coordinate_drag_event_completes_through_pending_layout_dispatch() {
assert_eq!(messages, vec![json!({ "id": 4107, "result": {} })]);
assert_eq!(
evaluate_string(&mut ctx, "JSON.stringify(window.__dragPending)").await,
- r#"["dragenter|true|drag-pending"]"#
+ r#"["dragenter|true||text/plain|string"]"#
+ );
+ assert_eq!(
+ evaluate_string(&mut ctx, "JSON.stringify([__savedDragTransfer.items.length,__savedDragTransfer.types.length,__savedDragTransfer.getData('text/plain')])").await,
+ r#"[0,0,""]"#,
+ "the saved event view must be disabled when the protocol command completes"
);
}
diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs
index ed48cc51fd..7941a543a1 100644
--- a/moli-renderer-v8/src/context_bootstrap.rs
+++ b/moli-renderer-v8/src/context_bootstrap.rs
@@ -332,9 +332,10 @@ pub(crate) use self::file_api::{
DirectoryReaderCallbackTaskEffect, FileEntryFileCallbackTask, FileEntryFileCallbackTaskEffect,
};
pub(crate) use self::file_api::{
- allowed_drag_drop_effect, apply_drag_modifier_drop_effect, build_clipboard_data_transfer,
- build_data_transfer_object, clipboard_data_transfer_contents, disable_clipboard_data_transfer,
- initialize_native_drag_data_transfer, prepare_drag_drop_effect, set_drag_drop_effect,
+ DragDataStore, allowed_drag_drop_effect, apply_drag_modifier_drop_effect,
+ build_clipboard_data_transfer, clipboard_data_transfer_contents,
+ disable_clipboard_data_transfer, initialize_native_drag_data_transfer,
+ prepare_drag_drop_effect, set_drag_drop_effect,
};
pub(crate) use self::file_api::{
build_file_list_object, build_file_object, file_list_files_from_object,
diff --git a/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs b/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs
index 82370dcd6a..dadd947d4a 100644
--- a/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs
+++ b/moli-renderer-v8/src/context_bootstrap/assets/prototype_bindings.rs
@@ -207,7 +207,7 @@ struct WorkerTemplateMethodsDeclaration {
}
#[derive(WebApiFunctionTemplate)]
-#[webapi(interface = web_api_interfaces::DataTransfer, enumerable)]
+#[webapi(interface = web_api_interfaces::DataTransfer, enumerable, receiver)]
struct DataTransferTemplateMethodsDeclaration {
#[webapi(
method = "getData",
@@ -232,7 +232,7 @@ struct DataTransferTemplateMethodsDeclaration {
}
#[derive(WebApiFunctionTemplate)]
-#[webapi(interface = web_api_interfaces::DataTransferItem, enumerable)]
+#[webapi(interface = web_api_interfaces::DataTransferItem, enumerable, receiver)]
struct DataTransferItemTemplateMethodsDeclaration {
#[webapi(
method = "getAsFile",
@@ -257,7 +257,7 @@ struct DataTransferItemTemplateMethodsDeclaration {
}
#[derive(WebApiFunctionTemplate)]
-#[webapi(interface = web_api_interfaces::DataTransferItemList, enumerable)]
+#[webapi(interface = web_api_interfaces::DataTransferItemList, enumerable, receiver)]
struct DataTransferItemListTemplateMethodsDeclaration {
#[webapi(
intrinsic_data_property = v8::Intrinsic::ArrayProtoValues,
diff --git a/moli-renderer-v8/src/context_bootstrap/file_api.rs b/moli-renderer-v8/src/context_bootstrap/file_api.rs
index a84fbc4eb9..800c660521 100644
--- a/moli-renderer-v8/src/context_bootstrap/file_api.rs
+++ b/moli-renderer-v8/src/context_bootstrap/file_api.rs
@@ -12,10 +12,10 @@ mod install;
pub(super) use data_transfer::data_transfer_constructor_callback;
pub(crate) use data_transfer::{
- allowed_drag_drop_effect, apply_drag_modifier_drop_effect, build_clipboard_data_transfer,
- build_data_transfer_object, clipboard_data_transfer_contents, disable_clipboard_data_transfer,
- initialize_native_drag_data_transfer, is_branded_data_transfer_object,
- prepare_drag_drop_effect, set_drag_drop_effect,
+ DragDataStore, allowed_drag_drop_effect, apply_drag_modifier_drop_effect,
+ build_clipboard_data_transfer, clipboard_data_transfer_contents,
+ disable_clipboard_data_transfer, initialize_native_drag_data_transfer,
+ is_branded_data_transfer_object, prepare_drag_drop_effect, set_drag_drop_effect,
};
pub(crate) use data_transfer::{
data_transfer_clear_data_callback, data_transfer_get_data_callback,
diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer.rs b/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer.rs
index ced8c9a513..ee712350e3 100644
--- a/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer.rs
+++ b/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer.rs
@@ -14,10 +14,12 @@ use moli_file_api::data_transfer::{
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
mod clipboard;
+mod drag_store;
pub(crate) use clipboard::{
build_clipboard_data_transfer, clipboard_data_transfer_contents,
disable_clipboard_data_transfer,
};
+pub(crate) use drag_store::DragDataStore;
const DATA_TRANSFER_FILES_SLOT: &str = "__lmDataTransferFiles";
const DATA_TRANSFER_ITEMS_SLOT: &str = "__lmDataTransferItems";
@@ -26,14 +28,18 @@ const DATA_TRANSFER_DROP_EFFECT_SLOT: &str = "__lmDataTransferDropEffect";
const DATA_TRANSFER_EFFECT_ALLOWED_SLOT: &str = "__lmDataTransferEffectAllowed";
const DATA_TRANSFER_MODE_SLOT: &str = "__lmDataTransferMode";
const DATA_TRANSFER_CLIPBOARD_SLOT: &str = "__lmDataTransferClipboard";
+const DATA_TRANSFER_STORE_SLOT: &str = "__lmDataTransferStore";
const DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT: &str = "__lmDataTransferItemArray";
const DATA_TRANSFER_ITEM_LIST_OWNER_SLOT: &str = "__lmDataTransferOwner";
+const DATA_TRANSFER_ITEM_LIST_VIEWS_SLOT: &str = "__lmDataTransferItemViews";
const DATA_TRANSFER_ITEM_LIST_INDEXED_LENGTH_SLOT: &str = "__lmDataTransferItemListIndexedLength";
const DATA_TRANSFER_ITEM_KIND_SLOT: &str = "__lmDataTransferItemKind";
const DATA_TRANSFER_ITEM_TYPE_SLOT: &str = "__lmDataTransferItemType";
const DATA_TRANSFER_ITEM_FILE_SLOT: &str = "__lmDataTransferItemFile";
const DATA_TRANSFER_ITEM_ENTRY_SLOT: &str = "__lmDataTransferItemEntry";
const DATA_TRANSFER_ITEM_STRING_SLOT: &str = "__lmDataTransferItemString";
+const DATA_TRANSFER_ITEM_SOURCE_SLOT: &str = "__lmDataTransferItemSource";
+const DATA_TRANSFER_ITEM_OWNER_SLOT: &str = "__lmDataTransferItemOwner";
const FILE_SYSTEM_ENTRY_FILESYSTEM_SLOT: &str = "__lmFileSystemEntryFilesystem";
const FILE_SYSTEM_ENTRY_FULL_PATH_SLOT: &str = "__lmFileSystemEntryFullPath";
const FILE_SYSTEM_ENTRY_IS_DIRECTORY_SLOT: &str = "__lmFileSystemEntryIsDirectory";
@@ -99,10 +105,18 @@ struct DataTransferShellDeclaration {
}
#[derive(Clone, Copy, PartialEq, Eq)]
+#[repr(u8)]
enum DataTransferMode {
- ReadWrite,
- ReadOnly,
- Disabled,
+ ReadWrite = 0,
+ ReadOnly = 1,
+ Disabled = 2,
+ Protected = 3,
+}
+
+impl DataTransferMode {
+ fn can_read(self) -> bool {
+ matches!(self, Self::ReadWrite | Self::ReadOnly)
+ }
}
fn data_transfer_mode<'s>(
@@ -112,6 +126,7 @@ fn data_transfer_mode<'s>(
match private_number_property(scope, object, DATA_TRANSFER_MODE_SLOT) {
Some(1.0) => DataTransferMode::ReadOnly,
Some(2.0) => DataTransferMode::Disabled,
+ Some(3.0) => DataTransferMode::Protected,
_ => DataTransferMode::ReadWrite,
}
}
@@ -125,7 +140,7 @@ fn item_list_is_writable<'s>(
}
#[derive(WebApiFunctionTemplate)]
-#[webapi(interface = web_api_interfaces::DataTransfer)]
+#[webapi(interface = web_api_interfaces::DataTransfer, receiver)]
struct DataTransferPrototypeAccessorsDeclaration {
#[webapi(accessor_property, getter = data_transfer_files_getter, enumerable)]
files: (),
@@ -159,6 +174,8 @@ struct DataTransferItemListObjectDeclaration<'s> {
items: Vec>,
#[webapi(slot = DATA_TRANSFER_ITEM_LIST_OWNER_SLOT)]
owner: v8::Local<'s, v8::Object>,
+ #[webapi(slot = DATA_TRANSFER_ITEM_LIST_VIEWS_SLOT)]
+ views: v8::Local<'s, v8::Map>,
#[webapi(
slot = DATA_TRANSFER_ITEM_LIST_INDEXED_LENGTH_SLOT,
constructor_default = 0.0
@@ -167,7 +184,7 @@ struct DataTransferItemListObjectDeclaration<'s> {
}
#[derive(WebApiFunctionTemplate)]
-#[webapi(interface = web_api_interfaces::DataTransferItemList)]
+#[webapi(interface = web_api_interfaces::DataTransferItemList, receiver)]
struct DataTransferItemListPrototypeAccessorsDeclaration {
#[webapi(accessor_property, getter = data_transfer_item_list_length_getter, enumerable)]
length: (),
@@ -208,8 +225,19 @@ struct DataTransferStringItemObjectDeclaration<'item_type, 'data> {
data: &'data str,
}
+// Item payloads belong to the store; each item list publishes its own stable
+// views whose access follows the associated DataTransfer's lifetime and mode.
+#[derive(WebApiObject)]
+#[webapi(interface = web_api_interfaces::DataTransferItem, require_prototype)]
+struct DataTransferItemViewDeclaration<'s> {
+ #[webapi(slot = DATA_TRANSFER_ITEM_SOURCE_SLOT)]
+ source: v8::Local<'s, v8::Object>,
+ #[webapi(slot = DATA_TRANSFER_ITEM_OWNER_SLOT)]
+ owner: v8::Local<'s, v8::Object>,
+}
+
#[derive(WebApiFunctionTemplate)]
-#[webapi(interface = web_api_interfaces::DataTransferItem)]
+#[webapi(interface = web_api_interfaces::DataTransferItem, receiver)]
struct DataTransferItemPrototypeAccessorsDeclaration {
#[webapi(accessor_property, getter = data_transfer_item_kind_getter, enumerable)]
kind: (),
@@ -542,6 +570,63 @@ fn item_list_owner<'s>(
get_private_object(scope, item_list, DATA_TRANSFER_ITEM_LIST_OWNER_SLOT)
}
+fn item_views<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ item_list: v8::Local<'s, v8::Object>,
+) -> Option> {
+ get_private_value(scope, item_list, DATA_TRANSFER_ITEM_LIST_VIEWS_SLOT)
+ .and_then(|value| v8::Local::::try_from(value).ok())
+}
+
+fn data_transfer_item_view<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ item_list: v8::Local<'s, v8::Object>,
+ source: v8::Local<'s, v8::Object>,
+) -> Option> {
+ let views = item_views(scope, item_list)?;
+ if let Some(value) = views.get(scope, source.into())
+ && let Ok(view) = v8::Local::::try_from(value)
+ {
+ return Some(view);
+ }
+ let owner = item_list_owner(scope, item_list)?;
+ let view = DataTransferItemViewDeclaration::new(source, owner)
+ .bind(scope)
+ .ok()?;
+ views.set(scope, source.into(), view.into())?;
+ Some(view)
+}
+
+fn item_data_object<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ item: v8::Local<'s, v8::Object>,
+) -> v8::Local<'s, v8::Object> {
+ get_private_object(scope, item, DATA_TRANSFER_ITEM_SOURCE_SLOT).unwrap_or(item)
+}
+
+fn item_is_associated<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ item: v8::Local<'s, v8::Object>,
+) -> bool {
+ if let Some(owner) = get_private_object(scope, item, DATA_TRANSFER_ITEM_OWNER_SLOT) {
+ data_transfer_mode(scope, owner) != DataTransferMode::Disabled
+ && get_private_object(scope, item, DATA_TRANSFER_ITEM_SOURCE_SLOT).is_some()
+ } else {
+ // Canonical records never escape the store; removed records have empty metadata.
+ private_string_property(scope, item, DATA_TRANSFER_ITEM_KIND_SLOT)
+ .is_some_and(|kind| !kind.is_empty())
+ }
+}
+
+pub(super) fn item_is_readable<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ item: v8::Local<'s, v8::Object>,
+) -> bool {
+ item_is_associated(scope, item)
+ && get_private_object(scope, item, DATA_TRANSFER_ITEM_OWNER_SLOT)
+ .is_none_or(|owner| data_transfer_mode(scope, owner).can_read())
+}
+
// Every write to the canonical item array runs inside DataTransferItemStore::mutate. The
// outcome controls the single observer notification and, consequently, the cached `types`
// identity required by Web IDL's CachedAttribute semantics.
@@ -581,6 +666,21 @@ impl<'s> DataTransferItemStore<'s> {
DataTransferItemListMutation::Changed(value) => (value, true),
};
if changed {
+ // Native event views share the backing item array. Array replacements
+ // must also reach the session before another event opens its view.
+ if let Some(owner) = item_list_owner(scope, self.item_list)
+ && let Some(backing) = get_private_object(scope, owner, DATA_TRANSFER_STORE_SLOT)
+ && let Some(backing_items) = Self::for_owner(scope, backing)
+ && let Some(array) = item_list_array(scope, self.item_list)
+ {
+ set_private_value(
+ scope,
+ backing_items.item_list,
+ DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT,
+ array.into(),
+ );
+ data_transfer_item_list_did_change(scope, backing_items.item_list);
+ }
data_transfer_item_list_did_change(scope, self.item_list);
}
Some(value)
@@ -623,14 +723,16 @@ impl<'s> DataTransferItemStore<'s> {
scope: &mut v8::PinScope<'s, '_>,
item: v8::Local<'s, v8::Object>,
) -> Option> {
- self.mutate(scope, |scope, item_array| {
- if append_item(scope, item_array, item).is_some() {
- DataTransferItemListMutation::Changed(Some(item))
- } else {
- DataTransferItemListMutation::Unchanged(None)
- }
- })
- .flatten()
+ let added = self
+ .mutate(scope, |scope, item_array| {
+ if append_item(scope, item_array, item).is_some() {
+ DataTransferItemListMutation::Changed(Some(item))
+ } else {
+ DataTransferItemListMutation::Unchanged(None)
+ }
+ })
+ .flatten()?;
+ data_transfer_item_view(scope, self.item_list, added)
}
fn set_string_data(self, scope: &mut v8::PinScope<'s, '_>, mime_type: &str, data: &str) {
@@ -751,35 +853,46 @@ pub(super) fn item_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
item: v8::Local<'s, v8::Object>,
) -> Option {
- private_string_property(scope, item, DATA_TRANSFER_ITEM_KIND_SLOT)
+ if !item_is_associated(scope, item) {
+ return Some(String::new());
+ }
+ let source = item_data_object(scope, item);
+ private_string_property(scope, source, DATA_TRANSFER_ITEM_KIND_SLOT)
}
fn item_type<'s>(
scope: &mut v8::PinScope<'s, '_>,
item: v8::Local<'s, v8::Object>,
) -> Option {
- private_string_property(scope, item, DATA_TRANSFER_ITEM_TYPE_SLOT)
+ if !item_is_associated(scope, item) {
+ return Some(String::new());
+ }
+ let source = item_data_object(scope, item);
+ private_string_property(scope, source, DATA_TRANSFER_ITEM_TYPE_SLOT)
}
pub(super) fn item_string_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
item: v8::Local<'s, v8::Object>,
) -> Option {
- private_string_property(scope, item, DATA_TRANSFER_ITEM_STRING_SLOT)
+ let source = item_data_object(scope, item);
+ private_string_property(scope, source, DATA_TRANSFER_ITEM_STRING_SLOT)
}
fn item_file_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
item: v8::Local<'s, v8::Object>,
) -> Option> {
- get_private_object(scope, item, DATA_TRANSFER_ITEM_FILE_SLOT)
+ let source = item_data_object(scope, item);
+ get_private_object(scope, source, DATA_TRANSFER_ITEM_FILE_SLOT)
}
fn item_entry_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
item: v8::Local<'s, v8::Object>,
) -> Option> {
- get_private_object(scope, item, DATA_TRANSFER_ITEM_ENTRY_SLOT)
+ let source = item_data_object(scope, item);
+ get_private_object(scope, source, DATA_TRANSFER_ITEM_ENTRY_SLOT)
}
fn item_summary<'s>(
@@ -882,7 +995,8 @@ fn build_data_transfer_item_list_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
owner: v8::Local<'s, v8::Object>,
) -> Option> {
- DataTransferItemListObjectDeclaration::new(owner)
+ let views = v8::Map::new(scope);
+ DataTransferItemListObjectDeclaration::new(owner, views)
.bind(scope)
.ok()
}
@@ -1078,6 +1192,39 @@ fn data_transfer_item_list_did_change<'s>(
DATA_TRANSFER_TYPES_SLOT,
v8::undefined(scope).into(),
);
+ let mode = data_transfer_mode(scope, owner);
+ // Removed items and retired event views must not retain payload records in
+ // the identity cache. Disassociation never clears the shared store itself.
+ if let Some(views) = item_views(scope, item_list) {
+ let entries = views.as_array(scope);
+ let null = v8::null(scope);
+ for index in (0..entries.length()).step_by(2) {
+ let Some(key) = entries.get_index(scope, index) else {
+ continue;
+ };
+ let Ok(source) = v8::Local::::try_from(key) else {
+ continue;
+ };
+ if mode != DataTransferMode::Disabled && item_is_associated(scope, source) {
+ continue;
+ }
+ if let Some(value) = entries.get_index(scope, index + 1)
+ && let Ok(view) = v8::Local::::try_from(value)
+ {
+ set_private_value(scope, view, DATA_TRANSFER_ITEM_SOURCE_SLOT, null.into());
+ }
+ let _ = views.delete(scope, key);
+ }
+ }
+ if mode == DataTransferMode::Disabled {
+ let empty = v8::Array::new(scope, 0);
+ set_private_value(
+ scope,
+ item_list,
+ DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT,
+ empty.into(),
+ );
+ }
let Some(item_array) = item_list_array(scope, item_list) else {
return;
};
@@ -1099,8 +1246,11 @@ fn data_transfer_item_list_did_change<'s>(
let Ok(item) = v8::Local::::try_from(value) else {
continue;
};
- let _ = item_list.set_index(scope, index, item.into());
- if item_kind(scope, item).as_deref() == Some("file")
+ if let Some(view) = data_transfer_item_view(scope, item_list, item) {
+ let _ = item_list.set_index(scope, index, view.into());
+ }
+ if mode.can_read()
+ && item_kind(scope, item).as_deref() == Some("file")
&& let Some(file) = item_file_object(scope, item)
{
files.push(file);
@@ -1198,8 +1348,7 @@ fn data_transfer_drop_effect_setter<'s>(
return;
}
};
- if data_transfer_mode(scope, args.this()) != DataTransferMode::Disabled
- && !private_bool_property(scope, args.this(), DATA_TRANSFER_CLIPBOARD_SLOT).unwrap_or(false)
+ if !private_bool_property(scope, args.this(), DATA_TRANSFER_CLIPBOARD_SLOT).unwrap_or(false)
&& valid_drop_effect(&value)
{
set_private_string(scope, args.this(), DATA_TRANSFER_DROP_EFFECT_SLOT, &value);
@@ -1251,7 +1400,7 @@ fn data_transfer_item_kind_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
- let value = private_string_property(scope, args.this(), DATA_TRANSFER_ITEM_KIND_SLOT)
+ let value = item_kind(scope, args.this())
.and_then(|value| v8_string(scope, &value).map(Into::into))
.unwrap_or_else(|| v8str(scope, "").into());
rv.set(value);
@@ -1262,7 +1411,7 @@ fn data_transfer_item_type_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
- let value = private_string_property(scope, args.this(), DATA_TRANSFER_ITEM_TYPE_SLOT)
+ let value = item_type(scope, args.this())
.and_then(|value| v8_string(scope, &value).map(Into::into))
.unwrap_or_else(|| v8str(scope, "").into());
rv.set(value);
@@ -1340,6 +1489,10 @@ pub(crate) fn data_transfer_get_data_callback<'s>(
let Some(parsed) = webidl::parse_args::(scope, &args) else {
return;
};
+ if !data_transfer_mode(scope, args.this()).can_read() {
+ rv.set(v8str(scope, "").into());
+ return;
+ }
let normalized_type = normalize_drag_data_type(&parsed.format);
let Some(item_list) = get_private_object(scope, args.this(), DATA_TRANSFER_ITEMS_SLOT) else {
rv.set(v8str(scope, "").into());
@@ -1532,7 +1685,9 @@ pub(crate) fn data_transfer_item_get_as_file_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
- if let Some(file) = item_file_object(scope, args.this()) {
+ if item_is_readable(scope, args.this())
+ && let Some(file) = item_file_object(scope, args.this())
+ {
rv.set(file.into());
} else {
rv.set(v8::null(scope).into());
@@ -1544,6 +1699,10 @@ pub(crate) fn data_transfer_item_webkit_get_as_entry_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
+ if !item_is_readable(scope, args.this()) {
+ rv.set(v8::null(scope).into());
+ return;
+ }
if let Some(entry) = item_entry_object(scope, args.this()) {
rv.set(entry.into());
return;
diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer/drag_store.rs b/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer/drag_store.rs
new file mode 100644
index 0000000000..b8eb88dec2
--- /dev/null
+++ b/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer/drag_store.rs
@@ -0,0 +1,98 @@
+use super::*;
+
+/// The session's backing transfer is never exposed to script. Native events
+/// publish separate DataTransfer/item views with event-local access rights.
+pub(crate) struct DragDataStore {
+ backing: v8::Global,
+}
+
+impl DragDataStore {
+ pub(crate) fn new(scope: &mut v8::PinScope<'_, '_>, data: &RendererDragData) -> Option {
+ let backing = build_data_transfer_object(scope, data)?;
+ Some(Self {
+ backing: v8::Global::new(scope, backing),
+ })
+ }
+
+ pub(crate) fn backing_transfer<'s>(
+ &self,
+ scope: &mut v8::PinScope<'s, '_>,
+ ) -> v8::Local<'s, v8::Object> {
+ v8::Local::new(scope, &self.backing)
+ }
+
+ pub(crate) fn open_event<'s>(
+ &self,
+ scope: &mut v8::PinScope<'s, '_>,
+ event_name: &str,
+ ) -> Option> {
+ let backing = self.backing_transfer(scope);
+ let transfer = DataTransferShellDeclaration::default().bind(scope).ok()?;
+ let items = initialize_data_transfer_object(scope, transfer)?;
+ let backing_items = DataTransferItemStore::for_owner(scope, backing)?;
+ let array = item_list_array(scope, backing_items.item_list)?;
+ set_private_value(
+ scope,
+ items,
+ DATA_TRANSFER_ITEM_LIST_ARRAY_SLOT,
+ array.into(),
+ );
+ set_private_value(scope, transfer, DATA_TRANSFER_STORE_SLOT, backing.into());
+ let mode = match event_name {
+ "dragstart" => DataTransferMode::ReadWrite,
+ "drop" => DataTransferMode::ReadOnly,
+ _ => DataTransferMode::Protected,
+ };
+ set_private_number(
+ scope,
+ transfer,
+ DATA_TRANSFER_MODE_SLOT,
+ f64::from(mode as u8),
+ );
+ for slot in [
+ DATA_TRANSFER_DROP_EFFECT_SLOT,
+ DATA_TRANSFER_EFFECT_ALLOWED_SLOT,
+ ] {
+ if let Some(value) = get_private_value(scope, backing, slot) {
+ set_private_value(scope, transfer, slot, value);
+ }
+ }
+ data_transfer_item_list_did_change(scope, items);
+ Some(DragDataTransferEvent { backing, transfer })
+ }
+}
+
+pub(crate) struct DragDataTransferEvent<'s> {
+ backing: v8::Local<'s, v8::Object>,
+ transfer: v8::Local<'s, v8::Object>,
+}
+
+impl<'s> DragDataTransferEvent<'s> {
+ pub(crate) fn object(&self) -> v8::Local<'s, v8::Object> {
+ self.transfer
+ }
+
+ /// Preserve the negotiated effects before invalidating every live view.
+ /// Payloads stay with the backing store for later events and UA defaults.
+ pub(crate) fn finish(self, scope: &mut v8::PinScope<'s, '_>) {
+ for slot in [
+ DATA_TRANSFER_DROP_EFFECT_SLOT,
+ DATA_TRANSFER_EFFECT_ALLOWED_SLOT,
+ ] {
+ if let Some(value) = get_private_value(scope, self.transfer, slot) {
+ set_private_value(scope, self.backing, slot, value);
+ }
+ }
+ set_private_number(
+ scope,
+ self.transfer,
+ DATA_TRANSFER_MODE_SLOT,
+ f64::from(DataTransferMode::Disabled as u8),
+ );
+ let null = v8::null(scope);
+ set_private_value(scope, self.transfer, DATA_TRANSFER_STORE_SLOT, null.into());
+ if let Some(items) = DataTransferItemStore::for_owner(scope, self.transfer) {
+ data_transfer_item_list_did_change(scope, items.item_list);
+ }
+ }
+}
diff --git a/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer_string.rs b/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer_string.rs
index 5f29c14cc4..a4d293ee77 100644
--- a/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer_string.rs
+++ b/moli-renderer-v8/src/context_bootstrap/file_api/data_transfer_string.rs
@@ -20,7 +20,7 @@ use crate::{
};
use moli_webidl_callback::WebIdlCallbackFunction;
-use super::data_transfer::{item_kind, item_string_value};
+use super::data_transfer::{item_is_readable, item_kind, item_string_value};
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "DataTransferItem.getAsString")]
@@ -112,7 +112,9 @@ pub(crate) fn data_transfer_item_get_as_string_callback<'s>(
let Some(callback) = parsed.callback else {
return;
};
- if item_kind(scope, args.this()).as_deref() != Some("string") {
+ if !item_is_readable(scope, args.this())
+ || item_kind(scope, args.this()).as_deref() != Some("string")
+ {
return;
}
let Some(value) = item_string_value(scope, args.this()) else {
diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs
index 9b3ccf6ac6..f44d07154d 100644
--- a/moli-renderer-v8/src/native_bridge/element.rs
+++ b/moli-renderer-v8/src/native_bridge/element.rs
@@ -285,8 +285,8 @@ pub(crate) use events::construct_focus_event;
pub(crate) use events::{
NodePublicEventDispatchOutcome, TextEditInputType, TouchEventPoint,
construct_activation_pointer_event, construct_clipboard_event, construct_command_event,
- construct_drag_event, construct_drag_event_with_related_target, construct_form_data_event,
- construct_input_event, construct_interest_event, construct_keyboard_event,
+ construct_drag_event_with_related_target, construct_form_data_event, construct_input_event,
+ construct_interest_event, construct_keyboard_event,
construct_mouse_event_with_detail_and_modifiers, construct_mouse_event_with_modifiers,
construct_mouse_event_with_related_target_and_modifiers, construct_pointer_event,
construct_pointer_event_with_modifiers, construct_pointer_event_with_related_target,
diff --git a/moli-renderer-v8/src/native_bridge/element/events.rs b/moli-renderer-v8/src/native_bridge/element/events.rs
index 7fb351700d..5c6d6c2d68 100644
--- a/moli-renderer-v8/src/native_bridge/element/events.rs
+++ b/moli-renderer-v8/src/native_bridge/element/events.rs
@@ -12,12 +12,12 @@ pub(super) use self::constructors::construct_click_event;
pub(crate) use self::constructors::construct_focus_event;
pub(crate) use self::constructors::{
TextEditInputType, TouchEventPoint, construct_activation_pointer_event,
- construct_clipboard_event, construct_command_event, construct_drag_event,
- construct_drag_event_with_related_target, construct_form_data_event, construct_input_event,
- construct_interest_event, construct_keyboard_event,
- construct_mouse_event_with_detail_and_modifiers, construct_mouse_event_with_modifiers,
- construct_mouse_event_with_related_target_and_modifiers, construct_pointer_event,
- construct_pointer_event_with_modifiers, construct_pointer_event_with_related_target,
+ construct_clipboard_event, construct_command_event, construct_drag_event_with_related_target,
+ construct_form_data_event, construct_input_event, construct_interest_event,
+ construct_keyboard_event, construct_mouse_event_with_detail_and_modifiers,
+ construct_mouse_event_with_modifiers, construct_mouse_event_with_related_target_and_modifiers,
+ construct_pointer_event, construct_pointer_event_with_modifiers,
+ construct_pointer_event_with_related_target,
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
construct_simple_event_for_target, construct_submit_event, construct_toggle_event,
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
diff --git a/moli-renderer-v8/src/native_bridge/element/events/constructors.rs b/moli-renderer-v8/src/native_bridge/element/events/constructors.rs
index 2adf6d95a3..292623266c 100644
--- a/moli-renderer-v8/src/native_bridge/element/events/constructors.rs
+++ b/moli-renderer-v8/src/native_bridge/element/events/constructors.rs
@@ -698,27 +698,6 @@ pub(crate) fn construct_pointer_event_with_related_target_and_modifiers<'s>(
construct_event(scope, "PointerEvent", event_type, init)
}
-pub(crate) fn construct_drag_event<'s>(
- scope: &mut v8::PinScope<'s, '_>,
- event_type: &str,
- x: f64,
- y: f64,
- buttons: i32,
- data_transfer: v8::Local<'s, v8::Value>,
- modifiers: u8,
-) -> Option> {
- construct_drag_event_with_related_target(
- scope,
- event_type,
- x,
- y,
- buttons,
- data_transfer,
- modifiers,
- None,
- )
-}
-
pub(crate) fn construct_drag_event_with_related_target<'s>(
scope: &mut v8::PinScope<'s, '_>,
event_type: &str,
diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs
index 284a15cae1..289a5f44e6 100644
--- a/moli-renderer-v8/src/script_vm.rs
+++ b/moli-renderer-v8/src/script_vm.rs
@@ -1185,7 +1185,7 @@ pub(super) struct ActiveTouchPoint {
}
pub(super) struct ActiveDragSession {
- pub data_transfer: v8::Global,
+ pub data_store: crate::context_bootstrap::DragDataStore,
pub drop_allowed: bool,
native: Option,
target: Option,
diff --git a/moli-renderer-v8/src/script_vm/input_dispatch.rs b/moli-renderer-v8/src/script_vm/input_dispatch.rs
index 6d86dc4fe6..1e3aeadf7c 100644
--- a/moli-renderer-v8/src/script_vm/input_dispatch.rs
+++ b/moli-renderer-v8/src/script_vm/input_dispatch.rs
@@ -22,10 +22,10 @@ use crate::native_bridge::element::{
activate_handle_after_pointer_release, activate_handle_via_click,
activate_handle_via_click_with_detail_and_modifiers, cache_input_files_from_selected_files,
captured_frame_input_surface_hit_test, construct_activation_pointer_event,
- construct_drag_event, construct_keyboard_event,
- construct_mouse_event_with_detail_and_modifiers, construct_mouse_event_with_modifiers,
- construct_mouse_event_with_related_target_and_modifiers, construct_pointer_event,
- construct_pointer_event_with_modifiers, construct_pointer_event_with_related_target,
+ construct_keyboard_event, construct_mouse_event_with_detail_and_modifiers,
+ construct_mouse_event_with_modifiers, construct_mouse_event_with_related_target_and_modifiers,
+ construct_pointer_event, construct_pointer_event_with_modifiers,
+ construct_pointer_event_with_related_target,
construct_pointer_event_with_related_target_and_modifiers, construct_simple_event,
construct_touch_event, construct_touch_event_with_points, construct_wheel_event,
contenteditable_editing_host, dispatch_public_event, drag_start_frame_input_surface_hit_test,
@@ -1220,26 +1220,24 @@ impl ScriptVm {
directories: Vec::new(),
drag_operations_mask: 1,
};
- if let Some(data_transfer) =
- crate::context_bootstrap::build_data_transfer_object(scope, &empty_drag_data)
+ if let Some(data_store) =
+ crate::context_bootstrap::DragDataStore::new(scope, &empty_drag_data)
{
- crate::context_bootstrap::initialize_native_drag_data_transfer(
- scope,
- data_transfer,
- );
- if let Some(event) = construct_drag_event(
+ let backing = data_store.backing_transfer(scope);
+ crate::context_bootstrap::initialize_native_drag_data_transfer(scope, backing);
+ if super::input_drag::fire_drag_event(
scope,
+ runtime_ptr,
+ drag_start_handle,
"dragstart",
- f64::from(drag_position.x),
- f64::from(drag_position.y),
+ drag_position,
buttons,
- data_transfer.into(),
modifiers,
- ) && dispatch_public_event(scope, runtime_ptr, drag_start_handle, event)
- .allows_default()
- {
+ &data_store,
+ None,
+ ) {
*active_drag_session = Some(ActiveDragSession {
- data_transfer: v8::Global::new(scope, data_transfer),
+ data_store,
drop_allowed: false,
native: Some(super::input_drag::NativeDragSource {
handle: drag_start_handle,
@@ -2009,16 +2007,19 @@ impl ScriptVm {
data: RendererDragData,
modifiers: u8,
) -> Result {
- let Some(handle) = observable_input_hit_test(
+ let position = moli_layout::LayoutPoint::new(x as f32, y as f32);
+ let Some(hit) = observable_input_hit_test(
&self._context_host.borrow(),
self.document_runtime.document_handle(),
- moli_layout::LayoutPoint::new(x as f32, y as f32),
+ position,
)?
- .map(|hit| hit.handle) else {
+ else {
return Ok(input_dispatch_outcome(false));
};
+ let handle = hit.handle;
- let incoming_drag_data_empty = data.items.is_empty() && data.files.is_empty();
+ let incoming_drag_data_empty =
+ data.items.is_empty() && data.files.is_empty() && data.directories.is_empty();
let replace_active_drag_data_transfer = event_name == "dragenter"
&& (!incoming_drag_data_empty || self.active_drag_session.is_none());
let clear_active_drag_data_transfer = event_name == "drop";
@@ -2030,48 +2031,38 @@ impl ScriptVm {
// remains exclusively borrowed, so the field pointer stays valid for the
// duration of this callback and is not aliased elsewhere.
let active_drag_session = unsafe { &mut *active_drag_session };
- let data_transfer =
- if replace_active_drag_data_transfer || active_drag_session.is_none() {
- let Some(data_transfer) =
- crate::context_bootstrap::build_data_transfer_object(scope, &data)
- else {
- return Ok(input_dispatch_outcome(false));
- };
- *active_drag_session = Some(ActiveDragSession {
- data_transfer: v8::Global::new(scope, data_transfer),
- drop_allowed: false,
- native: None,
- target: None,
- });
- data_transfer
- } else {
- v8::Local::new(
- scope,
- &active_drag_session
- .as_ref()
- .expect("active drag session should exist when reusing DataTransfer")
- .data_transfer,
- )
+ if replace_active_drag_data_transfer || active_drag_session.is_none() {
+ let Some(data_store) = crate::context_bootstrap::DragDataStore::new(scope, &data)
+ else {
+ return Ok(input_dispatch_outcome(false));
};
+ *active_drag_session = Some(ActiveDragSession {
+ data_store,
+ drop_allowed: false,
+ native: None,
+ target: None,
+ });
+ }
+ let session = active_drag_session
+ .as_ref()
+ .expect("active drag session exists");
+ let data_transfer = session.data_store.backing_transfer(scope);
crate::context_bootstrap::apply_drag_modifier_drop_effect(
scope,
data_transfer,
modifiers,
);
- let data_transfer_value: v8::Local<'_, v8::Value> = data_transfer.into();
- let allows_default = if let Some(event) = construct_drag_event(
+ let allows_default = super::input_drag::fire_drag_event(
scope,
+ runtime_ptr,
+ handle,
event_name,
- x,
- y,
+ hit.root_to_frame.map_point(position),
buttons,
- data_transfer_value,
modifiers,
- ) {
- dispatch_public_event(scope, runtime_ptr, handle, event).allows_default()
- } else {
- false
- };
+ &session.data_store,
+ None,
+ );
if event_name == "dragover"
&& let Some(session) = active_drag_session.as_mut()
{
diff --git a/moli-renderer-v8/src/script_vm/input_drag.rs b/moli-renderer-v8/src/script_vm/input_drag.rs
index 10098dc41c..b534dcbcce 100644
--- a/moli-renderer-v8/src/script_vm/input_drag.rs
+++ b/moli-renderer-v8/src/script_vm/input_drag.rs
@@ -6,7 +6,7 @@ use super::input_dispatch::{
};
use super::{ActiveDragSession, ScriptVm, input_dispatch_outcome};
use crate::context_bootstrap::{
- allowed_drag_drop_effect, prepare_drag_drop_effect, set_drag_drop_effect,
+ DragDataStore, allowed_drag_drop_effect, prepare_drag_drop_effect, set_drag_drop_effect,
};
use crate::document_runtime::DomHandle;
use crate::native_bridge::JsContextHost;
@@ -77,27 +77,67 @@ fn dispatch_drag(
position: LayoutPoint,
buttons: i32,
modifiers: u8,
- transfer: &v8::Global,
+ store: &DragDataStore,
related: Option,
) -> bool {
- let transfer = v8::Local::new(scope, transfer);
+ let point = target.root_to_frame.map_point(position);
+ fire_drag_event(
+ scope,
+ runtime,
+ target.handle,
+ event_name,
+ point,
+ buttons,
+ modifiers,
+ store,
+ related,
+ )
+}
+
+/// A native drag event owns one temporary view of the session's data store.
+/// Construct it in the target Document's realm, then retire it after dispatch.
+pub(super) fn fire_drag_event(
+ scope: &mut v8::PinScope<'_, '_>,
+ runtime: *mut JsContextHost,
+ target: DomHandle,
+ event_name: &str,
+ position: LayoutPoint,
+ buttons: i32,
+ modifiers: u8,
+ store: &DragDataStore,
+ related: Option,
+) -> bool {
+ let host = unsafe { &mut *runtime };
+ let Some(context) = host
+ .owner_dispatch_scope_for_node(target)
+ .and_then(|target| {
+ let owner = host.current_window_execution_context_owner(target)?;
+ host.window_execution_context(scope, owner, target)
+ .map(|(_, context)| context)
+ })
+ else {
+ return false;
+ };
+ let scope = &mut v8::ContextScope::new(scope, context);
+ let Some(view) = store.open_event(scope, event_name) else {
+ return false;
+ };
let related = related
.and_then(|handle| node_wrapper_from_handle(scope, handle))
.map(Into::into);
- let point = target.root_to_frame.map_point(position);
- construct_drag_event_with_related_target(
+ let allows_default = construct_drag_event_with_related_target(
scope,
event_name,
- f64::from(point.x),
- f64::from(point.y),
+ f64::from(position.x),
+ f64::from(position.y),
buttons,
- transfer.into(),
+ view.object().into(),
modifiers,
related,
)
- .is_some_and(|event| {
- dispatch_public_event(scope, runtime, target.handle, event).allows_default()
- })
+ .is_some_and(|event| dispatch_public_event(scope, runtime, target, event).allows_default());
+ view.finish(scope);
+ allows_default
}
fn end_native_drag(
@@ -112,7 +152,7 @@ fn end_native_drag(
let Some(source) = session.native else {
return;
};
- let transfer = v8::Local::new(scope, &session.data_transfer);
+ let transfer = session.data_store.backing_transfer(scope);
let mut effect = if allow_drop && session.drop_allowed {
allowed_drag_drop_effect(scope, transfer)
} else {
@@ -129,7 +169,7 @@ fn end_native_drag(
position,
buttons,
modifiers,
- &session.data_transfer,
+ &session.data_store,
None,
);
if allows_default && !perform_drop_default_action(scope, runtime, target.handle, transfer) {
@@ -147,7 +187,7 @@ fn end_native_drag(
position,
buttons,
modifiers,
- &session.data_transfer,
+ &session.data_store,
None,
);
}
@@ -163,7 +203,7 @@ fn end_native_drag(
position,
0,
modifiers,
- &session.data_transfer,
+ &session.data_store,
None,
);
}
@@ -224,7 +264,7 @@ impl ScriptVm {
);
finished = true;
} else if event_name == "mousemove" {
- let transfer = v8::Local::new(scope, &session.data_transfer);
+ let transfer = session.data_store.backing_transfer(scope);
set_drag_drop_effect(scope, transfer, "none");
if !dispatch_drag(
scope,
@@ -237,7 +277,7 @@ impl ScriptVm {
position,
buttons,
modifiers,
- &session.data_transfer,
+ &session.data_store,
None,
) {
end_native_drag(scope, runtime, &session, position, 0, modifiers, false);
@@ -257,7 +297,7 @@ impl ScriptVm {
position,
buttons,
modifiers,
- &session.data_transfer,
+ &session.data_store,
previous.map(|h| h.handle),
);
}
@@ -271,7 +311,7 @@ impl ScriptVm {
position,
buttons,
modifiers,
- &session.data_transfer,
+ &session.data_store,
hit.map(|h| h.handle),
);
}
@@ -287,7 +327,7 @@ impl ScriptVm {
position,
buttons,
modifiers,
- &session.data_transfer,
+ &session.data_store,
None,
) && allowed_drag_drop_effect(scope, transfer) != "none"
} else {
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/drag_data_modes.js b/moli-renderer-v8/src/script_vm/tests/browser_api/drag_data_modes.js
new file mode 100644
index 0000000000..05caa6b58e
--- /dev/null
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/drag_data_modes.js
@@ -0,0 +1,123 @@
+globalThis.__installDragDataModeProbe = function(mode, capture, modifiers) {
+ document.body.innerHTML = 'source
A
B
';
+ const source = document.getElementById('source');
+ const file = new File(['private-file-contents'], 'private.txt', {type:'text/plain'});
+ const rows = [], transfers = [], callbacks = [], microtasks = [], trace = [];
+ const expectedTypes = ['text/plain', 'text/html', 'application/test', 'Files'];
+ const checks = [];
+ const check = (name, actual, expected) => checks.push({name, actual, expected, pass:JSON.stringify(actual)===JSON.stringify(expected)});
+ const exceptionName = action => { try { action(); return 'none'; } catch (error) { return error.name; } };
+ let starts = 0, drops = 0;
+ function retain(dt, event) {
+ const row = {dt, event, items:dt.items, files:dt.files, types:dt.types, string:dt.items[0], file:dt.items[3]};
+ transfers.push(row);
+ return row;
+ }
+ function disabled(row, label) {
+ const prefix = label + ':' + row.event + ':' + transfers.indexOf(row);
+ const dt = row.dt;
+ check(prefix+':empty getData', dt.getData('text/plain'), '');
+ check(prefix+':empty types', Array.from(dt.types), []);
+ check(prefix+':new frozen types snapshot', dt.types !== row.types && Object.isFrozen(dt.types), true);
+ check(prefix+':saved types unchanged', Array.from(row.types), expectedTypes);
+ check(prefix+':items same object', dt.items === row.items, true);
+ check(prefix+':items empty', row.items.length, 0);
+ check(prefix+':indexed item absent', row.items[0] === undefined, true);
+ check(prefix+':files same object', dt.files === row.files, true);
+ check(prefix+':saved files empty', row.files.length, 0);
+ if (row.string) {
+ check(prefix+':saved string metadata disabled', [row.string.kind,row.string.type], ['','']);
+ row.string.getAsString(value => callbacks.push({phase:'disabled',value}));
+ }
+ if (row.file) {
+ check(prefix+':saved file metadata disabled', [row.file.kind,row.file.type], ['','']);
+ check(prefix+':saved file unavailable', row.file.getAsFile(), null);
+ check(prefix+':saved entry unavailable', row.file.webkitGetAsEntry(), null);
+ }
+ check(prefix+':add blocked', row.items.add('no','application/blocked'), null);
+ check(prefix+':remove disabled throws', exceptionName(()=>row.items.remove(999)), 'InvalidStateError');
+ const allowed = dt.effectAllowed;
+ dt.effectAllowed = 'none';
+ check(prefix+':effectAllowed immutable', dt.effectAllowed, allowed);
+ dt.setData('text/plain','escaped');
+ dt.clearData();
+ row.items.clear();
+ check(prefix+':store cannot be repopulated', [dt.getData('text/plain'),row.items.length,dt.types.length], ['',0,0]);
+ }
+ for (const type of ['dragstart','drag','dragenter','dragover','dragleave','drop','dragend']) {
+ document.addEventListener(type, event => {
+ const dt = event.dataTransfer;
+ const prefix = type+':'+rows.length;
+ check(prefix+':fresh event DataTransfer', transfers.every(row=>row.dt!==dt), true);
+ for (const row of transfers) disabled(row, prefix+':previous');
+ if (type==='dragstart') {
+ starts++;
+ dt.setData('text/plain','payload');
+ dt.setData('text/html','payload');
+ dt.items.add('extra','application/test');
+ const added = dt.items.add(file);
+ check(prefix+':added file is indexed view', added===dt.items[3], true);
+ dt.effectAllowed='all';
+ }
+ const row = retain(dt,type);
+ check(prefix+':same items object', dt.items===dt.items, true);
+ check(prefix+':same files object', dt.files===dt.files, true);
+ check(prefix+':stable frozen types', dt.types===row.types && Object.isFrozen(row.types), true);
+ check(prefix+':metadata enumerable', Array.from(dt.types), expectedTypes);
+ check(prefix+':indexed metadata', Array.from(dt.items, item=>[item.kind,item.type]), [['string','text/plain'],['string','text/html'],['string','application/test'],['file','text/plain']]);
+ const readable = type==='dragstart' || type==='drop';
+ check(prefix+':payload read mode', dt.getData('text/plain'), readable?'payload':'');
+ check(prefix+':file list read mode', dt.files.length, readable?1:0);
+ if (row.file) {
+ check(prefix+':file item read mode', readable?row.file.getAsFile()===file:row.file.getAsFile()===null, true);
+ check(prefix+':entry read mode', !!row.file.webkitGetAsEntry(), readable);
+ }
+ if (row.string) {
+ row.string.getAsString(value=>callbacks.push({phase:type,value}));
+ check(prefix+':callback conversion before mode', exceptionName(()=>row.string.getAsString({})), 'TypeError');
+ }
+ let conversions=0;
+ check(prefix+':format conversion still runs', dt.getData({toString(){conversions++;return 'text/plain'}}), readable?'payload':'');
+ check(prefix+':format conversion count', conversions, 1);
+ const marker={};
+ let caught;
+ try { dt.getData({toString(){throw marker;}}); } catch (error) {caught=error;}
+ check(prefix+':format conversion exception propagates', caught===marker, true);
+ Promise.resolve().then(()=>microtasks.push({phase:type,data:dt.getData('text/plain'),items:dt.items.length}));
+ if (type!=='dragstart') {
+ const before=Array.from(dt.types), allowed=dt.effectAllowed;
+ dt.setData('text/plain','tampered');
+ dt.clearData('text/html');
+ check(prefix+':string add blocked', dt.items.add('no','application/blocked'), null);
+ check(prefix+':file add blocked', dt.items.add(file), null);
+ check(prefix+':remove readonly/protected throws', exceptionName(()=>dt.items.remove(999)), 'InvalidStateError');
+ dt.items.clear();
+ dt.effectAllowed='none';
+ check(prefix+':readonly/protected mutations ignored', Array.from(dt.types), before);
+ check(prefix+':effectAllowed cannot change', dt.effectAllowed, allowed);
+ check(prefix+':data remains unmodified', dt.getData('text/plain'), readable?'payload':'');
+ }
+ if (type==='dragenter' || type==='dragover') {
+ dt.dropEffect='copy';
+ check(prefix+':dropEffect writable', dt.dropEffect, 'copy');
+ if (mode==='drop') event.preventDefault();
+ }
+ if (type==='drop') {drops++;event.preventDefault();}
+ if (type==='dragstart' && mode==='cancel') event.preventDefault();
+ rows.push({entryAvailable:!!row.file?.webkitGetAsEntry(),type,data:dt.getData('text/plain'),items:dt.items.length,files:dt.files.length,types:Array.from(dt.types),dropEffect:dt.dropEffect,effectAllowed:dt.effectAllowed});
+ trace.push(type);
+ });
+ }
+ if (capture) source.addEventListener('pointerdown', event=>source.setPointerCapture(event.pointerId));
+ return {
+ afterDispatch(label) {for(const row of transfers) disabled(row,label);},
+ finish() {
+ for(const row of transfers) disabled(row,'finish');
+ check('one native dragstart', starts, 1);
+ check('drop iff accepted', drops, mode==='drop'?1:0);
+ check('only readable events schedule string callbacks', callbacks.filter(row=>!['dragstart','drop'].includes(row.phase)), []);
+ check('captured callback payload survives disassociation', callbacks.filter(row=>['dragstart','drop'].includes(row.phase)).map(row=>row.value), mode==='drop'?['payload','payload']:['payload']);
+ return {mode,capture,modifiers,checks,rows,callbacks,microtasks,trace,passed:checks.filter(c=>c.pass).length,total:checks.length,complete:checks.every(c=>c.pass)};
+ }
+ };
+};
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/drag_data_modes.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/drag_data_modes.rs
new file mode 100644
index 0000000000..d754bd16f8
--- /dev/null
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/drag_data_modes.rs
@@ -0,0 +1,234 @@
+use super::*;
+
+fn mode_input(vm: &mut ScriptVm, pointer_type: &str, input: (&str, f64, i32, u8)) {
+ let (event, x, buttons, modifiers) = input;
+ vm.dispatch_mouse_event_at_point_with_pointer_and_modifiers(
+ x,
+ 80.0,
+ event,
+ if event == "mousemove" { -1 } else { 0 },
+ Some(buttons),
+ i32::from(event != "mousemove"),
+ 0.0,
+ 0.0,
+ crate::runtime::RendererPointerEventProperties {
+ pointer_id: 17,
+ pointer_type: pointer_type.to_owned(),
+ pressure: if buttons == 0 { 0.0 } else { 0.5 },
+ ..Default::default()
+ },
+ modifiers,
+ )
+ .unwrap();
+}
+
+#[tokio::test]
+async fn native_drag_data_modes_invalidate_views_and_preserve_queued_strings() {
+ let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
+ for pointer in ["mouse", "pen"] {
+ for capture in [false, true] {
+ for mode in ["cancel", "drop", "reject", "escape"] {
+ for modifiers in [0, 8, 15] {
+ let mut vm = new_storage_page_task_executor_test_vm_with_loader(
+ "https://drag-data-modes.test/",
+ &loader,
+ );
+ vm.eval("if (!document.documentElement) document.appendChild(document.createElement('html')); if (!document.body) document.documentElement.appendChild(document.createElement('body')); 'ready'").unwrap();
+ vm.eval(&format!("{}; globalThis.__dragResults=__installDragDataModeProbe('{mode}',{capture},{modifiers}); 'ready'", include_str!("drag_data_modes.js"))).unwrap();
+ vm.publish_layout_for_test().unwrap();
+ for input in [
+ ("mousemove", 80.0, 0, modifiers),
+ ("mousedown", 80.0, 1, modifiers),
+ ("mousemove", 100.0, 1, modifiers),
+ ] {
+ mode_input(&mut vm, pointer, input);
+ }
+ if mode == "escape" {
+ vm.dispatch_key_event("keydown", "Escape", "Escape", "", 0, false, false)
+ .unwrap();
+ } else {
+ for x in [320.0, 520.0] {
+ mode_input(&mut vm, pointer, ("mousemove", x, 1, modifiers ^ 8));
+ }
+ }
+ vm.eval("__dragResults.afterDispatch('before-release'); 'ready'")
+ .unwrap();
+ mode_input(
+ &mut vm,
+ pointer,
+ (
+ "mouseup",
+ if mode == "escape" { 100.0 } else { 520.0 },
+ 0,
+ modifiers ^ 8,
+ ),
+ );
+ vm.eval("__dragResults.afterDispatch('after-release'); 'ready'")
+ .unwrap();
+ let mut tasks = 0;
+ while vm
+ .run_one_user_interaction_executor_turn(&loader)
+ .await
+ .unwrap()
+ {
+ tasks += 1;
+ assert!(
+ tasks <= 2,
+ "protected/disabled items must not schedule callbacks"
+ );
+ }
+ assert_eq!(tasks, if mode == "drop" { 2 } else { 1 });
+ assert_eq!(vm.eval("globalThis.__dragFinished=__dragResults.finish(); __dragFinished.complete").unwrap(), "true", "{pointer}, {mode}, capture {capture}, modifiers {modifiers}: {}", vm.eval("JSON.stringify(__dragFinished.checks.filter(c=>!c.pass))").unwrap());
+ assert_eq!(vm.eval("__dragFinished.microtasks.length===__dragFinished.rows.length && __dragFinished.microtasks.every(row=>row.items===4 && row.data===(['dragstart','drop'].includes(row.phase)?'payload':''))").unwrap(), "true", "listener cleanup must run while the event view is still associated");
+ }
+ }
+ }
+ }
+}
+
+#[test]
+fn constructed_transfer_remains_writable_after_synthetic_event_dispatch() {
+ let mut vm = new_storage_test_vm("https://synthetic-transfer.test/");
+ assert_eq!(
+ vm.eval(
+ r#"
+ globalThis.dt=new DataTransfer(); const item=dt.items.add('before','text/plain');
+ document.dispatchEvent(new DragEvent('drop',{dataTransfer:dt}));
+ dt.setData('text/plain','after');
+ JSON.stringify([dt.getData('text/plain'),dt.items[0]===item,item.kind,dt.items.length])
+ "#
+ )
+ .unwrap(),
+ r#"["after",true,"string",1]"#
+ );
+}
+
+#[test]
+fn transfer_receiver_validation_precedes_argument_conversion() {
+ let mut vm = new_storage_test_vm("https://transfer-receiver.test/");
+ assert_eq!(vm.eval(r#"
+ const dt=new DataTransfer(); dt.setData('text/plain','value');
+ const classes=[DataTransfer,DataTransferItemList,DataTransferItem];
+ const objects=[dt,dt.items,dt.items[0]];
+ let conversions=0; const format={toString(){conversions++; return 'text/plain';}};
+ const failures=[];
+ for(let i=0;iproto.getData.call(receiver,format):i===1?()=>proto.remove.call(receiver,{valueOf(){conversions++;return 0;}}):()=>proto.getAsString.call(receiver,{});
+ try {method(); failures.push(false);} catch(e) {failures.push(e instanceof TypeError);}
+ }
+ }
+ JSON.stringify([failures.every(Boolean),conversions])
+ "#).unwrap(), "[true,0]");
+}
+
+#[test]
+fn external_drag_views_hide_payload_until_drop_and_keep_text_default_action() {
+ use crate::runtime::{RendererDragData, RendererDragDataItem};
+ let mut vm = new_rendered_test_vm(
+ "https://external-drag.test/",
+ "",
+ );
+ vm.eval(r#"
+ globalThis.transfers=[]; globalThis.observations=[];
+ for (const type of ['dragenter','dragover','drop']) target.addEventListener(type,event=>{
+ const dt=event.dataTransfer;
+ observations.push([type,dt.getData('text/plain'),dt.items.length,Array.from(dt.types)]);
+ dt.setData('text/plain','tampered'); dt.clearData();
+ observations.push([dt.getData('text/plain'),transfers.every(old=>old.items.length===0 && old.getData('text/plain')==='')]);
+ transfers.push(dt);
+ }); 'ready'
+ "#).unwrap();
+ let data = RendererDragData {
+ items: vec![RendererDragDataItem {
+ mime_type: "text/plain".to_owned(),
+ data: "external".to_owned(),
+ title: None,
+ base_url: None,
+ }],
+ files: vec![],
+ directories: vec![],
+ drag_operations_mask: 1,
+ };
+ for event in ["dragenter", "dragover", "drop"] {
+ vm.dispatch_drag_event_at_point(10.0, 10.0, event, data.clone(), 0)
+ .unwrap();
+ }
+ assert_eq!(vm.eval("JSON.stringify([observations,target.value,transfers.every(dt=>dt.items.length===0 && dt.types.length===0 && dt.getData('text/plain')==='')])").unwrap(), r#"[[["dragenter","",1,["text/plain"]],["",true],["dragover","",1,["text/plain"]],["",true],["drop","external",1,["text/plain"]],["external",true]],"external",true]"#);
+}
+
+#[test]
+fn external_file_drop_uses_backing_store_after_event_view_is_disabled() {
+ use crate::runtime::{RendererDragData, RendererDraggedFile};
+ let mut vm = new_rendered_test_vm(
+ "https://external-file-drag.test/",
+ "",
+ );
+ vm.eval(r#"
+ globalThis.saved=null;globalThis.payload=null;globalThis.events=[];
+ target.addEventListener('drop',event=>{
+ saved=event.dataTransfer; payload=saved.items[0];
+ events.push([saved.files.length,payload.getAsFile()===saved.files[0],payload.getAsFile().name]);
+ saved.items.clear(); saved.setData('text/plain','tampered');
+ });
+ target.addEventListener('input',()=>events.push(['input',saved.files.length,saved.items.length]));
+ 'ready'
+ "#).unwrap();
+ let data = RendererDragData {
+ items: vec![],
+ files: vec![RendererDraggedFile {
+ bytes: b"contents".to_vec(),
+ mime_type: "text/plain".to_owned(),
+ name: "upload.txt".to_owned(),
+ last_modified: 7.0,
+ }],
+ directories: vec![],
+ drag_operations_mask: 1,
+ };
+ vm.dispatch_drag_event_at_point(10.0, 10.0, "drop", data, 0)
+ .unwrap();
+ assert_eq!(vm.eval("JSON.stringify([events,target.files[0].name,target.files[0].size,saved.files.length,payload.kind,payload.getAsFile()])").unwrap(),r#"[[[1,true,"upload.txt"],["input",0,0]],"upload.txt",8,0,"",null]"#);
+}
+
+#[test]
+fn native_drag_event_views_use_each_target_documents_realm() {
+ for origin in ["parent", "child"] {
+ let mut vm = new_rendered_test_vm(
+ "https://drag-realms.test/",
+ "",
+ );
+ vm.eval(&format!(r#"
+ globalThis.child=frame.contentWindow;
+ child.document.body.innerHTML='';
+ const source={source};source.draggable=true;
+ globalThis.rows=[];globalThis.saved=[];
+ for(const [win,name] of [[window,'parent'],[child,'child']]) {{
+ for(const type of ['dragstart','dragenter','dragover','drop','dragend']) win.document.addEventListener(type,event=>{{
+ const dt=event.dataTransfer;
+ if(type==='dragstart') {{dt.setData('text/plain','realm-payload');dt.effectAllowed='copy';}}
+ rows.push([name,type,event instanceof win.DragEvent,dt instanceof win.DataTransfer,dt.items instanceof win.DataTransferItemList,dt.items[0] instanceof win.DataTransferItem,event.view===win,saved.every(old=>old.items.length===0 && old.types.length===0),dt.getData('text/plain')]);
+ saved.push(dt);
+ if(type==='dragenter'||type==='dragover') {{dt.dropEffect='copy';event.preventDefault();}}
+ if(type==='drop') event.preventDefault();
+ }});
+ }} 'ready'
+ "#,source=if origin=="parent" {"outside"} else {"child.document.getElementById('inside')"})).unwrap();
+ vm.publish_layout_for_test().unwrap();
+ let (start, end) = if origin == "parent" {
+ (80.0, 320.0)
+ } else {
+ (320.0, 80.0)
+ };
+ for input in [
+ ("mousemove", start, 0, 0),
+ ("mousedown", start, 1, 0),
+ ("mousemove", start + 20.0, 1, 0),
+ ("mousemove", end, 1, 0),
+ ("mouseup", end, 0, 0),
+ ] {
+ mode_input(&mut vm, "mouse", input);
+ }
+ assert_eq!(vm.eval("rows.every(row=>row.slice(2,8).every(Boolean) && row[8]===(['dragstart','drop'].includes(row[1])?'realm-payload':'')) && rows.filter(row=>row[1]==='drop').length===1 && saved.every(dt=>dt.items.length===0)").unwrap(),"true","{origin}: {}",vm.eval("JSON.stringify(rows)").unwrap());
+ }
+}
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs
index 291d7858d1..ba6797d2f3 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs
@@ -26,6 +26,7 @@ mod document_domain_setter;
mod document_open_navigation_abort;
mod document_open_origin;
mod dom_event_worlds;
+mod drag_data_modes;
mod drag_init;
mod drag_lifecycle;
mod error_event_init;
diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/native_event_construction.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/native_event_construction.rs
index 1e34a39a64..d8d1ffa5ea 100644
--- a/moli-renderer-v8/src/script_vm/tests/browser_api/native_event_construction.rs
+++ b/moli-renderer-v8/src/script_vm/tests/browser_api/native_event_construction.rs
@@ -93,7 +93,7 @@ fn native_event_producers_bypass_author_constructors_dictionaries_and_indexed_se
0,
),
element::construct_pointer_event(scope, "pointerdown", 11.0, 12.0, 0, 1, &pointer),
- element::construct_drag_event(
+ element::construct_drag_event_with_related_target(
scope,
"dragstart",
11.0,
@@ -101,6 +101,7 @@ fn native_event_producers_bypass_author_constructors_dictionaries_and_indexed_se
1,
transfer.into(),
0,
+ None,
),
element::construct_wheel_event(scope, "wheel", 11.0, 12.0, 4.0, 5.0, 0, 1, 0),
element::construct_touch_event_with_points(
diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/files_and_transfer.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/files_and_transfer.rs
index db2da88836..e91eebd3c2 100644
--- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/files_and_transfer.rs
+++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/files_and_transfer.rs
@@ -503,6 +503,9 @@ fn data_transfer_declared_slots_ignore_prototype_spoofing() {
entry.__lmFileSystemEntryIsFile = false;
entry.__lmFileSystemEntryIsDirectory = true;
+ const receiverError = action => {
+ try { action(); return "no error"; } catch (error) { return error.name; }
+ };
return JSON.stringify({
ownNamesBefore,
descriptors,
@@ -521,14 +524,14 @@ fn data_transfer_declared_slots_ignore_prototype_spoofing() {
entry.isDirectory
].join('|'),
fake: [
- DataTransfer.prototype.getData.call(fakeDataTransfer, 'text/plain'),
- fakeDataTransfer.types === undefined ? 'undefined' : fakeDataTransfer.types.join(','),
- fakeDataTransfer.dropEffect,
- fakeDataTransfer.effectAllowed,
- fakeList.length,
- fakeItem.kind,
- fakeItem.type,
- fakeItem.getAsFile(),
+ receiverError(() => DataTransfer.prototype.getData.call(fakeDataTransfer, 'text/plain')),
+ receiverError(() => fakeDataTransfer.types === undefined ? 'undefined' : fakeDataTransfer.types.join(',')),
+ receiverError(() => fakeDataTransfer.dropEffect),
+ receiverError(() => fakeDataTransfer.effectAllowed),
+ receiverError(() => fakeList.length),
+ receiverError(() => fakeItem.kind),
+ receiverError(() => fakeItem.type),
+ receiverError(() => fakeItem.getAsFile()),
entryNameGetter.call(fakeEntry),
entryPathGetter.call(fakeEntry)
].map(value => value === null ? 'null' : String(value)).join('|')
@@ -540,7 +543,7 @@ fn data_transfer_declared_slots_ignore_prototype_spoofing() {
assert_eq!(
result,
- r#"{"ownNamesBefore":{"dataTransfer":[],"itemList":[],"item":[],"entry":[]},"descriptors":{"dataTransfer":["files:function:get files:0:undefined:none:none:true:true","items:function:get items:0:undefined:none:none:true:true","types:function:get types:0:undefined:none:none:true:true","dropEffect:function:get dropEffect:0:function:set dropEffect:1:true:true","effectAllowed:function:get effectAllowed:0:function:set effectAllowed:1:true:true"],"itemList":["length:function:get length:0:undefined:none:none:true:true"],"item":["kind:function:get kind:0:undefined:none:none:true:true","type:function:get type:0:undefined:none:none:true:true"],"entry":["filesystem:function:get filesystem:0:undefined:none:none:true:true","fullPath:function:get fullPath:0:undefined:none:none:true:true","isDirectory:function:get isDirectory:0:undefined:none:none:true:true","isFile:function:get isFile:0:undefined:none:none:true:true","name:function:get name:0:undefined:none:none:true:true"]},"real":"alpha|text/plain,Files|none|none|2|file|text/plain|note.txt|note.txt|/note.txt|true|false","fake":"|undefined|none|uninitialized|0|||null||"}"#
+ r#"{"ownNamesBefore":{"dataTransfer":[],"itemList":[],"item":[],"entry":[]},"descriptors":{"dataTransfer":["files:function:get files:0:undefined:none:none:true:true","items:function:get items:0:undefined:none:none:true:true","types:function:get types:0:undefined:none:none:true:true","dropEffect:function:get dropEffect:0:function:set dropEffect:1:true:true","effectAllowed:function:get effectAllowed:0:function:set effectAllowed:1:true:true"],"itemList":["length:function:get length:0:undefined:none:none:true:true"],"item":["kind:function:get kind:0:undefined:none:none:true:true","type:function:get type:0:undefined:none:none:true:true"],"entry":["filesystem:function:get filesystem:0:undefined:none:none:true:true","fullPath:function:get fullPath:0:undefined:none:none:true:true","isDirectory:function:get isDirectory:0:undefined:none:none:true:true","isFile:function:get isFile:0:undefined:none:none:true:true","name:function:get name:0:undefined:none:none:true:true"]},"real":"alpha|text/plain,Files|none|none|2|file|text/plain|note.txt|note.txt|/note.txt|true|false","fake":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError|TypeError||"}"#
);
}
diff --git a/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-cancel-basic.html b/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-cancel-basic.html
index b5b5273afc..919e5ba0bb 100644
--- a/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-cancel-basic.html
+++ b/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-cancel-basic.html
@@ -47,11 +47,14 @@ const dropped = new Promise(function (resolve) {
promise_test(async function () {
await dropped;
+ assert_equals(firstDataTransfer.items.length, 0, "saved dragenter view is disabled");
+ assert_equals(firstDataTransfer.types.length, 0, "cancel does not reactivate the old view");
+ assert_equals(firstDataTransfer.getData("text"), "", "old payload stays inaccessible");
assert_array_equals(
events,
[
- "dragenter:initial:true",
- "dragover:mutated-before-cancel:true",
+ "dragenter::true",
+ "dragover::false",
"drop:fresh-after-cancel:false",
],
"dragCancel should clear the active DataTransfer without dispatching a DOM event",
diff --git a/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-lifecycle-basic.html b/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-lifecycle-basic.html
index 41060bf6e0..99d473dbe0 100644
--- a/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-lifecycle-basic.html
+++ b/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-lifecycle-basic.html
@@ -71,16 +71,18 @@ const dropped = new Promise(function (resolve) {
promise_test(async function () {
await dropped;
+ assert_equals(firstDataTransfer.items.length, 0, "saved event item list is disabled");
+ assert_equals(firstDataTransfer.getData("text"), "", "saved event cannot read the store");
assert_array_equals(
events,
[
- "dragenter:true:initial-text:copy:copyMove",
- "dragover-before:true:initial-text",
- "dragover-after:mutated-in-over:move",
- "drop:true:mutated-in-over:move:hello world",
- "input:hello mutated-in-over:21:21",
+ "dragenter:true::copy:copyMove",
+ "dragover-before:false:",
+ "dragover-after::move",
+ "drop:false:initial-text:move:hello world",
+ "input:hello initial-text:18:18",
],
- "drag lifecycle should reuse live DataTransfer and run text drop default action",
+ "drag lifecycle should use temporary protected views and preserve data for the text drop default action",
);
-}, "Input.dispatchDragEvent drives live DataTransfer reuse and text input default drop");
+}, "Input.dispatchDragEvent protects event views and preserves text input default drop");
diff --git a/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-mixed-payload-basic.html b/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-mixed-payload-basic.html
index 82b74b58a1..3ae641f7b3 100644
--- a/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-mixed-payload-basic.html
+++ b/moli-wpt-compat/fixtures/wpt/ported/fileapi/drag-mixed-payload-basic.html
@@ -6,90 +6,90 @@