diff --git a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp index 88b8a2003b..b3f6734624 100644 --- a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp @@ -70,7 +70,7 @@ static bool is_primitive_type(ByteString const& type) static bool is_simple_type(ByteString const& type) { // Small types that it makes sense just to pass by value. - return type.is_one_of("Gfx::Color", "Web::DevicePixels", "Gfx::IntPoint", "Gfx::FloatPoint", "Web::DevicePixelPoint", "Gfx::IntSize", "Gfx::FloatSize", "Web::DevicePixelSize", "Core::File::OpenMode", "Web::Cookie::Source"); + return type.is_one_of("Gfx::Color", "Web::DevicePixels", "Gfx::IntPoint", "Gfx::FloatPoint", "Web::DevicePixelPoint", "Gfx::IntSize", "Gfx::FloatSize", "Web::DevicePixelSize", "Core::File::OpenMode", "Web::Cookie::Source", "Web::HTML::AllowMultipleFiles"); } static bool is_primitive_or_simple_type(ByteString const& type) diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn index 920adbabcf..84c30ce754 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn @@ -140,6 +140,7 @@ source_set("HTML") { "PotentialCORSRequest.cpp", "PromiseRejectionEvent.cpp", "SelectItem.cpp", + "SelectedFile.cpp", "SessionHistoryEntry.cpp", "SharedImageRequest.cpp", "SourceSet.cpp", diff --git a/Tests/LibWeb/Text/expected/input-file.txt b/Tests/LibWeb/Text/expected/input-file.txt new file mode 100644 index 0000000000..6e3a94c410 --- /dev/null +++ b/Tests/LibWeb/Text/expected/input-file.txt @@ -0,0 +1,7 @@ +Select file...file1 Select files...4 files selected. input1: +file1: Contents for file1 +input2: +file1: Contents for file1 +file2: Contents for file2 +file3: Contents for file3 +file4: Contents for file4 diff --git a/Tests/LibWeb/Text/input/input-file.html b/Tests/LibWeb/Text/input/input-file.html new file mode 100644 index 0000000000..0ee499460a --- /dev/null +++ b/Tests/LibWeb/Text/input/input-file.html @@ -0,0 +1,32 @@ + + + + diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index e9b4b480fb..b9d51888b1 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -389,6 +389,7 @@ set(SOURCES HTML/Scripting/TemporaryExecutionContext.cpp HTML/Scripting/WindowEnvironmentSettingsObject.cpp HTML/Scripting/WorkerEnvironmentSettingsObject.cpp + HTML/SelectedFile.cpp HTML/SelectItem.cpp HTML/SessionHistoryEntry.cpp HTML/SharedImageRequest.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 4adba1b4ea..0406287bd1 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -445,6 +445,7 @@ class Path2D; class Plugin; class PluginArray; class PromiseRejectionEvent; +class SelectedFile; class SharedImageRequest; class Storage; class SubmitEvent; @@ -468,6 +469,7 @@ class WorkerGlobalScope; class WorkerLocation; class WorkerNavigator; +enum class AllowMultipleFiles; enum class MediaSeekMode; enum class SandboxingFlagSet; diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 51735c72c5..c0902bb29a 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -213,12 +215,12 @@ static void show_the_picker_if_applicable(HTMLInputElement& element) // with the bubbles attribute initialized to true. // 5. Otherwise, update the file selection for element. - bool const multiple = element.has_attribute(HTML::AttributeNames::multiple); - auto weak_element = element.make_weak_ptr(); + auto allow_multiple_files = element.has_attribute(HTML::AttributeNames::multiple) ? AllowMultipleFiles::Yes : AllowMultipleFiles::No; + auto weak_element = element.make_weak_ptr(); // FIXME: Pass along accept attribute information https://html.spec.whatwg.org/multipage/input.html#attr-input-accept // The accept attribute may be specified to provide user agents with a hint of what file types will be accepted. - element.document().browsing_context()->top_level_browsing_context()->page().client().page_did_request_file_picker(weak_element, multiple); + element.document().browsing_context()->top_level_browsing_context()->page().did_request_file_picker(weak_element, allow_multiple_files); return; } @@ -380,6 +382,51 @@ void HTMLInputElement::did_pick_color(Optional picked_color) } } +void HTMLInputElement::did_select_files(Span selected_files) +{ + // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable + // 4. If the user dismissed the prompt without changing their selection, then queue an element task on the user + // interaction task source given element to fire an event named cancel at element, with the bubbles attribute + // initialized to true. + if (selected_files.is_empty()) { + queue_an_element_task(HTML::Task::Source::UserInteraction, [this]() { + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::cancel, { .bubbles = true })); + }); + + return; + } + + Vector> files; + files.ensure_capacity(selected_files.size()); + + for (auto& selected_file : selected_files) { + auto contents = selected_file.take_contents(); + + auto mime_type = MUST(MimeSniff::Resource::sniff(contents)); + auto blob = FileAPI::Blob::create(realm(), move(contents), mime_type.essence()); + + // FIXME: The FileAPI should use ByteString for file names. + auto file_name = MUST(String::from_byte_string(selected_file.name())); + + auto file = MUST(FileAPI::File::create(realm(), { JS::make_handle(blob) }, file_name)); + files.unchecked_append(file); + } + + // https://html.spec.whatwg.org/multipage/input.html#update-the-file-selection + // 1. Queue an element task on the user interaction task source given element and the following steps: + queue_an_element_task(HTML::Task::Source::UserInteraction, [this, files = move(files)]() mutable { + // 1. Update element's selected files so that it represents the user's selection. + m_selected_files = FileAPI::FileList::create(realm(), move(files)); + update_file_input_shadow_tree(); + + // 2. Fire an event named input at the input element, with the bubbles and composed attributes initialized to true. + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::input, { .bubbles = true, .composed = true })); + + // 3. Fire an event named change at the input element, with the bubbles attribute initialized to true. + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::change, { .bubbles = true })); + }); +} + String HTMLInputElement::value() const { switch (value_attribute_mode()) { diff --git a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h index 95c287e7d8..278d5d1532 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -94,6 +94,8 @@ public: void did_pick_color(Optional picked_color); + void did_select_files(Span selected_files); + JS::GCPtr files(); void set_files(JS::GCPtr); diff --git a/Userland/Libraries/LibWeb/HTML/SelectedFile.cpp b/Userland/Libraries/LibWeb/HTML/SelectedFile.cpp new file mode 100644 index 0000000000..be31a4e1b3 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/SelectedFile.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace Web::HTML { + +ErrorOr SelectedFile::from_file_path(ByteString const& file_path) +{ + // https://html.spec.whatwg.org/multipage/input.html#file-upload-state-(type=file):concept-input-file-path + // Filenames must not contain path components, even in the case that a user has selected an entire directory + // hierarchy or multiple files with the same name from different directories. + auto name = LexicalPath::basename(file_path); + + auto file = TRY(Core::File::open(file_path, Core::File::OpenMode::Read)); + return SelectedFile { move(name), IPC::File { *file } }; +} + +SelectedFile::SelectedFile(ByteString name, ByteBuffer contents) + : m_name(move(name)) + , m_file_or_contents(move(contents)) +{ +} + +SelectedFile::SelectedFile(ByteString name, IPC::File file) + : m_name(move(name)) + , m_file_or_contents(move(file)) +{ +} + +ByteBuffer SelectedFile::take_contents() +{ + VERIFY(m_file_or_contents.has()); + return move(m_file_or_contents.get()); +} + +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SelectedFile const& file) +{ + TRY(encoder.encode(file.name())); + TRY(encoder.encode(file.file_or_contents())); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto name = TRY(decoder.decode()); + auto file_or_contents = TRY((decoder.decode>())); + + ByteBuffer contents; + + if (file_or_contents.has()) { + auto file = TRY(Core::File::adopt_fd(file_or_contents.get().take_fd(), Core::File::OpenMode::Read)); + contents = TRY(file->read_until_eof()); + } else { + contents = move(file_or_contents.get()); + } + + return Web::HTML::SelectedFile { move(name), move(contents) }; +} diff --git a/Userland/Libraries/LibWeb/HTML/SelectedFile.h b/Userland/Libraries/LibWeb/HTML/SelectedFile.h new file mode 100644 index 0000000000..8d1739231d --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/SelectedFile.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::HTML { + +enum class AllowMultipleFiles { + No, + Yes, +}; + +class SelectedFile { +public: + static ErrorOr from_file_path(ByteString const& file_path); + + SelectedFile(ByteString name, ByteBuffer contents); + SelectedFile(ByteString name, IPC::File file); + + ByteString const& name() const { return m_name; } + auto const& file_or_contents() const { return m_file_or_contents; } + ByteBuffer take_contents(); + +private: + ByteString m_name; + Variant m_file_or_contents; +}; + +} + +namespace IPC { + +template<> +ErrorOr encode(Encoder&, Web::HTML::SelectedFile const&); + +template<> +ErrorOr decode(Decoder&); + +} diff --git a/Userland/Libraries/LibWeb/Page/Page.cpp b/Userland/Libraries/LibWeb/Page/Page.cpp index e6df976060..dfdce259f5 100644 --- a/Userland/Libraries/LibWeb/Page/Page.cpp +++ b/Userland/Libraries/LibWeb/Page/Page.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -343,6 +344,30 @@ void Page::color_picker_update(Optional picked_color, HTML::ColorPickerUp } } +void Page::did_request_file_picker(WeakPtr target, HTML::AllowMultipleFiles allow_multiple_files) +{ + if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { + m_pending_non_blocking_dialog = PendingNonBlockingDialog::FilePicker; + m_pending_non_blocking_dialog_target = move(target); + + m_client->page_did_request_file_picker(allow_multiple_files); + } +} + +void Page::file_picker_closed(Span selected_files) +{ + if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::FilePicker) { + m_pending_non_blocking_dialog = PendingNonBlockingDialog::None; + + if (m_pending_non_blocking_dialog_target) { + auto& input_element = verify_cast(*m_pending_non_blocking_dialog_target); + input_element.did_select_files(selected_files); + + m_pending_non_blocking_dialog_target.clear(); + } + } +} + void Page::did_request_select_dropdown(WeakPtr target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items) { if (m_pending_non_blocking_dialog == PendingNonBlockingDialog::None) { diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index f1e360c70a..595a29ea8e 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -134,12 +134,16 @@ public: void did_request_color_picker(WeakPtr target, Color current_color); void color_picker_update(Optional picked_color, HTML::ColorPickerUpdateState state); + void did_request_file_picker(WeakPtr target, HTML::AllowMultipleFiles); + void file_picker_closed(Span selected_files); + void did_request_select_dropdown(WeakPtr target, Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items); void select_dropdown_closed(Optional value); enum class PendingNonBlockingDialog { None, ColorPicker, + FilePicker, Select, }; @@ -280,8 +284,8 @@ public: virtual void request_file(FileRequest) = 0; // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable - virtual void page_did_request_file_picker(WeakPtr, [[maybe_unused]] bool multiple) {}; virtual void page_did_request_color_picker([[maybe_unused]] Color current_color) {}; + virtual void page_did_request_file_picker(Web::HTML::AllowMultipleFiles) {}; virtual void page_did_request_select_dropdown([[maybe_unused]] Web::CSSPixelPoint content_position, [[maybe_unused]] Web::CSSPixels minimum_width, [[maybe_unused]] Vector items) {}; virtual void page_did_finish_text_test() {}; diff --git a/Userland/Libraries/LibWebView/ViewImplementation.cpp b/Userland/Libraries/LibWebView/ViewImplementation.cpp index 2b5d5e61cd..833abb4509 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.cpp +++ b/Userland/Libraries/LibWebView/ViewImplementation.cpp @@ -250,6 +250,11 @@ void ViewImplementation::color_picker_update(Optional picked_color, Web:: client().async_color_picker_update(page_id(), picked_color, state); } +void ViewImplementation::file_picker_closed(Vector selected_files) +{ + client().async_file_picker_closed(page_id(), move(selected_files)); +} + void ViewImplementation::select_dropdown_closed(Optional value) { client().async_select_dropdown_closed(page_id(), value); diff --git a/Userland/Libraries/LibWebView/ViewImplementation.h b/Userland/Libraries/LibWebView/ViewImplementation.h index f1c107c679..805728ccd9 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.h +++ b/Userland/Libraries/LibWebView/ViewImplementation.h @@ -86,6 +86,7 @@ public: void confirm_closed(bool accepted); void prompt_closed(Optional response); void color_picker_update(Optional picked_color, Web::HTML::ColorPickerUpdateState state); + void file_picker_closed(Vector selected_files); void select_dropdown_closed(Optional value); void toggle_media_play_state(); @@ -164,6 +165,7 @@ public: Function on_minimize_window; Function on_fullscreen_window; Function on_request_color_picker; + Function on_request_file_picker; Function items)> on_request_select_dropdown; Function on_finish_handling_input_event; Function on_text_test_finish; diff --git a/Userland/Libraries/LibWebView/WebContentClient.cpp b/Userland/Libraries/LibWebView/WebContentClient.cpp index 21875ce0b0..1f423eb833 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.cpp +++ b/Userland/Libraries/LibWebView/WebContentClient.cpp @@ -806,6 +806,19 @@ void WebContentClient::did_request_color_picker(u64 page_id, Color const& curren view.on_request_color_picker(current_color); } +void WebContentClient::did_request_file_picker(u64 page_id, Web::HTML::AllowMultipleFiles allow_multiple_files) +{ + auto maybe_view = m_views.get(page_id); + if (!maybe_view.has_value()) { + dbgln("Received request file picker for unknown page ID {}", page_id); + return; + } + auto& view = *maybe_view.value(); + + if (view.on_request_file_picker) + view.on_request_file_picker(allow_multiple_files); +} + void WebContentClient::did_request_select_dropdown(u64 page_id, Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) { auto maybe_view = m_views.get(page_id); diff --git a/Userland/Libraries/LibWebView/WebContentClient.h b/Userland/Libraries/LibWebView/WebContentClient.h index 85af653e7d..f55b1f5124 100644 --- a/Userland/Libraries/LibWebView/WebContentClient.h +++ b/Userland/Libraries/LibWebView/WebContentClient.h @@ -89,6 +89,7 @@ private: virtual Messages::WebContentClient::DidRequestFullscreenWindowResponse did_request_fullscreen_window(u64 page_id) override; virtual void did_request_file(u64 page_id, ByteString const& path, i32) override; virtual void did_request_color_picker(u64 page_id, Color const& current_color) override; + virtual void did_request_file_picker(u64 page_id, Web::HTML::AllowMultipleFiles) override; virtual void did_request_select_dropdown(u64 page_id, Gfx::IntPoint content_position, i32 minimum_width, Vector const& items) override; virtual void did_finish_handling_input_event(u64 page_id, bool event_was_accepted) override; virtual void did_finish_text_test(u64 page_id) override; diff --git a/Userland/Services/WebContent/ConnectionFromClient.cpp b/Userland/Services/WebContent/ConnectionFromClient.cpp index cbcaf800f0..585a58e02c 100644 --- a/Userland/Services/WebContent/ConnectionFromClient.cpp +++ b/Userland/Services/WebContent/ConnectionFromClient.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include #include #include @@ -1317,6 +1319,18 @@ void ConnectionFromClient::color_picker_update(u64 page_id, Optional cons page.page().color_picker_update(picked_color, state); } +void ConnectionFromClient::file_picker_closed(u64 page_id, Vector const& selected_files) +{ + auto maybe_page = page(page_id); + if (!maybe_page.has_value()) { + dbgln("ConnectionFromClient::color_picker_update: No page with ID {}", page_id); + return; + } + + auto& page = maybe_page.release_value(); + page.page().file_picker_closed(const_cast&>(selected_files)); +} + void ConnectionFromClient::select_dropdown_closed(u64 page_id, Optional const& value) { auto maybe_page = page(page_id); diff --git a/Userland/Services/WebContent/ConnectionFromClient.h b/Userland/Services/WebContent/ConnectionFromClient.h index 5dd491c47f..6e3159c9d8 100644 --- a/Userland/Services/WebContent/ConnectionFromClient.h +++ b/Userland/Services/WebContent/ConnectionFromClient.h @@ -106,6 +106,7 @@ private: virtual void confirm_closed(u64 page_id, bool accepted) override; virtual void prompt_closed(u64 page_id, Optional const& response) override; virtual void color_picker_update(u64 page_id, Optional const& picked_color, Web::HTML::ColorPickerUpdateState const& state) override; + virtual void file_picker_closed(u64 page_id, Vector const& selected_files) override; virtual void select_dropdown_closed(u64 page_id, Optional const& value) override; virtual void toggle_media_play_state(u64 page_id) override; diff --git a/Userland/Services/WebContent/PageClient.cpp b/Userland/Services/WebContent/PageClient.cpp index 73dc906753..3d66bf2fdf 100644 --- a/Userland/Services/WebContent/PageClient.cpp +++ b/Userland/Services/WebContent/PageClient.cpp @@ -549,6 +549,11 @@ void PageClient::page_did_request_color_picker(Color current_color) client().async_did_request_color_picker(m_id, current_color); } +void PageClient::page_did_request_file_picker(Web::HTML::AllowMultipleFiles allow_multiple_files) +{ + client().async_did_request_file_picker(m_id, allow_multiple_files); +} + void PageClient::page_did_request_select_dropdown(Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items) { client().async_did_request_select_dropdown(m_id, page().css_to_device_point(content_position).to_type(), minimum_width * device_pixels_per_css_pixel(), items); diff --git a/Userland/Services/WebContent/PageClient.h b/Userland/Services/WebContent/PageClient.h index fb40371dd3..8936cfdf23 100644 --- a/Userland/Services/WebContent/PageClient.h +++ b/Userland/Services/WebContent/PageClient.h @@ -133,6 +133,7 @@ private: virtual void page_did_close_top_level_traversable() override; virtual void request_file(Web::FileRequest) override; virtual void page_did_request_color_picker(Color current_color) override; + virtual void page_did_request_file_picker(Web::HTML::AllowMultipleFiles) override; virtual void page_did_request_select_dropdown(Web::CSSPixelPoint content_position, Web::CSSPixels minimum_width, Vector items) override; virtual void page_did_finish_text_test() override; virtual void page_did_change_theme_color(Gfx::Color color) override; diff --git a/Userland/Services/WebContent/WebContentClient.ipc b/Userland/Services/WebContent/WebContentClient.ipc index 57b8729a56..0abce74f54 100644 --- a/Userland/Services/WebContent/WebContentClient.ipc +++ b/Userland/Services/WebContent/WebContentClient.ipc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,7 @@ endpoint WebContentClient did_request_fullscreen_window(u64 page_id) => (Gfx::IntRect window_rect) did_request_file(u64 page_id, ByteString path, i32 request_id) =| did_request_color_picker(u64 page_id, Color current_color) =| + did_request_file_picker(u64 page_id, Web::HTML::AllowMultipleFiles allow_multiple_files) =| did_request_select_dropdown(u64 page_id, Gfx::IntPoint content_position, i32 minimum_width, Vector items) =| did_finish_handling_input_event(u64 page_id, bool event_was_accepted) =| did_change_theme_color(u64 page_id, Gfx::Color color) =| diff --git a/Userland/Services/WebContent/WebContentServer.ipc b/Userland/Services/WebContent/WebContentServer.ipc index 7db19cae92..f16e836254 100644 --- a/Userland/Services/WebContent/WebContentServer.ipc +++ b/Userland/Services/WebContent/WebContentServer.ipc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -93,6 +94,7 @@ endpoint WebContentServer confirm_closed(u64 page_id, bool accepted) =| prompt_closed(u64 page_id, Optional response) =| color_picker_update(u64 page_id, Optional picked_color, Web::HTML::ColorPickerUpdateState state) =| + file_picker_closed(u64 page_id, Vector selected_files) =| select_dropdown_closed(u64 page_id, Optional value) =| toggle_media_play_state(u64 page_id) =| diff --git a/Userland/Utilities/headless-browser.cpp b/Userland/Utilities/headless-browser.cpp index 5990263444..433e53aafc 100644 --- a/Userland/Utilities/headless-browser.cpp +++ b/Userland/Utilities/headless-browser.cpp @@ -8,6 +8,7 @@ */ #include +#include #include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -433,6 +435,21 @@ static ErrorOr run_test(HeadlessWebContentView& view, StringView inp promise->resolve({}); }; view.on_text_test_finish = {}; + + view.on_request_file_picker = [&](auto allow_multiple_files) { + // Create some dummy files for tests. + Vector selected_files; + selected_files.empend("file1"sv, MUST(ByteBuffer::copy("Contents for file1"sv.bytes()))); + + if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) { + selected_files.empend("file2"sv, MUST(ByteBuffer::copy("Contents for file2"sv.bytes()))); + selected_files.empend("file3"sv, MUST(ByteBuffer::copy("Contents for file3"sv.bytes()))); + selected_files.empend("file4"sv, MUST(ByteBuffer::copy("Contents for file4"sv.bytes()))); + } + + view.file_picker_closed(move(selected_files)); + }; + view.load(URL("about:blank"sv)); MUST(promise->await());