diff --git a/AK/Debug.h.in b/AK/Debug.h.in index ddb495e39fa..85c58a87efd 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -114,6 +114,10 @@ # cmakedefine01 ICO_DEBUG #endif +#ifndef IDB_DEBUG +# cmakedefine01 IDB_DEBUG +#endif + #ifndef IDL_DEBUG # cmakedefine01 IDL_DEBUG #endif diff --git a/Libraries/LibCore/EventLoopImplementationWindows.cpp b/Libraries/LibCore/EventLoopImplementationWindows.cpp index 8ca0ec207d8..2e38479c518 100644 --- a/Libraries/LibCore/EventLoopImplementationWindows.cpp +++ b/Libraries/LibCore/EventLoopImplementationWindows.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include @@ -55,10 +56,12 @@ struct EventLoopTimer { }; struct ThreadData { - static ThreadData& the() + static ThreadData* the() { thread_local OwnPtr thread_data = make(); - return *thread_data; + if (thread_data) + return &*thread_data; + return nullptr; } ThreadData() @@ -76,7 +79,7 @@ struct ThreadData { }; EventLoopImplementationWindows::EventLoopImplementationWindows() - : m_wake_event(ThreadData::the().wake_event.handle) + : m_wake_event(ThreadData::the()->wake_event.handle) { } @@ -92,9 +95,10 @@ int EventLoopImplementationWindows::exec() size_t EventLoopImplementationWindows::pump(PumpMode) { - auto& thread_data = ThreadData::the(); - auto& notifiers = thread_data.notifiers; - auto& timers = thread_data.timers; + auto& event_queue = ThreadEventQueue::current(); + auto* thread_data = ThreadData::the(); + auto& notifiers = thread_data->notifiers; + auto& timers = thread_data->timers; size_t event_count = 1 + notifiers.size() + timers.size(); // If 64 events limit proves to be insufficient RegisterWaitForSingleObject or other methods @@ -103,30 +107,41 @@ size_t EventLoopImplementationWindows::pump(PumpMode) VERIFY(event_count <= MAXIMUM_WAIT_OBJECTS); Vector event_handles; - event_handles.append(thread_data.wake_event.handle); + event_handles.append(thread_data->wake_event.handle); for (auto& entry : notifiers) event_handles.append(entry.key.handle); for (auto& entry : timers) event_handles.append(entry.key.handle); - DWORD result = WaitForMultipleObjects(event_count, event_handles.data(), FALSE, INFINITE); - size_t index = result - WAIT_OBJECT_0; - VERIFY(index < event_count); - - if (index != 0) { - if (index <= notifiers.size()) { - Notifier* notifier = *notifiers.get(event_handles[index]); - ThreadEventQueue::current().post_event(*notifier, make(notifier->fd(), notifier->type())); - } else { - auto& timer = *timers.get(event_handles[index]); - if (auto strong_owner = timer.owner.strong_ref()) - if (timer.fire_when_not_visible == TimerShouldFireWhenNotVisible::Yes || strong_owner->is_visible_for_timer_purposes()) - ThreadEventQueue::current().post_event(*strong_owner, make()); + bool has_pending_events = event_queue.has_pending_events(); + int timeout = has_pending_events ? 0 : INFINITE; + DWORD result = WaitForMultipleObjects(event_count, event_handles.data(), FALSE, timeout); + if (result == WAIT_TIMEOUT) { + // FIXME: This verification sometimes fails with ERROR_INVALID_HANDLE, but when I check + // the handles they all seem to be valid. + // VERIFY(GetLastError() == ERROR_SUCCESS || GetLastError() == ERROR_IO_PENDING); + } else { + size_t const index = result - WAIT_OBJECT_0; + VERIFY(index < event_count); + // : 1 - skip wake event + for (size_t i = index ? index : 1; i < event_count; i++) { + // i == index already checked by WaitForMultipleObjects + if (i == index || WaitForSingleObject(event_handles[i], 0) == WAIT_OBJECT_0) { + if (i <= notifiers.size()) { + Notifier* notifier = *notifiers.get(event_handles[i]); + event_queue.post_event(*notifier, make(notifier->fd(), notifier->type())); + } else { + auto& timer = *timers.get(event_handles[i]); + if (auto strong_owner = timer.owner.strong_ref()) + if (timer.fire_when_not_visible == TimerShouldFireWhenNotVisible::Yes || strong_owner->is_visible_for_timer_purposes()) + event_queue.post_event(*strong_owner, make()); + } + } } } - return ThreadEventQueue::current().process(); + return event_queue.process(); } void EventLoopImplementationWindows::quit(int code) @@ -167,7 +182,7 @@ void EventLoopManagerWindows::register_notifier(Notifier& notifier) int rc = WSAEventSelect(notifier.fd(), event, notifier_type_to_network_event(notifier.type())); VERIFY(!rc); - auto& notifiers = ThreadData::the().notifiers; + auto& notifiers = ThreadData::the()->notifiers; VERIFY(!notifiers.get(event).has_value()); notifiers.set(Handle(event), ¬ifier); } @@ -175,13 +190,16 @@ void EventLoopManagerWindows::register_notifier(Notifier& notifier) void EventLoopManagerWindows::unregister_notifier(Notifier& notifier) { // remove_first_matching would be clearer, but currently there is no such method in HashMap - ThreadData::the().notifiers.remove_all_matching([&](auto&, auto value) { return value == ¬ifier; }); + if (ThreadData::the()) + ThreadData::the()->notifiers.remove_all_matching([&](auto&, auto value) { return value == ¬ifier; }); } intptr_t EventLoopManagerWindows::register_timer(EventReceiver& object, int milliseconds, bool should_reload, TimerShouldFireWhenNotVisible fire_when_not_visible) { VERIFY(milliseconds >= 0); - HANDLE timer = CreateWaitableTimer(NULL, FALSE, NULL); + // FIXME: This is a temporary fix for issue #3641 + bool manual_reset = static_cast(object).is_single_shot(); + HANDLE timer = CreateWaitableTimer(NULL, manual_reset, NULL); VERIFY(timer); LARGE_INTEGER first_time = {}; @@ -190,15 +208,16 @@ intptr_t EventLoopManagerWindows::register_timer(EventReceiver& object, int mill BOOL rc = SetWaitableTimer(timer, &first_time, should_reload ? milliseconds : 0, NULL, NULL, FALSE); VERIFY(rc); - auto& timers = ThreadData::the().timers; + auto& timers = ThreadData::the()->timers; VERIFY(!timers.get(timer).has_value()); timers.set(Handle(timer), { object, fire_when_not_visible }); - return (intptr_t)timer; + return reinterpret_cast(timer); } void EventLoopManagerWindows::unregister_timer(intptr_t timer_id) { - ThreadData::the().timers.remove((HANDLE)timer_id); + if (ThreadData::the()) + ThreadData::the()->timers.remove(reinterpret_cast(timer_id)); } int EventLoopManagerWindows::register_signal([[maybe_unused]] int signal_number, [[maybe_unused]] Function handler) diff --git a/Libraries/LibGC/Cell.h b/Libraries/LibGC/Cell.h index 4b857db94d1..5c5260715d0 100644 --- a/Libraries/LibGC/Cell.h +++ b/Libraries/LibGC/Cell.h @@ -107,8 +107,8 @@ public: visit(value); } - template - void visit(Vector const& vector) + template + void visit(Vector const& vector) { for (auto& value : vector) visit(value); diff --git a/Libraries/LibIPC/Connection.cpp b/Libraries/LibIPC/Connection.cpp index cc02f30d85b..8f15685393d 100644 --- a/Libraries/LibIPC/Connection.cpp +++ b/Libraries/LibIPC/Connection.cpp @@ -12,7 +12,6 @@ #include #include #include -#include namespace IPC { @@ -40,21 +39,16 @@ bool ConnectionBase::is_open() const ErrorOr ConnectionBase::post_message(Message const& message) { - return post_message(message.endpoint_magic(), TRY(message.encode())); + return post_message(TRY(message.encode())); } -ErrorOr ConnectionBase::post_message(u32 endpoint_magic, MessageBuffer buffer) +ErrorOr ConnectionBase::post_message(MessageBuffer buffer) { // NOTE: If this connection is being shut down, but has not yet been destroyed, // the socket will be closed. Don't try to send more messages. if (!m_transport->is_open()) return Error::from_string_literal("Trying to post_message during IPC shutdown"); - if (buffer.data().size() > TransportSocket::SOCKET_BUFFER_SIZE) { - auto wrapper = LargeMessageWrapper::create(endpoint_magic, buffer); - buffer = MUST(wrapper->encode()); - } - MUST(buffer.transfer_message(*m_transport)); m_responsiveness_timer->start(); @@ -85,7 +79,7 @@ void ConnectionBase::handle_messages() } if (auto response = handler_result.release_value()) { - if (auto post_result = post_message(m_local_endpoint_magic, *response); post_result.is_error()) { + if (auto post_result = post_message(*response); post_result.is_error()) { dbgln("IPC::ConnectionBase::handle_messages: {}", post_result.error()); } } @@ -100,24 +94,11 @@ void ConnectionBase::wait_for_transport_to_become_readable() ErrorOr ConnectionBase::drain_messages_from_peer() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& unparsed_message) { - auto const& bytes = unparsed_message.bytes; - UnprocessedFileDescriptors unprocessed_fds; - unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); - if (auto message = try_parse_message(bytes, unprocessed_fds)) { - if (message->message_id() == LargeMessageWrapper::MESSAGE_ID) { - LargeMessageWrapper* wrapper = static_cast(message.ptr()); - auto wrapped_message = wrapper->wrapped_message_data(); - unprocessed_fds.return_fds_to_front_of_queue(wrapper->take_fds()); - auto parsed_message = try_parse_message(wrapped_message, unprocessed_fds); - VERIFY(parsed_message); - m_unprocessed_messages.append(parsed_message.release_nonnull()); - return; - } - + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([&](auto&& raw_message) { + if (auto message = try_parse_message(raw_message.bytes, raw_message.fds)) { m_unprocessed_messages.append(message.release_nonnull()); } else { - dbgln("Failed to parse IPC message {:hex-dump}", bytes); + dbgln("Failed to parse IPC message {:hex-dump}", raw_message.bytes); VERIFY_NOT_REACHED(); } }); diff --git a/Libraries/LibIPC/Connection.h b/Libraries/LibIPC/Connection.h index 0f5b918226f..c1644e66b6a 100644 --- a/Libraries/LibIPC/Connection.h +++ b/Libraries/LibIPC/Connection.h @@ -15,10 +15,6 @@ #include #include #include -#include -#include -#include -#include namespace IPC { @@ -30,7 +26,7 @@ public: [[nodiscard]] bool is_open() const; ErrorOr post_message(Message const&); - ErrorOr post_message(u32 endpoint_magic, MessageBuffer); + ErrorOr post_message(MessageBuffer); void shutdown(); virtual void die() { } @@ -43,7 +39,7 @@ protected: virtual void may_have_become_unresponsive() { } virtual void did_become_responsive() { } virtual void shutdown_with_error(Error const&); - virtual OwnPtr try_parse_message(ReadonlyBytes, UnprocessedFileDescriptors&) = 0; + virtual OwnPtr try_parse_message(ReadonlyBytes, Queue&) = 0; OwnPtr wait_for_specific_endpoint_message_impl(u32 endpoint_magic, int message_id); void wait_for_transport_to_become_readable(); @@ -102,7 +98,7 @@ protected: return {}; } - virtual OwnPtr try_parse_message(ReadonlyBytes bytes, UnprocessedFileDescriptors& fds) override + virtual OwnPtr try_parse_message(ReadonlyBytes bytes, Queue& fds) override { auto local_message = LocalEndpoint::decode_message(bytes, fds); if (!local_message.is_error()) diff --git a/Libraries/LibIPC/Decoder.h b/Libraries/LibIPC/Decoder.h index c7fdd892b72..7fa284c26a2 100644 --- a/Libraries/LibIPC/Decoder.h +++ b/Libraries/LibIPC/Decoder.h @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -38,7 +37,7 @@ inline ErrorOr decode(Decoder&) class Decoder { public: - Decoder(Stream& stream, UnprocessedFileDescriptors& files) + Decoder(Stream& stream, Queue& files) : m_stream(stream) , m_files(files) { @@ -63,11 +62,11 @@ public: ErrorOr decode_size(); Stream& stream() { return m_stream; } - UnprocessedFileDescriptors& files() { return m_files; } + Queue& files() { return m_files; } private: Stream& m_stream; - UnprocessedFileDescriptors& m_files; + Queue& m_files; }; template diff --git a/Libraries/LibIPC/Message.cpp b/Libraries/LibIPC/Message.cpp index 5652bac21cb..680cc329d08 100644 --- a/Libraries/LibIPC/Message.cpp +++ b/Libraries/LibIPC/Message.cpp @@ -6,7 +6,6 @@ #include #include -#include #include namespace IPC { @@ -47,53 +46,4 @@ ErrorOr MessageBuffer::transfer_message(Transport& transport) return {}; } -NonnullOwnPtr LargeMessageWrapper::create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap) -{ - auto size = buffer_to_wrap.data().size(); - auto wrapped_message_data = MUST(Core::AnonymousBuffer::create_with_size(size)); - memcpy(wrapped_message_data.data(), buffer_to_wrap.data().data(), size); - Vector files; - for (auto& owned_fd : buffer_to_wrap.take_fds()) { - files.append(File::adopt_fd(owned_fd->take_fd())); - } - return make(endpoint_magic, move(wrapped_message_data), move(files)); -} - -LargeMessageWrapper::LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds) - : m_endpoint_magic(endpoint_magic) - , m_wrapped_message_data(move(wrapped_message_data)) - , m_wrapped_fds(move(wrapped_fds)) -{ -} - -ErrorOr LargeMessageWrapper::encode() const -{ - MessageBuffer buffer; - Encoder stream { buffer }; - TRY(stream.encode(m_endpoint_magic)); - TRY(stream.encode(MESSAGE_ID)); - TRY(stream.encode(m_wrapped_message_data)); - TRY(stream.encode(m_wrapped_fds.size())); - for (auto const& wrapped_fd : m_wrapped_fds) { - TRY(stream.append_file_descriptor(wrapped_fd.take_fd())); - } - - return buffer; -} - -ErrorOr> LargeMessageWrapper::decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files) -{ - Decoder decoder { stream, files }; - auto wrapped_message_data = TRY(decoder.decode()); - - Vector wrapped_fds; - auto num_fds = TRY(decoder.decode()); - for (u32 i = 0; i < num_fds; ++i) { - auto fd = TRY(decoder.decode()); - wrapped_fds.append(move(fd)); - } - - return make(endpoint_magic, wrapped_message_data, move(wrapped_fds)); -} - } diff --git a/Libraries/LibIPC/Message.h b/Libraries/LibIPC/Message.h index 65333340f2a..e79638618ed 100644 --- a/Libraries/LibIPC/Message.h +++ b/Libraries/LibIPC/Message.h @@ -8,14 +8,8 @@ #pragma once #include -#include -#include #include -#include -#include -#include #include -#include namespace IPC { @@ -67,30 +61,4 @@ protected: Message() = default; }; -class LargeMessageWrapper : public Message { -public: - ~LargeMessageWrapper() override = default; - - static constexpr int MESSAGE_ID = 0x0; - - static NonnullOwnPtr create(u32 endpoint_magic, MessageBuffer& buffer_to_wrap); - - u32 endpoint_magic() const override { return m_endpoint_magic; } - int message_id() const override { return MESSAGE_ID; } - char const* message_name() const override { return "LargeMessageWrapper"; } - ErrorOr encode() const override; - - static ErrorOr> decode(u32 endpoint_magic, Stream& stream, UnprocessedFileDescriptors& files); - - ReadonlyBytes wrapped_message_data() const { return ReadonlyBytes { m_wrapped_message_data.data(), m_wrapped_message_data.size() }; } - auto take_fds() { return move(m_wrapped_fds); } - - LargeMessageWrapper(u32 endpoint_magic, Core::AnonymousBuffer wrapped_message_data, Vector&& wrapped_fds); - -private: - u32 m_endpoint_magic { 0 }; - Core::AnonymousBuffer m_wrapped_message_data; - Vector m_wrapped_fds; -}; - } diff --git a/Libraries/LibIPC/TransportSocket.cpp b/Libraries/LibIPC/TransportSocket.cpp index 86239539ce7..0c7a2064cd4 100644 --- a/Libraries/LibIPC/TransportSocket.cpp +++ b/Libraries/LibIPC/TransportSocket.cpp @@ -13,26 +13,80 @@ namespace IPC { +void SendQueue::enqueue_message(Vector&& bytes, Vector&& fds) +{ + Threading::MutexLocker locker(m_mutex); + m_bytes.append(bytes.data(), bytes.size()); + m_fds.append(fds.data(), fds.size()); + m_condition.signal(); +} + +SendQueue::Running SendQueue::block_until_message_enqueued() +{ + Threading::MutexLocker locker(m_mutex); + while (m_bytes.is_empty() && m_fds.is_empty() && m_running) + m_condition.wait(); + return m_running ? Running::Yes : Running::No; +} + +SendQueue::BytesAndFds SendQueue::dequeue(size_t max_bytes) +{ + Threading::MutexLocker locker(m_mutex); + auto bytes_to_send = min(max_bytes, m_bytes.size()); + Vector bytes; + bytes.append(m_bytes.data(), bytes_to_send); + m_bytes.remove(0, bytes_to_send); + return { move(bytes), move(m_fds) }; +} + +void SendQueue::return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds) +{ + Threading::MutexLocker locker(m_mutex); + m_bytes.prepend(bytes.data(), bytes.size()); + m_fds.prepend(fds.data(), fds.size()); +} + +void SendQueue::stop() +{ + Threading::MutexLocker locker(m_mutex); + m_running = false; + m_condition.signal(); +} + TransportSocket::TransportSocket(NonnullOwnPtr socket) : m_socket(move(socket)) { m_send_queue = adopt_ref(*new SendQueue); m_send_thread = Threading::Thread::construct([this, send_queue = m_send_queue]() -> intptr_t { for (;;) { - send_queue->mutex.lock(); - while (send_queue->messages.is_empty() && send_queue->running) - send_queue->condition.wait(); - - if (!send_queue->running) { - send_queue->mutex.unlock(); + if (send_queue->block_until_message_enqueued() == SendQueue::Running::No) break; + + auto [bytes, fds] = send_queue->dequeue(4096); + ReadonlyBytes remaining_to_send_bytes = bytes; + + Threading::RWLockLocker lock(m_socket_rw_lock); + auto result = send_message(*m_socket, remaining_to_send_bytes, fds); + if (result.is_error()) { + dbgln("TransportSocket::send_thread: {}", result.error()); + VERIFY_NOT_REACHED(); } - auto [bytes, fds] = send_queue->messages.take_first(); - send_queue->mutex.unlock(); + if (!remaining_to_send_bytes.is_empty() || !fds.is_empty()) { + send_queue->return_unsent_data_to_front_of_queue(remaining_to_send_bytes, fds); + } - if (auto result = send_message(*m_socket, bytes, fds); result.is_error()) { - dbgln("TransportSocket::send_thread: {}", result.error()); + if (!m_socket->is_open()) + break; + + { + Vector pollfds; + pollfds.append({ .fd = m_socket->fd().value(), .events = POLLOUT, .revents = 0 }); + + ErrorOr result { 0 }; + do { + result = Core::System::poll(pollfds, -1); + } while (result.is_error() && result.error().code() == EINTR); } } return 0; @@ -45,32 +99,32 @@ TransportSocket::TransportSocket(NonnullOwnPtr socket) TransportSocket::~TransportSocket() { - { - Threading::MutexLocker locker(m_send_queue->mutex); - m_send_queue->running = false; - m_send_queue->condition.signal(); - } + m_send_queue->stop(); (void)m_send_thread->join(); } void TransportSocket::set_up_read_hook(Function hook) { + Threading::RWLockLocker lock(m_socket_rw_lock); VERIFY(m_socket->is_open()); m_socket->on_ready_to_read = move(hook); } bool TransportSocket::is_open() const { + Threading::RWLockLocker lock(m_socket_rw_lock); return m_socket->is_open(); } void TransportSocket::close() { + Threading::RWLockLocker lock(m_socket_rw_lock); m_socket->close(); } void TransportSocket::wait_until_readable() { + Threading::RWLockLocker lock(m_socket_rw_lock); auto maybe_did_become_readable = m_socket->can_read_without_blocking(-1); if (maybe_did_become_readable.is_error()) { dbgln("TransportSocket::wait_until_readable: {}", maybe_did_become_readable.error()); @@ -114,67 +168,39 @@ void TransportSocket::post_message(Vector const& bytes_to_write, Vectorenqueue_message(move(message_buffer), move(raw_fds)); } -void TransportSocket::queue_message_on_send_thread(MessageToSend&& message_to_send) const -{ - Threading::MutexLocker lock(m_send_queue->mutex); - m_send_queue->messages.append(move(message_to_send)); - m_send_queue->condition.signal(); -} - -ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes&& bytes_to_write, Vector const& unowned_fds) +ErrorOr TransportSocket::send_message(Core::LocalSocket& socket, ReadonlyBytes& bytes_to_write, Vector& unowned_fds) { auto num_fds_to_transfer = unowned_fds.size(); while (!bytes_to_write.is_empty()) { ErrorOr maybe_nwritten = 0; if (num_fds_to_transfer > 0) { maybe_nwritten = socket.send_message(bytes_to_write, 0, unowned_fds); - if (!maybe_nwritten.is_error()) - num_fds_to_transfer = 0; } else { maybe_nwritten = socket.write_some(bytes_to_write); } if (maybe_nwritten.is_error()) { - if (auto error = maybe_nwritten.release_error(); error.is_errno() && (error.code() == EAGAIN || error.code() == EWOULDBLOCK)) { - - // FIXME: Refactor this to pass the unwritten bytes back to the caller to send 'later' - // or next time the socket is writable - Vector pollfds; - if (pollfds.is_empty()) - pollfds.append({ .fd = socket.fd().value(), .events = POLLOUT, .revents = 0 }); - - ErrorOr result { 0 }; - do { - constexpr u32 POLL_TIMEOUT_MS = 100; - result = Core::System::poll(pollfds, POLL_TIMEOUT_MS); - } while (result.is_error() && result.error().code() == EINTR); - - if (!result.is_error() && result.value() != 0) - continue; - - switch (error.code()) { - case EPIPE: - return Error::from_string_literal("IPC::transfer_message: Disconnected from peer"); - case EAGAIN: - return Error::from_string_literal("IPC::transfer_message: Timed out waiting for socket to become writable"); - default: - return Error::from_syscall("IPC::transfer_message write"sv, -error.code()); - } + if (auto error = maybe_nwritten.release_error(); error.is_errno() && (error.code() == EAGAIN || error.code() == EWOULDBLOCK || error.code() == EINTR)) { + return {}; } else { return error; } } bytes_to_write = bytes_to_write.slice(maybe_nwritten.value()); + num_fds_to_transfer = 0; + unowned_fds.clear(); } return {}; } -TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) +TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possible_without_blocking(Function&& callback) { + Threading::RWLockLocker lock(m_socket_rw_lock); + bool should_shutdown = false; while (is_open()) { u8 buffer[4096]; @@ -222,7 +248,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib Message message; received_fd_count += header.fd_count; for (size_t i = 0; i < header.fd_count; ++i) - message.fds.append(m_unprocessed_fds.dequeue()); + message.fds.enqueue(m_unprocessed_fds.dequeue()); message.bytes.append(m_unprocessed_bytes.data() + index + sizeof(MessageHeader), header.payload_size); callback(move(message)); } else if (header.type == MessageHeader::Type::FileDescriptorAcknowledgement) { @@ -252,7 +278,7 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib header.fd_count = received_fd_count; header.type = MessageHeader::Type::FileDescriptorAcknowledgement; memcpy(message_buffer.data(), &header, sizeof(MessageHeader)); - queue_message_on_send_thread({ move(message_buffer), {} }); + m_send_queue->enqueue_message(move(message_buffer), {}); } if (index < m_unprocessed_bytes.size()) { @@ -267,11 +293,13 @@ TransportSocket::ShouldShutdown TransportSocket::read_as_many_messages_as_possib ErrorOr TransportSocket::release_underlying_transport_for_transfer() { + Threading::RWLockLocker lock(m_socket_rw_lock); return m_socket->release_fd(); } ErrorOr TransportSocket::clone_for_transfer() { + Threading::RWLockLocker lock(m_socket_rw_lock); return IPC::File::clone_fd(m_socket->fd().value()); } diff --git a/Libraries/LibIPC/TransportSocket.h b/Libraries/LibIPC/TransportSocket.h index 2c466d0f08c..e7a19f4e159 100644 --- a/Libraries/LibIPC/TransportSocket.h +++ b/Libraries/LibIPC/TransportSocket.h @@ -9,9 +9,9 @@ #include #include -#include #include #include +#include #include namespace IPC { @@ -42,6 +42,31 @@ private: int m_fd; }; +class SendQueue : public AtomicRefCounted { +public: + enum class Running { + No, + Yes, + }; + Running block_until_message_enqueued(); + void stop(); + + void enqueue_message(Vector&& bytes, Vector&& fds); + struct BytesAndFds { + Vector bytes; + Vector fds; + }; + BytesAndFds dequeue(size_t max_bytes); + void return_unsent_data_to_front_of_queue(ReadonlyBytes const& bytes, Vector const& fds); + +private: + Vector m_bytes; + Vector m_fds; + Threading::Mutex m_mutex; + Threading::ConditionVariable m_condition { m_mutex }; + bool m_running { true }; +}; + class TransportSocket { AK_MAKE_NONCOPYABLE(TransportSocket); AK_MAKE_NONMOVABLE(TransportSocket); @@ -66,9 +91,9 @@ public: }; struct Message { Vector bytes; - Vector fds; + Queue fds; }; - ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&& schedule_shutdown); + ShouldShutdown read_as_many_messages_as_possible_without_blocking(Function&&); // Obnoxious name to make it clear that this is a dangerous operation. ErrorOr release_underlying_transport_for_transfer(); @@ -76,30 +101,20 @@ public: ErrorOr clone_for_transfer(); private: - static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes&&, Vector const& unowned_fds); + static ErrorOr send_message(Core::LocalSocket&, ReadonlyBytes& bytes, Vector& unowned_fds); NonnullOwnPtr m_socket; + mutable Threading::RWLock m_socket_rw_lock; ByteBuffer m_unprocessed_bytes; - UnprocessedFileDescriptors m_unprocessed_fds; + Queue m_unprocessed_fds; // After file descriptor is sent, it is moved to the wait queue until an acknowledgement is received from the peer. // This is necessary to handle a specific behavior of the macOS kernel, which may prematurely garbage-collect the file // descriptor contained in the message before the peer receives it. https://openradar.me/9477351 Queue> m_fds_retained_until_received_by_peer; - struct MessageToSend { - Vector bytes; - Vector fds; - }; - struct SendQueue : public AtomicRefCounted { - AK::SinglyLinkedList messages; - Threading::Mutex mutex; - Threading::ConditionVariable condition { mutex }; - bool running { true }; - }; RefPtr m_send_thread; RefPtr m_send_queue; - void queue_message_on_send_thread(MessageToSend&&) const; }; } diff --git a/Libraries/LibIPC/UnprocessedFileDescriptors.h b/Libraries/LibIPC/UnprocessedFileDescriptors.h deleted file mode 100644 index 991c5598b8a..00000000000 --- a/Libraries/LibIPC/UnprocessedFileDescriptors.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2025, Aliaksandr Kalenik - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include - -namespace IPC { - -class UnprocessedFileDescriptors { -public: - void enqueue(File&& fd) - { - m_fds.append(move(fd)); - } - - File dequeue() - { - return m_fds.take_first(); - } - - void return_fds_to_front_of_queue(Vector&& fds) - { - m_fds.prepend(move(fds)); - } - - size_t size() const { return m_fds.size(); } - -private: - Vector m_fds; -}; - -} diff --git a/Libraries/LibJS/AST.cpp b/Libraries/LibJS/AST.cpp index 7e11a1d6bd6..1f0a40b8273 100644 --- a/Libraries/LibJS/AST.cpp +++ b/Libraries/LibJS/AST.cpp @@ -69,7 +69,7 @@ static void update_function_name(Value value, FlyString const& name) if (!value.is_function()) return; auto& function = value.as_function(); - if (is(function) && function.name().is_empty()) + if (is(function) && static_cast(function).name().is_empty()) static_cast(function).set_name(name); } diff --git a/Libraries/LibJS/Bytecode/Interpreter.cpp b/Libraries/LibJS/Bytecode/Interpreter.cpp index dea70a0957c..9e22a199a7b 100644 --- a/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -1212,14 +1212,14 @@ inline ThrowCompletionOr put_by_property_key(VM& vm, Value base, Value thi switch (kind) { case Op::PropertyKind::Getter: { auto& function = value.as_function(); - if (function.name().is_empty() && is(function)) + if (is(function) && static_cast(function).name().is_empty()) static_cast(&function)->set_name(MUST(String::formatted("get {}", name))); object->define_direct_accessor(name, &function, nullptr, Attribute::Configurable | Attribute::Enumerable); break; } case Op::PropertyKind::Setter: { auto& function = value.as_function(); - if (function.name().is_empty() && is(function)) + if (is(function) && static_cast(function).name().is_empty()) static_cast(&function)->set_name(MUST(String::formatted("set {}", name))); object->define_direct_accessor(name, nullptr, &function, Attribute::Configurable | Attribute::Enumerable); break; diff --git a/Libraries/LibJS/Runtime/Array.h b/Libraries/LibJS/Runtime/Array.h index 6ae77de113b..e30df2335e5 100644 --- a/Libraries/LibJS/Runtime/Array.h +++ b/Libraries/LibJS/Runtime/Array.h @@ -58,11 +58,16 @@ protected: explicit Array(Object& prototype); private: + virtual bool is_array_exotic_object() const final { return true; } + ThrowCompletionOr set_length(PropertyDescriptor const&); bool m_length_writable { true }; }; +template<> +inline bool Object::fast_is() const { return is_array_exotic_object(); } + enum class Holes { SkipHoles, ReadThroughHoles, diff --git a/Libraries/LibJS/Runtime/BoundFunction.cpp b/Libraries/LibJS/Runtime/BoundFunction.cpp index 0f799289b34..a8d4be7df0b 100644 --- a/Libraries/LibJS/Runtime/BoundFunction.cpp +++ b/Libraries/LibJS/Runtime/BoundFunction.cpp @@ -39,8 +39,6 @@ BoundFunction::BoundFunction(Realm& realm, FunctionObject& bound_target_function , m_bound_target_function(&bound_target_function) , m_bound_this(bound_this) , m_bound_arguments(move(bound_arguments)) - // FIXME: Non-standard and redundant, remove. - , m_name(MUST(String::formatted("bound {}", bound_target_function.name()))) { } diff --git a/Libraries/LibJS/Runtime/BoundFunction.h b/Libraries/LibJS/Runtime/BoundFunction.h index 7c48ebf4eed..9f90e3e7779 100644 --- a/Libraries/LibJS/Runtime/BoundFunction.h +++ b/Libraries/LibJS/Runtime/BoundFunction.h @@ -23,7 +23,6 @@ public: virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) override; virtual ThrowCompletionOr> internal_construct(ReadonlySpan arguments_list, FunctionObject& new_target) override; - virtual FlyString const& name() const override { return m_name; } virtual bool is_strict_mode() const override { return m_bound_target_function->is_strict_mode(); } virtual bool has_constructor() const override { return m_bound_target_function->has_constructor(); } @@ -39,8 +38,6 @@ private: GC::Ptr m_bound_target_function; // [[BoundTargetFunction]] Value m_bound_this; // [[BoundThis]] Vector m_bound_arguments; // [[BoundArguments]] - - FlyString m_name; }; } diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp index c35bdb5097f..4acb9be3b9d 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.cpp @@ -426,10 +426,9 @@ ECMAScriptFunctionObject::ECMAScriptFunctionObject( , m_shared_data(move(shared_data)) , m_environment(parent_environment) , m_private_environment(private_environment) - , m_realm(&prototype.shape().realm()) { if (!is_arrow_function() && kind() == FunctionKind::Normal) - unsafe_set_shape(m_realm->intrinsics().normal_function_shape()); + unsafe_set_shape(realm()->intrinsics().normal_function_shape()); // 15. Set F.[[ScriptOrModule]] to GetActiveScriptOrModule(). m_script_or_module = vm().get_active_script_or_module(); @@ -643,7 +642,6 @@ void ECMAScriptFunctionObject::visit_edges(Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_environment); visitor.visit(m_private_environment); - visitor.visit(m_realm); visitor.visit(m_home_object); visitor.visit(m_name_string); @@ -694,27 +692,13 @@ ThrowCompletionOr ECMAScriptFunctionObject::prepare_for_ordinary_call(Exec // 1. Let callerContext be the running execution context. // 2. Let calleeContext be a new ECMAScript code execution context. - // NOTE: In the specification, PrepareForOrdinaryCall "returns" a new callee execution context. - // To avoid heap allocations, we put our ExecutionContext objects on the C++ stack instead. - // Whoever calls us should put an ExecutionContext on their stack and pass that as the `callee_context`. - // 3. Set the Function of calleeContext to F. callee_context.function = this; callee_context.function_name = m_name_string; // 4. Let calleeRealm be F.[[Realm]]. - auto callee_realm = m_realm; - // NOTE: This non-standard fallback is needed until we can guarantee that literally - // every function has a realm - especially in LibWeb that's sometimes not the case - // when a function is created while no JS is running, as we currently need to rely on - // that (:acid2:, I know - see set_event_handler_attribute() for an example). - // If there's no 'current realm' either, we can't continue and crash. - if (!callee_realm) - callee_realm = vm.current_realm(); - VERIFY(callee_realm); - // 5. Set the Realm of calleeContext to calleeRealm. - callee_context.realm = callee_realm; + callee_context.realm = realm(); // 6. Set the ScriptOrModule of calleeContext to F.[[ScriptOrModule]]. callee_context.script_or_module = m_script_or_module; @@ -758,15 +742,7 @@ void ECMAScriptFunctionObject::ordinary_call_bind_this(ExecutionContext& callee_ return; // 3. Let calleeRealm be F.[[Realm]]. - auto callee_realm = m_realm; - // NOTE: This non-standard fallback is needed until we can guarantee that literally - // every function has a realm - especially in LibWeb that's sometimes not the case - // when a function is created while no JS is running, as we currently need to rely on - // that (:acid2:, I know - see set_event_handler_attribute() for an example). - // If there's no 'current realm' either, we can't continue and crash. - if (!callee_realm) - callee_realm = vm.current_realm(); - VERIFY(callee_realm); + auto callee_realm = realm(); // 4. Let localEnv be the LexicalEnvironment of calleeContext. auto local_env = callee_context.lexical_environment; diff --git a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h index 95b053996b1..0bfd51aa4dd 100644 --- a/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h +++ b/Libraries/LibJS/Runtime/ECMAScriptFunctionObject.h @@ -127,7 +127,7 @@ public: Statement const& ecmascript_code() const { return *shared_data().m_ecmascript_code; } [[nodiscard]] virtual FunctionParameters const& formal_parameters() const override { return *shared_data().m_formal_parameters; } - virtual FlyString const& name() const override { return shared_data().m_name; } + FlyString const& name() const { return shared_data().m_name; } void set_name(FlyString const& name); void set_is_class_constructor() { const_cast(shared_data()).m_is_class_constructor = true; } @@ -135,7 +135,7 @@ public: auto& bytecode_executable() const { return m_bytecode_executable; } Environment* environment() { return m_environment; } - virtual Realm* realm() const override { return m_realm; } + virtual Realm* realm() const override { return &shape().realm(); } [[nodiscard]] ConstructorKind constructor_kind() const { return shared_data().m_constructor_kind; } void set_constructor_kind(ConstructorKind constructor_kind) { const_cast(shared_data()).m_constructor_kind = constructor_kind; } @@ -210,7 +210,6 @@ private: // Internal Slots of ECMAScript Function Objects, https://tc39.es/ecma262/#table-internal-slots-of-ecmascript-function-objects GC::Ptr m_environment; // [[Environment]] GC::Ptr m_private_environment; // [[PrivateEnvironment]] - GC::Ptr m_realm; // [[Realm]] ScriptOrModule m_script_or_module; // [[ScriptOrModule]] GC::Ptr m_home_object; // [[HomeObject]] struct ClassData { diff --git a/Libraries/LibJS/Runtime/FunctionObject.h b/Libraries/LibJS/Runtime/FunctionObject.h index 6af855a0f30..5d94b9449fd 100644 --- a/Libraries/LibJS/Runtime/FunctionObject.h +++ b/Libraries/LibJS/Runtime/FunctionObject.h @@ -26,8 +26,6 @@ public: virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) = 0; virtual ThrowCompletionOr> internal_construct([[maybe_unused]] ReadonlySpan arguments_list, [[maybe_unused]] FunctionObject& new_target) { VERIFY_NOT_REACHED(); } - virtual FlyString const& name() const = 0; - void set_function_name(Variant const& name_arg, Optional const& prefix = {}); void set_function_length(double length); diff --git a/Libraries/LibJS/Runtime/FunctionPrototype.h b/Libraries/LibJS/Runtime/FunctionPrototype.h index e61e89fc7bd..493fcd53ca4 100644 --- a/Libraries/LibJS/Runtime/FunctionPrototype.h +++ b/Libraries/LibJS/Runtime/FunctionPrototype.h @@ -19,7 +19,6 @@ public: virtual ~FunctionPrototype() override = default; virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) override; - virtual FlyString const& name() const override { return m_name; } private: explicit FunctionPrototype(Realm&); @@ -29,9 +28,6 @@ private: JS_DECLARE_NATIVE_FUNCTION(call); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(symbol_has_instance); - - // 20.2.3: The Function prototype object has a "name" property whose value is the empty String. - FlyString m_name; }; } diff --git a/Libraries/LibJS/Runtime/NativeFunction.h b/Libraries/LibJS/Runtime/NativeFunction.h index 6fab066c68b..19875d56ad3 100644 --- a/Libraries/LibJS/Runtime/NativeFunction.h +++ b/Libraries/LibJS/Runtime/NativeFunction.h @@ -34,7 +34,7 @@ public: virtual ThrowCompletionOr call(); virtual ThrowCompletionOr> construct(FunctionObject& new_target); - virtual FlyString const& name() const override { return m_name; } + FlyString const& name() const { return m_name; } virtual bool is_strict_mode() const override; virtual bool has_constructor() const override { return false; } virtual Realm* realm() const override { return m_realm; } diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index 34fce27b8f0..0225c30d7cf 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -198,6 +198,7 @@ public: virtual bool is_regexp_object() const { return false; } virtual bool is_bigint_object() const { return false; } virtual bool is_string_object() const { return false; } + virtual bool is_array_exotic_object() const { return false; } virtual bool is_global_object() const { return false; } virtual bool is_proxy_object() const { return false; } virtual bool is_native_function() const { return false; } diff --git a/Libraries/LibJS/Runtime/PrimitiveString.cpp b/Libraries/LibJS/Runtime/PrimitiveString.cpp index a161042e2e5..1c6d8d5a56e 100644 --- a/Libraries/LibJS/Runtime/PrimitiveString.cpp +++ b/Libraries/LibJS/Runtime/PrimitiveString.cpp @@ -106,6 +106,17 @@ Utf16View PrimitiveString::utf16_string_view() const return m_utf16_string->view(); } +bool PrimitiveString::operator==(PrimitiveString const& other) const +{ + if (this == &other) + return true; + if (m_utf8_string.has_value() && other.m_utf8_string.has_value()) + return m_utf8_string->bytes_as_string_view() == other.m_utf8_string->bytes_as_string_view(); + if (m_utf16_string.has_value() && other.m_utf16_string.has_value()) + return m_utf16_string->string() == other.m_utf16_string->string(); + return utf8_string_view() == other.utf8_string_view(); +} + ThrowCompletionOr> PrimitiveString::get(VM& vm, PropertyKey const& property_key) const { if (property_key.is_symbol()) diff --git a/Libraries/LibJS/Runtime/PrimitiveString.h b/Libraries/LibJS/Runtime/PrimitiveString.h index 4fefbd785c2..7be62544c5e 100644 --- a/Libraries/LibJS/Runtime/PrimitiveString.h +++ b/Libraries/LibJS/Runtime/PrimitiveString.h @@ -47,6 +47,8 @@ public: ThrowCompletionOr> get(VM&, PropertyKey const&) const; + [[nodiscard]] bool operator==(PrimitiveString const&) const; + protected: enum class RopeTag { Rope }; explicit PrimitiveString(RopeTag) diff --git a/Libraries/LibJS/Runtime/ProxyObject.cpp b/Libraries/LibJS/Runtime/ProxyObject.cpp index 60762d8e254..318742043ed 100644 --- a/Libraries/LibJS/Runtime/ProxyObject.cpp +++ b/Libraries/LibJS/Runtime/ProxyObject.cpp @@ -897,10 +897,4 @@ void ProxyObject::visit_edges(Cell::Visitor& visitor) visitor.visit(m_handler); } -FlyString const& ProxyObject::name() const -{ - VERIFY(is_function()); - return static_cast(*m_target).name(); -} - } diff --git a/Libraries/LibJS/Runtime/ProxyObject.h b/Libraries/LibJS/Runtime/ProxyObject.h index e1cf4e73d0b..e52cda49db1 100644 --- a/Libraries/LibJS/Runtime/ProxyObject.h +++ b/Libraries/LibJS/Runtime/ProxyObject.h @@ -21,7 +21,6 @@ public: virtual ~ProxyObject() override = default; - virtual FlyString const& name() const override; virtual bool has_constructor() const override; Object const& target() const { return m_target; } diff --git a/Libraries/LibJS/Runtime/VM.cpp b/Libraries/LibJS/Runtime/VM.cpp index 53e11d7d5b9..2bf87066011 100644 --- a/Libraries/LibJS/Runtime/VM.cpp +++ b/Libraries/LibJS/Runtime/VM.cpp @@ -759,13 +759,6 @@ void VM::pop_execution_context() on_call_stack_emptied(); } -#if ARCH(X86_64) -struct [[gnu::packed]] NativeStackFrame { - NativeStackFrame* prev; - FlatPtr return_address; -}; -#endif - static RefPtr get_source_range(ExecutionContext const* context) { // native function diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index 555dab57828..eedf42c81f3 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -926,6 +926,26 @@ ThrowCompletionOr Value::to_property_key(VM& vm) const return MUST(key.to_string(vm)); } +// 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 +ThrowCompletionOr Value::to_i32(VM& vm) const +{ + if (is_int32()) + return as_i32(); + +#if __has_builtin(__builtin_arm_jcvt) + if (is_double()) + return __builtin_arm_jcvt(m_value.as_double); +#endif + + return to_i32_slow_case(vm); +} + +// 7.1.7 ToUint32 ( argument ), https://tc39.es/ecma262/#sec-touint32 +ThrowCompletionOr Value::to_u32(VM& vm) const +{ + return static_cast(TRY(to_i32(vm))); +} + // 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const { @@ -934,6 +954,9 @@ ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const // 1. Let number be ? ToNumber(argument). double number = TRY(to_number(vm)).as_double(); +#if __has_builtin(__builtin_arm_jcvt) + return __builtin_arm_jcvt(number); +#else // 2. If number is not finite or number is either +0𝔽 or -0𝔽, return +0𝔽. if (!isfinite(number) || number == 0) return 0; @@ -951,42 +974,7 @@ ThrowCompletionOr Value::to_i32_slow_case(VM& vm) const if (int32bit >= 2147483648.0) int32bit -= 4294967296.0; return static_cast(int32bit); -} - -// 7.1.6 ToInt32 ( argument ), https://tc39.es/ecma262/#sec-toint32 -ThrowCompletionOr Value::to_i32(VM& vm) const -{ - if (is_int32()) - return as_i32(); - return to_i32_slow_case(vm); -} - -// 7.1.7 ToUint32 ( argument ), https://tc39.es/ecma262/#sec-touint32 -ThrowCompletionOr Value::to_u32(VM& vm) const -{ - // OPTIMIZATION: If this value is encoded as a positive i32, return it directly. - if (is_int32() && as_i32() >= 0) - return as_i32(); - - // 1. Let number be ? ToNumber(argument). - double number = TRY(to_number(vm)).as_double(); - - // 2. If number is not finite or number is either +0𝔽 or -0𝔽, return +0𝔽. - if (!isfinite(number) || number == 0) - return 0; - - // 3. Let int be the mathematical value whose sign is the sign of number and whose magnitude is floor(abs(ℝ(number))). - auto int_val = floor(fabs(number)); - if (signbit(number)) - int_val = -int_val; - - // 4. Let int32bit be int modulo 2^32. - auto int32bit = modulo(int_val, NumericLimits::max() + 1.0); - - // 5. Return 𝔽(int32bit). - // Cast to i64 here to ensure that the double --> u32 cast doesn't invoke undefined behavior - // Otherwise, negative numbers cause a UBSAN warning. - return static_cast(static_cast(int32bit)); +#endif } // 7.1.8 ToInt16 ( argument ), https://tc39.es/ecma262/#sec-toint16 @@ -2237,7 +2225,7 @@ bool same_value_non_number(Value lhs, Value rhs) // 5. If x is a String, then if (lhs.is_string()) { // a. If x and y are exactly the same sequence of code units (same length and same code units at corresponding indices), return true; otherwise, return false. - return lhs.as_string().utf8_string_view() == rhs.as_string().utf8_string_view(); + return lhs.as_string() == rhs.as_string(); } // 3. If x is undefined, return true. diff --git a/Libraries/LibJS/Runtime/WrappedFunction.h b/Libraries/LibJS/Runtime/WrappedFunction.h index 93291000794..d29e67db66a 100644 --- a/Libraries/LibJS/Runtime/WrappedFunction.h +++ b/Libraries/LibJS/Runtime/WrappedFunction.h @@ -22,9 +22,6 @@ public: virtual ThrowCompletionOr internal_call(Value this_argument, ReadonlySpan arguments_list) override; - // FIXME: Remove this (and stop inventing random internal slots that shouldn't exist, jeez) - virtual FlyString const& name() const override { return m_wrapped_target_function->name(); } - virtual Realm* realm() const override { return m_realm; } FunctionObject const& wrapped_target_function() const { return m_wrapped_target_function; } diff --git a/Libraries/LibTest/TestSuite.cpp b/Libraries/LibTest/TestSuite.cpp index 4b8e0977059..0a2971cf87a 100644 --- a/Libraries/LibTest/TestSuite.cpp +++ b/Libraries/LibTest/TestSuite.cpp @@ -25,9 +25,9 @@ public: void restart() { m_started = UnixDateTime::now(); } - u64 elapsed_milliseconds() + AK::Duration elapsed() const { - return (UnixDateTime::now() - m_started).to_milliseconds(); + return UnixDateTime::now() - m_started; } private: @@ -187,17 +187,18 @@ int TestSuite::run(Vector> const& tests) m_current_test_result = TestResult::NotRun; enable_reporting(); - u64 total_time = 0; + AK::Duration total_time; u64 sum_of_squared_times = 0; - u64 min_time = NumericLimits::max(); - u64 max_time = 0; + AK::Duration min_time = AK::Duration::max(); + AK::Duration max_time; for (u64 i = 0; i < repetitions; ++i) { TestElapsedTimer timer; t->func()(); - auto const iteration_time = timer.elapsed_milliseconds(); + auto const iteration_time = timer.elapsed(); + auto const iteration_ms = iteration_time.to_milliseconds(); total_time += iteration_time; - sum_of_squared_times += iteration_time * iteration_time; + sum_of_squared_times += iteration_ms * iteration_ms; min_time = min(min_time, iteration_time); max_time = max(max_time, iteration_time); @@ -206,20 +207,26 @@ int TestSuite::run(Vector> const& tests) m_current_test_result = TestResult::Passed; } + auto const total_time_ms = total_time.to_milliseconds(); + if (repetitions != 1) { - double average = total_time / double(repetitions); + double average = total_time_ms / static_cast(repetitions); double average_squared = average * average; - double standard_deviation = sqrt((sum_of_squared_times + repetitions * average_squared - 2 * total_time * average) / (repetitions - 1)); + double standard_deviation = sqrt((sum_of_squared_times + repetitions * average_squared - 2 * total_time_ms * average) / (repetitions - 1)); dbgln("{} {} '{}' on average in {:.1f}±{:.1f}ms (min={}ms, max={}ms, total={}ms)", test_result_to_string(m_current_test_result), test_type, t->name(), - average, standard_deviation, min_time, max_time, total_time); + average, + standard_deviation, + min_time.to_milliseconds(), + max_time.to_milliseconds(), + total_time_ms); } else { - dbgln("{} {} '{}' in {}ms", test_result_to_string(m_current_test_result), test_type, t->name(), total_time); + dbgln("{} {} '{}' in {}ms", test_result_to_string(m_current_test_result), test_type, t->name(), total_time_ms); } if (t->is_benchmark()) { - m_benchtime += total_time; + m_bench_time += total_time; benchmark_count++; switch (m_current_test_result) { @@ -233,7 +240,7 @@ int TestSuite::run(Vector> const& tests) break; } } else { - m_testtime += total_time; + m_test_time += total_time; test_count++; switch (m_current_test_result) { @@ -249,13 +256,16 @@ int TestSuite::run(Vector> const& tests) } } + auto const runtime = m_test_time + m_bench_time; + auto const elapsed = global_timer.elapsed() - runtime; + dbgln("Finished {} tests and {} benchmarks in {}ms ({}ms tests, {}ms benchmarks, {}ms other).", test_count, benchmark_count, - global_timer.elapsed_milliseconds(), - m_testtime, - m_benchtime, - global_timer.elapsed_milliseconds() - (m_testtime + m_benchtime)); + elapsed.to_truncated_milliseconds(), + m_test_time.to_truncated_milliseconds(), + m_bench_time.to_truncated_milliseconds(), + (elapsed - runtime).to_truncated_milliseconds()); if (test_count != 0) { if (test_passed_count == test_count) { diff --git a/Libraries/LibTest/TestSuite.h b/Libraries/LibTest/TestSuite.h index abd39ee47d0..c293a771826 100644 --- a/Libraries/LibTest/TestSuite.h +++ b/Libraries/LibTest/TestSuite.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -65,8 +66,8 @@ public: private: static TestSuite* s_global; Vector> m_cases; - u64 m_testtime = 0; - u64 m_benchtime = 0; + AK::Duration m_test_time; + AK::Duration m_bench_time; ByteString m_suite_name; u64 m_benchmark_repetitions = 1; u64 m_randomized_runs = 100; diff --git a/Libraries/LibURL/URL.cpp b/Libraries/LibURL/URL.cpp index ec7932199e6..8a82f790854 100644 --- a/Libraries/LibURL/URL.cpp +++ b/Libraries/LibURL/URL.cpp @@ -246,6 +246,18 @@ String URL::serialize_path() const return output.to_string_without_validation(); } +// This function is used whenever a path is needed to access the actual file on disk. +// On Windows serialize_path can produce a path like /C:/path/to/tst.htm, so the leading slash needs to be removed to obtain a valid path. +ByteString URL::file_path() const +{ + ByteString path = percent_decode(serialize_path()); +#ifdef AK_OS_WINDOWS + if (path.starts_with('/')) + path = path.substring(1); +#endif + return path; +} + // https://url.spec.whatwg.org/#concept-url-serializer String URL::serialize(ExcludeFragment exclude_fragment) const { diff --git a/Libraries/LibURL/URL.h b/Libraries/LibURL/URL.h index ad5e60f8bd7..88ec9f965a8 100644 --- a/Libraries/LibURL/URL.h +++ b/Libraries/LibURL/URL.h @@ -122,6 +122,7 @@ public: } String serialize_path() const; + ByteString file_path() const; String serialize(ExcludeFragment = ExcludeFragment::No) const; ByteString serialize_for_display() const; ByteString to_byte_string() const { return serialize().to_byte_string(); } diff --git a/Libraries/LibUnicode/Segmenter.cpp b/Libraries/LibUnicode/Segmenter.cpp index db66f9e5516..63023995d88 100644 --- a/Libraries/LibUnicode/Segmenter.cpp +++ b/Libraries/LibUnicode/Segmenter.cpp @@ -87,15 +87,13 @@ public: virtual Optional previous_boundary(size_t boundary, Inclusive inclusive) override { auto icu_boundary = align_boundary(boundary); - if (!icu_boundary.has_value()) - return {}; if (inclusive == Inclusive::Yes) { - if (static_cast(m_segmenter->isBoundary(*icu_boundary))) - return static_cast(*icu_boundary); + if (static_cast(m_segmenter->isBoundary(icu_boundary))) + return static_cast(icu_boundary); } - if (auto index = m_segmenter->preceding(*icu_boundary); index != icu::BreakIterator::DONE) + if (auto index = m_segmenter->preceding(icu_boundary); index != icu::BreakIterator::DONE) return static_cast(index); return {}; @@ -104,15 +102,13 @@ public: virtual Optional next_boundary(size_t boundary, Inclusive inclusive) override { auto icu_boundary = align_boundary(boundary); - if (!icu_boundary.has_value()) - return {}; if (inclusive == Inclusive::Yes) { - if (static_cast(m_segmenter->isBoundary(*icu_boundary))) - return static_cast(*icu_boundary); + if (static_cast(m_segmenter->isBoundary(icu_boundary))) + return static_cast(icu_boundary); } - if (auto index = m_segmenter->following(*icu_boundary); index != icu::BreakIterator::DONE) + if (auto index = m_segmenter->following(icu_boundary); index != icu::BreakIterator::DONE) return static_cast(index); return {}; @@ -177,25 +173,25 @@ public: } private: - Optional align_boundary(size_t boundary) + i32 align_boundary(size_t boundary) { auto icu_boundary = static_cast(boundary); return m_segmented_text.visit( - [&](String const& text) -> Optional { + [&](String const& text) { if (boundary >= text.byte_count()) - return {}; + return static_cast(text.byte_count()); U8_SET_CP_START(text.bytes().data(), 0, icu_boundary); return icu_boundary; }, - [&](icu::UnicodeString const& text) -> Optional { + [&](icu::UnicodeString const& text) { if (icu_boundary >= text.length()) - return {}; + return text.length(); return text.getChar32Start(icu_boundary); }, - [](Empty) -> Optional { VERIFY_NOT_REACHED(); }); + [](Empty) -> i32 { VERIFY_NOT_REACHED(); }); } void for_each_boundary(SegmentationCallback callback) diff --git a/Libraries/LibWeb/Bindings/ImageConstructor.cpp b/Libraries/LibWeb/Bindings/ImageConstructor.cpp index 91449b8435c..8111de81942 100644 --- a/Libraries/LibWeb/Bindings/ImageConstructor.cpp +++ b/Libraries/LibWeb/Bindings/ImageConstructor.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 62f8c2c9985..94af3e8fd4a 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -195,6 +195,7 @@ set(SOURCES CSS/Time.cpp CSS/Transformation.cpp CSS/TransitionEvent.cpp + CSS/URL.cpp CSS/VisualViewport.cpp Cookie/Cookie.cpp Cookie/ParsedCookie.cpp @@ -566,6 +567,7 @@ set(SOURCES IndexedDB/IDBVersionChangeEvent.cpp IndexedDB/Internal/Algorithms.cpp IndexedDB/Internal/Database.cpp + IndexedDB/Internal/Index.cpp IndexedDB/Internal/Key.cpp IndexedDB/Internal/ObjectStore.cpp IndexedDB/Internal/RequestList.cpp @@ -689,6 +691,7 @@ set(SOURCES Painting/SVGPaintable.cpp Painting/SVGSVGPaintable.cpp Painting/ScrollFrame.cpp + Painting/ScrollState.cpp Painting/ShadowPainting.cpp Painting/StackingContext.cpp Painting/TableBordersPainting.cpp diff --git a/Libraries/LibWeb/CSS/CSSAnimation.cpp b/Libraries/LibWeb/CSS/CSSAnimation.cpp index 3a431da28b5..19a66c2e86d 100644 --- a/Libraries/LibWeb/CSS/CSSAnimation.cpp +++ b/Libraries/LibWeb/CSS/CSSAnimation.cpp @@ -24,7 +24,7 @@ Optional CSSAnimation::class_specific_composite_order(GC::Ref(*other_animation) }; - // The existance of an owning element determines the animation class, so both animations should have their owning + // The existence of an owning element determines the animation class, so both animations should have their owning // element in the same state VERIFY(!owning_element() == !other->owning_element()); diff --git a/Libraries/LibWeb/CSS/CSSImportRule.cpp b/Libraries/LibWeb/CSS/CSSImportRule.cpp index 49f61e89b56..ea5773c50bb 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.cpp +++ b/Libraries/LibWeb/CSS/CSSImportRule.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -17,23 +16,23 @@ #include #include #include +#include #include namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSImportRule); -GC::Ref CSSImportRule::create(URL::URL url, DOM::Document& document, RefPtr supports, Vector> media_query_list) +GC::Ref CSSImportRule::create(JS::Realm& realm, URL url, GC::Ptr document, RefPtr supports, Vector> media_query_list) { - auto& realm = document.realm(); - return realm.create(move(url), document, supports, move(media_query_list)); + return realm.create(realm, move(url), document, move(supports), move(media_query_list)); } -CSSImportRule::CSSImportRule(URL::URL url, DOM::Document& document, RefPtr supports, Vector> media_query_list) - : CSSRule(document.realm(), Type::Import) +CSSImportRule::CSSImportRule(JS::Realm& realm, URL url, GC::Ptr document, RefPtr supports, Vector> media_query_list) + : CSSRule(realm, Type::Import) , m_url(move(url)) , m_document(document) - , m_supports(supports) + , m_supports(move(supports)) , m_media_query_list(move(media_query_list)) { } @@ -57,7 +56,10 @@ void CSSImportRule::set_parent_style_sheet(CSSStyleSheet* parent_style_sheet) // Crude detection of whether we're already fetching. if (m_style_sheet || m_document_load_event_delayer.has_value()) return; - fetch(); + + // Only try to fetch if we now have a parent + if (parent_style_sheet) + fetch(); } // https://www.w3.org/TR/cssom/#serialize-a-css-rule @@ -70,7 +72,7 @@ String CSSImportRule::serialized() const builder.append("@import "sv); // 2. The result of performing serialize a URL on the rule’s location. - serialize_a_url(builder, m_url.to_string()); + builder.append(m_url.to_string()); // AD-HOC: Serialize the rule's supports condition if it exists. // This isn't currently specified, but major browsers include this in their serialization of import rules @@ -104,15 +106,18 @@ void CSSImportRule::fetch() // 3. Let parsedUrl be the result of the URL parser steps with rule’s URL and parentStylesheet’s location. // If the algorithm returns an error, return. [CSSOM] - // FIXME: Stop producing a URL::URL when parsing the @import - auto parsed_url = url().to_string(); + auto parsed_url = DOMURL::parse(href(), parent_style_sheet.location()); + if (!parsed_url.has_value()) { + dbgln("Unable to parse @import url `{}` parent location `{}` as a URL.", href(), parent_style_sheet.location()); + return; + } // FIXME: Figure out the "correct" way to delay the load event. m_document_load_event_delayer.emplace(*m_document); // 4. Fetch a style resource from parsedUrl, with stylesheet parentStylesheet, destination "style", CORS mode "no-cors", and processResponse being the following steps given response response and byte stream, null or failure byteStream: - fetch_a_style_resource(parsed_url, parent_style_sheet, Fetch::Infrastructure::Request::Destination::Style, CorsMode::NoCors, - [strong_this = GC::Ref { *this }, parent_style_sheet = GC::Ref { parent_style_sheet }](auto response, auto maybe_byte_stream) { + fetch_a_style_resource(parsed_url->to_string(), parent_style_sheet, Fetch::Infrastructure::Request::Destination::Style, CorsMode::NoCors, + [strong_this = GC::Ref { *this }, parent_style_sheet = GC::Ref { parent_style_sheet }, parsed_url = parsed_url.value()](auto response, auto maybe_byte_stream) { // AD-HOC: Stop delaying the load event. ScopeGuard guard = [strong_this] { strong_this->m_document_load_event_delayer.clear(); @@ -139,19 +144,19 @@ void CSSImportRule::fetch() auto encoding = "utf-8"sv; auto maybe_decoder = TextCodec::decoder_for(encoding); if (!maybe_decoder.has_value()) { - dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Unsupported encoding: {}", strong_this->url(), encoding); + dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Unsupported encoding: {}", parsed_url, encoding); return; } auto& decoder = maybe_decoder.release_value(); auto decoded_or_error = TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(decoder, *byte_stream); if (decoded_or_error.is_error()) { - dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Encoding was: {}", strong_this->url(), encoding); + dbgln_if(CSS_LOADER_DEBUG, "CSSImportRule: Failed to decode CSS file: {} Encoding was: {}", parsed_url, encoding); return; } auto decoded = decoded_or_error.release_value(); - auto* imported_style_sheet = parse_css_stylesheet(Parser::ParsingParams(*strong_this->m_document, strong_this->url()), decoded, strong_this->url(), strong_this->m_media_query_list); + auto* imported_style_sheet = parse_css_stylesheet(Parser::ParsingParams(*strong_this->m_document, parsed_url), decoded, parsed_url, strong_this->m_media_query_list); // 5. Set importedStylesheet’s origin-clean flag to parentStylesheet’s origin-clean flag. imported_style_sheet->set_origin_clean(parent_style_sheet->is_origin_clean()); diff --git a/Libraries/LibWeb/CSS/CSSImportRule.h b/Libraries/LibWeb/CSS/CSSImportRule.h index a3712ed3bfa..a9f3abb3480 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.h +++ b/Libraries/LibWeb/CSS/CSSImportRule.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2021, the SerenityOS developers. - * Copyright (c) 2021-2024, Sam Atkins + * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2022, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause @@ -8,9 +8,9 @@ #pragma once -#include #include #include +#include #include namespace Web::CSS { @@ -21,13 +21,12 @@ class CSSImportRule final GC_DECLARE_ALLOCATOR(CSSImportRule); public: - [[nodiscard]] static GC::Ref create(URL::URL, DOM::Document&, RefPtr, Vector>); + [[nodiscard]] static GC::Ref create(JS::Realm&, URL, GC::Ptr, RefPtr, Vector>); virtual ~CSSImportRule() = default; - URL::URL const& url() const { return m_url; } - // FIXME: This should return only the specified part of the url. eg, "stuff/foo.css", not "https://example.com/stuff/foo.css". - String href() const { return m_url.to_string(); } + URL const& url() const { return m_url; } + String href() const { return m_url.url(); } CSSStyleSheet* loaded_style_sheet() { return m_style_sheet; } CSSStyleSheet const* loaded_style_sheet() const { return m_style_sheet; } @@ -37,7 +36,7 @@ public: Optional supports_text() const; private: - CSSImportRule(URL::URL, DOM::Document&, RefPtr, Vector>); + CSSImportRule(JS::Realm&, URL, GC::Ptr, RefPtr, Vector>); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -49,7 +48,7 @@ private: void fetch(); void set_style_sheet(GC::Ref); - URL::URL m_url; + URL m_url; GC::Ptr m_document; RefPtr m_supports; Vector> m_media_query_list; diff --git a/Libraries/LibWeb/CSS/CSSImportRule.idl b/Libraries/LibWeb/CSS/CSSImportRule.idl index 2256427abd2..c16a272a42b 100644 --- a/Libraries/LibWeb/CSS/CSSImportRule.idl +++ b/Libraries/LibWeb/CSS/CSSImportRule.idl @@ -6,7 +6,8 @@ [Exposed=Window] interface CSSImportRule : CSSRule { readonly attribute USVString href; - [SameObject, PutForwards=mediaText] readonly attribute MediaList media; + // AD-HOC: media is null if styleSheet is null. Spec issue: https://github.com/w3c/csswg-drafts/issues/12063 + [SameObject, PutForwards=mediaText] readonly attribute MediaList? media; [SameObject, ImplementedAs=style_sheet_for_bindings] readonly attribute CSSStyleSheet? styleSheet; [FIXME] readonly attribute CSSOMString? layerName; readonly attribute CSSOMString? supportsText; diff --git a/Libraries/LibWeb/CSS/CSSStyleProperties.cpp b/Libraries/LibWeb/CSS/CSSStyleProperties.cpp index 64f622cda43..3af27b22b93 100644 --- a/Libraries/LibWeb/CSS/CSSStyleProperties.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleProperties.cpp @@ -1158,6 +1158,13 @@ WebIDL::ExceptionOr CSSStyleProperties::set_css_text(StringView css_text) // 4. Update style attribute for the CSS declaration block. update_style_attribute(); + // Non-standard: Invalidate style for the owners of our containing sheet, if any. + if (auto rule = parent_rule()) { + if (auto sheet = rule->parent_style_sheet()) { + sheet->invalidate_owners(DOM::StyleInvalidationReason::CSSStylePropertiesTextChange); + } + } + return {}; } diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp index 0500e1989a0..f79ab359ae0 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.cpp @@ -24,7 +24,7 @@ namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSStyleSheet); -GC::Ref CSSStyleSheet::create(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional location) +GC::Ref CSSStyleSheet::create(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional<::URL::URL> location) { return realm.create(realm, rules, media, move(location)); } @@ -37,16 +37,16 @@ WebIDL::ExceptionOr> CSSStyleSheet::construct_impl(JS::Re // 2. Set sheet’s location to the base URL of the associated Document for the current principal global object. auto associated_document = as(HTML::current_principal_global_object()).document(); - sheet->set_location(associated_document->base_url().to_string()); + sheet->set_location(associated_document->base_url()); // 3. Set sheet’s stylesheet base URL to the baseURL attribute value from options. if (options.has_value() && options->base_url.has_value()) { - Optional sheet_location_url; + Optional<::URL::URL> sheet_location_url; if (sheet->location().has_value()) - sheet_location_url = URL::Parser::basic_parse(sheet->location().release_value()); + sheet_location_url = sheet->location().release_value(); // AD-HOC: This isn't explicitly mentioned in the specification, but multiple modern browsers do this. - Optional url = sheet->location().has_value() ? sheet_location_url->complete_url(options->base_url.value()) : URL::Parser::basic_parse(options->base_url.value()); + Optional<::URL::URL> url = sheet->location().has_value() ? sheet_location_url->complete_url(options->base_url.value()) : ::URL::Parser::basic_parse(options->base_url.value()); if (!url.has_value()) return WebIDL::NotAllowedError::create(realm, "Constructed style sheets must have a valid base URL"_string); @@ -95,12 +95,12 @@ WebIDL::ExceptionOr> CSSStyleSheet::construct_impl(JS::Re return sheet; } -CSSStyleSheet::CSSStyleSheet(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional location) +CSSStyleSheet::CSSStyleSheet(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional<::URL::URL> location) : StyleSheet(realm, media) , m_rules(&rules) { if (location.has_value()) - set_location(location->to_string()); + set_location(move(location)); for (auto& rule : *m_rules) rule->set_parent_style_sheet(this); @@ -140,8 +140,7 @@ WebIDL::ExceptionOr CSSStyleSheet::insert_rule(StringView rule, unsign return WebIDL::NotAllowedError::create(realm(), "Can't call insert_rule() on non-modifiable stylesheets."_string); // 3. Let parsed rule be the return value of invoking parse a rule with rule. - auto context = !m_owning_documents_or_shadow_roots.is_empty() ? Parser::ParsingParams { (*m_owning_documents_or_shadow_roots.begin())->document() } : Parser::ParsingParams { realm() }; - auto parsed_rule = parse_css_rule(context, rule); + auto parsed_rule = parse_css_rule(make_parsing_params(), rule); // 4. If parsed rule is a syntax error, return parsed rule. if (!parsed_rule) @@ -208,8 +207,7 @@ GC::Ref CSSStyleSheet::replace(String text) HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 1. Let rules be the result of running parse a stylesheet’s contents from text. - auto context = !m_owning_documents_or_shadow_roots.is_empty() ? Parser::ParsingParams { (*m_owning_documents_or_shadow_roots.begin())->document() } : CSS::Parser::ParsingParams { realm }; - auto* parsed_stylesheet = parse_css_stylesheet(context, text); + auto* parsed_stylesheet = parse_css_stylesheet(make_parsing_params(), text); auto& rules = parsed_stylesheet->rules(); // 2. If rules contains one or more @import rules, remove those rules from rules. @@ -242,8 +240,7 @@ WebIDL::ExceptionOr CSSStyleSheet::replace_sync(StringView text) return WebIDL::NotAllowedError::create(realm(), "Can't call replaceSync() on non-modifiable stylesheets"_string); // 2. Let rules be the result of running parse a stylesheet’s contents from text. - auto context = !m_owning_documents_or_shadow_roots.is_empty() ? Parser::ParsingParams { (*m_owning_documents_or_shadow_roots.begin())->document() } : CSS::Parser::ParsingParams { realm() }; - auto* parsed_stylesheet = parse_css_stylesheet(context, text); + auto* parsed_stylesheet = parse_css_stylesheet(make_parsing_params(), text); auto& rules = parsed_stylesheet->rules(); // 3. If rules contains one or more @import rules, remove those rules from rules. @@ -426,4 +423,11 @@ bool CSSStyleSheet::has_associated_font_loader(FontLoader& font_loader) const return false; } +Parser::ParsingParams CSSStyleSheet::make_parsing_params() const +{ + if (!m_owning_documents_or_shadow_roots.is_empty()) + return Parser::ParsingParams { (*m_owning_documents_or_shadow_roots.begin())->document() }; + return Parser::ParsingParams { realm() }; +} + } diff --git a/Libraries/LibWeb/CSS/CSSStyleSheet.h b/Libraries/LibWeb/CSS/CSSStyleSheet.h index b3c93b102b6..ecb4ece8e77 100644 --- a/Libraries/LibWeb/CSS/CSSStyleSheet.h +++ b/Libraries/LibWeb/CSS/CSSStyleSheet.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -27,12 +28,13 @@ struct CSSStyleSheetInit { bool disabled { false }; }; +// https://drafts.csswg.org/cssom-1/#cssstylesheet class CSSStyleSheet final : public StyleSheet { WEB_PLATFORM_OBJECT(CSSStyleSheet, StyleSheet); GC_DECLARE_ALLOCATOR(CSSStyleSheet); public: - [[nodiscard]] static GC::Ref create(JS::Realm&, CSSRuleList&, MediaList&, Optional location); + [[nodiscard]] static GC::Ref create(JS::Realm&, CSSRuleList&, MediaList&, Optional<::URL::URL> location); static WebIDL::ExceptionOr> construct_impl(JS::Realm&, Optional const& options = {}); virtual ~CSSStyleSheet() override = default; @@ -74,8 +76,8 @@ public: Vector> const& import_rules() const { return m_import_rules; } - Optional base_url() const { return m_base_url; } - void set_base_url(Optional base_url) { m_base_url = move(base_url); } + Optional<::URL::URL> base_url() const { return m_base_url; } + void set_base_url(Optional<::URL::URL> base_url) { m_base_url = move(base_url); } bool constructed() const { return m_constructed; } @@ -94,7 +96,7 @@ public: bool has_associated_font_loader(FontLoader& font_loader) const; private: - CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional location); + CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional<::URL::URL> location); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -104,6 +106,8 @@ private: void set_constructed(bool constructed) { m_constructed = constructed; } void set_disallow_modification(bool disallow_modification) { m_disallow_modification = disallow_modification; } + Parser::ParsingParams make_parsing_params() const; + Optional m_source_text; GC::Ptr m_rules; @@ -113,7 +117,7 @@ private: GC::Ptr m_owner_css_rule; - Optional m_base_url; + Optional<::URL::URL> m_base_url; GC::Ptr m_constructor_document; HashTable> m_owning_documents_or_shadow_roots; bool m_constructed { false }; diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 1498ee729df..115fabef478 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -225,40 +225,40 @@ public: : m_value(color) { } - SVGPaint(URL::URL const& url) + SVGPaint(::URL::URL const& url) : m_value(url) { } bool is_color() const { return m_value.has(); } - bool is_url() const { return m_value.has(); } + bool is_url() const { return m_value.has<::URL::URL>(); } Color as_color() const { return m_value.get(); } - URL::URL const& as_url() const { return m_value.get(); } + ::URL::URL const& as_url() const { return m_value.get<::URL::URL>(); } private: - Variant m_value; + Variant<::URL::URL, Color> m_value; }; // https://drafts.fxtf.org/css-masking-1/#typedef-mask-reference class MaskReference { public: // TODO: Support other mask types. - MaskReference(URL::URL const& url) + MaskReference(::URL::URL const& url) : m_url(url) { } - URL::URL const& url() const { return m_url; } + ::URL::URL const& url() const { return m_url; } private: - URL::URL m_url; + ::URL::URL m_url; }; // https://drafts.fxtf.org/css-masking/#the-clip-path // TODO: Support clip sources. class ClipPathReference { public: - ClipPathReference(URL::URL const& url) + ClipPathReference(::URL::URL const& url) : m_clip_source(url) { } @@ -270,16 +270,16 @@ public: bool is_basic_shape() const { return m_clip_source.has(); } - bool is_url() const { return m_clip_source.has(); } + bool is_url() const { return m_clip_source.has<::URL::URL>(); } - URL::URL const& url() const { return m_clip_source.get(); } + ::URL::URL const& url() const { return m_clip_source.get<::URL::URL>(); } BasicShapeStyleValue const& basic_shape() const { return *m_clip_source.get(); } private: using BasicShape = NonnullRefPtr; - Variant m_clip_source; + Variant<::URL::URL, BasicShape> m_clip_source; }; struct BackgroundLayerData { diff --git a/Libraries/LibWeb/CSS/Default.css b/Libraries/LibWeb/CSS/Default.css index 6cbcfaeb7b6..97992619541 100644 --- a/Libraries/LibWeb/CSS/Default.css +++ b/Libraries/LibWeb/CSS/Default.css @@ -79,7 +79,7 @@ input[type=range] { width: 20ch; height: 16px; - &::track { + &::slider-track { display: block; position: relative; height: 4px; @@ -89,14 +89,14 @@ input[type=range] { border: 1px solid rgba(0, 0, 0, 0.5); } - &::fill { + &::slider-fill { display: block; position: absolute; height: 100%; background-color: AccentColor; } - &::thumb { + &::slider-thumb { display: block; margin-top: -6px; width: 16px; @@ -115,27 +115,27 @@ meter { width: 300px; height: 12px; - &::track { + &::slider-track { display: block; height: 100%; background-color: hsl(0, 0%, 96%); border: 1px solid rgba(0, 0, 0, 0.5); } - &::fill { + &::slider-fill { display: block; height: 100%; } - &:optimal-value::fill { + &:optimal-value::slider-fill { background-color: hsl(141, 53%, 53%); } - &:suboptimal-value::fill { + &:suboptimal-value::slider-fill { background-color: hsl(48, 100%, 67%); } - &:even-less-good-value::fill { + &:even-less-good-value::slider-fill { background-color: hsl(348, 100%, 61%); } } @@ -146,14 +146,14 @@ progress { width: 300px; height: 12px; - &::track { + &::slider-track { display: block; height: 100%; background-color: AccentColorText; border: 1px solid rgba(0, 0, 0, 0.5); } - &::fill { + &::slider-fill { display: block; height: 100%; background-color: AccentColor; diff --git a/Libraries/LibWeb/CSS/Fetch.cpp b/Libraries/LibWeb/CSS/Fetch.cpp index 53a3fa6089a..13c102771a7 100644 --- a/Libraries/LibWeb/CSS/Fetch.cpp +++ b/Libraries/LibWeb/CSS/Fetch.cpp @@ -23,7 +23,7 @@ void fetch_a_style_resource(String const& url_value, CSSStyleSheet const& sheet, auto base = sheet.base_url().value_or(environment_settings.api_base_url()); // 3. Let parsedUrl be the result of the URL parser steps with urlValue’s url and base. If the algorithm returns an error, return. - auto parsed_url = URL::Parser::basic_parse(url_value, base); + auto parsed_url = ::URL::Parser::basic_parse(url_value, base); if (!parsed_url.has_value()) return; diff --git a/Libraries/LibWeb/CSS/Interpolation.cpp b/Libraries/LibWeb/CSS/Interpolation.cpp index de84c8345d8..c87cb4f3853 100644 --- a/Libraries/LibWeb/CSS/Interpolation.cpp +++ b/Libraries/LibWeb/CSS/Interpolation.cpp @@ -586,8 +586,13 @@ NonnullRefPtr interpolate_value(DOM::Element& element, Calc layout_node = *node; return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta), ColorSyntax::Modern); } - case CSSStyleValue::Type::Integer: - return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta)); + case CSSStyleValue::Type::Integer: { + // https://drafts.csswg.org/css-values/#combine-integers + // Interpolation of is defined as Vresult = round((1 - p) × VA + p × VB); + // that is, interpolation happens in the real number space as for s, and the result is converted to an by rounding to the nearest integer. + auto interpolated_value = interpolate_raw(from.as_integer().value(), to.as_integer().value(), delta); + return IntegerStyleValue::create(round_to(interpolated_value)); + } case CSSStyleValue::Type::Length: { // FIXME: Absolutize values auto const& from_length = from.as_length().length(); @@ -611,6 +616,11 @@ NonnullRefPtr interpolate_value(DOM::Element& element, Calc auto from_ratio = from.as_ratio().ratio(); auto to_ratio = to.as_ratio().ratio(); + // https://drafts.csswg.org/css-values/#combine-ratio + // If either is degenerate, the values cannot be interpolated. + if (from_ratio.is_degenerate() || to_ratio.is_degenerate()) + return delta >= 0.5f ? to : from; + // The interpolation of a is defined by converting each to a number by dividing the first value // by the second (so a ratio of 3 / 2 would become 1.5), taking the logarithm of that result (so the 1.5 would // become approximately 0.176), then interpolating those values. The result during the interpolation is diff --git a/Libraries/LibWeb/CSS/ParsedFontFace.cpp b/Libraries/LibWeb/CSS/ParsedFontFace.cpp index 94f11f54d40..e451f03d82e 100644 --- a/Libraries/LibWeb/CSS/ParsedFontFace.cpp +++ b/Libraries/LibWeb/CSS/ParsedFontFace.cpp @@ -38,7 +38,7 @@ Vector ParsedFontFace::sources_from_style_value(CSSStyle [&](FontSourceStyleValue::Local const& local) { sources.empend(extract_font_name(local.name), OptionalNone {}); }, - [&](URL::URL const& url) { + [&](::URL::URL const& url) { // FIXME: tech() sources.empend(url, font_source.format()); }); diff --git a/Libraries/LibWeb/CSS/ParsedFontFace.h b/Libraries/LibWeb/CSS/ParsedFontFace.h index b52bcde095e..7322a64d1e9 100644 --- a/Libraries/LibWeb/CSS/ParsedFontFace.h +++ b/Libraries/LibWeb/CSS/ParsedFontFace.h @@ -19,7 +19,7 @@ namespace Web::CSS { class ParsedFontFace { public: struct Source { - Variant local_or_url; + Variant local_or_url; // FIXME: Do we need to keep this around, or is it only needed to discard unwanted formats during parsing? Optional format; }; diff --git a/Libraries/LibWeb/CSS/Parser/Helpers.cpp b/Libraries/LibWeb/CSS/Parser/Helpers.cpp index cfc3e5487d3..6d194b2c954 100644 --- a/Libraries/LibWeb/CSS/Parser/Helpers.cpp +++ b/Libraries/LibWeb/CSS/Parser/Helpers.cpp @@ -42,7 +42,7 @@ GC::Ref internal_css_realm() return *realm; } -CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const& context, StringView css, Optional location, Vector> media_query_list) +CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const& context, StringView css, Optional<::URL::URL> location, Vector> media_query_list) { if (css.is_empty()) { auto rule_list = CSS::CSSRuleList::create_empty(*context.realm); diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 811f8999c63..5b4f7a2c422 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -45,14 +45,14 @@ ParsingParams::ParsingParams(JS::Realm& realm, ParsingMode mode) { } -ParsingParams::ParsingParams(JS::Realm& realm, URL::URL url, ParsingMode mode) +ParsingParams::ParsingParams(JS::Realm& realm, ::URL::URL url, ParsingMode mode) : realm(realm) , url(move(url)) , mode(mode) { } -ParsingParams::ParsingParams(DOM::Document const& document, URL::URL url, ParsingMode mode) +ParsingParams::ParsingParams(DOM::Document const& document, ::URL::URL url, ParsingMode mode) : realm(const_cast(document.realm())) , document(&document) , url(move(url)) @@ -86,7 +86,7 @@ Parser::Parser(ParsingParams const& context, Vector tokens) // https://drafts.csswg.org/css-syntax/#parse-stylesheet template -Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream& input, Optional location) +Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream& input, Optional<::URL::URL> location) { // To parse a stylesheet from an input given an optional url location: @@ -119,10 +119,10 @@ Vector Parser::parse_a_stylesheets_contents(TokenStream& input) } // https://drafts.csswg.org/css-syntax/#parse-a-css-stylesheet -CSSStyleSheet* Parser::parse_as_css_stylesheet(Optional location, Vector> media_query_list) +CSSStyleSheet* Parser::parse_as_css_stylesheet(Optional<::URL::URL> location, Vector> media_query_list) { // To parse a CSS stylesheet, first parse a stylesheet. - auto const& style_sheet = parse_a_stylesheet(m_token_stream, {}); + auto const& style_sheet = parse_a_stylesheet(m_token_stream, location); // Interpret all of the resulting top-level qualified rules as style rules, defined below. GC::RootVector rules(realm().heap()); @@ -1772,8 +1772,8 @@ Parser::ContextType Parser::context_type_for_at_rule(FlyString const& name) return ContextType::Unknown; } -template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional); -template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional); +template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional<::URL::URL>); +template Parser::ParsedStyleSheet Parser::parse_a_stylesheet(TokenStream&, Optional<::URL::URL>); template Vector Parser::parse_a_stylesheets_contents(TokenStream& input); template Vector Parser::parse_a_stylesheets_contents(TokenStream& input); @@ -1853,10 +1853,10 @@ bool Parser::is_parsing_svg_presentation_attribute() const // https://www.w3.org/TR/css-values-4/#relative-urls // FIXME: URLs shouldn't be completed during parsing, but when used. -Optional Parser::complete_url(StringView relative_url) const +Optional<::URL::URL> Parser::complete_url(StringView relative_url) const { if (!m_url.is_valid()) - return URL::Parser::basic_parse(relative_url); + return ::URL::Parser::basic_parse(relative_url); return m_url.complete_url(relative_url); } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index d118e6697f3..0931aa232f9 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -32,6 +32,7 @@ #include #include #include +#include #include namespace Web::CSS::Parser { @@ -69,13 +70,13 @@ enum class ParsingMode { struct ParsingParams { explicit ParsingParams(ParsingMode = ParsingMode::Normal); explicit ParsingParams(JS::Realm&, ParsingMode = ParsingMode::Normal); - explicit ParsingParams(JS::Realm&, URL::URL, ParsingMode = ParsingMode::Normal); - explicit ParsingParams(DOM::Document const&, URL::URL, ParsingMode = ParsingMode::Normal); + explicit ParsingParams(JS::Realm&, ::URL::URL, ParsingMode = ParsingMode::Normal); + explicit ParsingParams(DOM::Document const&, ::URL::URL, ParsingMode = ParsingMode::Normal); explicit ParsingParams(DOM::Document const&, ParsingMode = ParsingMode::Normal); GC::Ptr realm; GC::Ptr document; - URL::URL url; + ::URL::URL url; ParsingMode mode { ParsingMode::Normal }; }; @@ -89,7 +90,7 @@ class Parser { public: static Parser create(ParsingParams const&, StringView input, StringView encoding = "utf-8"sv); - CSSStyleSheet* parse_as_css_stylesheet(Optional location, Vector> media_query_list = {}); + CSSStyleSheet* parse_as_css_stylesheet(Optional<::URL::URL> location, Vector> media_query_list = {}); struct PropertiesAndCustomProperties { Vector properties; @@ -142,11 +143,11 @@ private: // "Parse a stylesheet" is intended to be the normal parser entry point, for parsing stylesheets. struct ParsedStyleSheet { - Optional location; + Optional<::URL::URL> location; Vector rules; }; template - ParsedStyleSheet parse_a_stylesheet(TokenStream&, Optional location); + ParsedStyleSheet parse_a_stylesheet(TokenStream&, Optional<::URL::URL> location); // "Parse a stylesheet’s contents" is intended for use by the CSSStyleSheet replace() method, and similar, which parse text into the contents of an existing stylesheet. template @@ -276,7 +277,7 @@ private: Optional parse_repeat(Vector const&); Optional parse_track_sizing_function(ComponentValue const&); - Optional parse_url_function(TokenStream&); + Optional parse_url_function(TokenStream&); RefPtr parse_url_value(TokenStream&); Optional parse_shape_radius(TokenStream&); @@ -471,11 +472,11 @@ private: JS::Realm& realm() const; bool in_quirks_mode() const; bool is_parsing_svg_presentation_attribute() const; - Optional complete_url(StringView) const; + Optional<::URL::URL> complete_url(StringView) const; GC::Ptr m_document; GC::Ptr m_realm; - URL::URL m_url; + ::URL::URL m_url; ParsingMode m_parsing_mode { ParsingMode::Normal }; Vector m_tokens; @@ -519,7 +520,7 @@ private: namespace Web { -CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const&, StringView, Optional location = {}, Vector> = {}); +CSS::CSSStyleSheet* parse_css_stylesheet(CSS::Parser::ParsingParams const&, StringView, Optional<::URL::URL> location = {}, Vector> = {}); CSS::Parser::Parser::PropertiesAndCustomProperties parse_css_style_attribute(CSS::Parser::ParsingParams const&, StringView); Vector parse_css_list_of_descriptors(CSS::Parser::ParsingParams const&, CSS::AtRuleID, StringView); RefPtr parse_css_value(CSS::Parser::ParsingParams const&, StringView, CSS::PropertyID property_id = CSS::PropertyID::Invalid); diff --git a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp index cddd279e031..de02d4ba10f 100644 --- a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp @@ -153,9 +153,9 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) TokenStream tokens { rule.prelude }; tokens.discard_whitespace(); - Optional url = parse_url_function(tokens); + Optional url = parse_url_function(tokens); if (!url.has_value() && tokens.next_token().is(Token::Type::String)) - url = complete_url(tokens.consume_a_token().token().string()); + url = URL { tokens.consume_a_token().token().string().to_string() }; if (!url.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string()); @@ -191,7 +191,7 @@ GC::Ptr Parser::convert_to_import_rule(AtRule const& rule) return {}; } - return CSSImportRule::create(url.value(), const_cast(*document()), supports, move(media_query_list)); + return CSSImportRule::create(realm(), url.release_value(), const_cast(m_document.ptr()), supports, move(media_query_list)); } Optional Parser::parse_layer_name(TokenStream& tokens, AllowBlankLayerName allow_blank_layer_name) @@ -435,7 +435,10 @@ GC::Ptr Parser::convert_to_namespace_rule(AtRule const& rule) FlyString namespace_uri; if (auto url = parse_url_function(tokens); url.has_value()) { - namespace_uri = url.value().to_string(); + // "A URI string parsed from the URI syntax must be treated as a literal string: as with the STRING syntax, no + // URI-specific normalization is applied." + // https://drafts.csswg.org/css-namespaces/#syntax + namespace_uri = url->url(); } else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) { namespace_uri = url_token.token().string(); } else { diff --git a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp index 7fa84481199..16ce3677fd1 100644 --- a/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/ValueParsing.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -2014,9 +2013,13 @@ RefPtr Parser::parse_image_value(TokenStreamurl().starts_with('#')) { + // FIXME: Stop completing the URL here + auto completed_url = complete_url(url->url()); + if (completed_url.has_value()) { + tokens.discard_a_mark(); + return ImageStyleValue::create(completed_url.release_value()); + } } tokens.restore_a_mark(); return nullptr; @@ -2562,24 +2565,16 @@ RefPtr Parser::parse_easing_value(TokenStream& to return nullptr; } -Optional Parser::parse_url_function(TokenStream& tokens) +Optional Parser::parse_url_function(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); - auto& component_value = tokens.consume_a_token(); - - auto convert_string_to_url = [&](StringView url_string) -> Optional { - auto url = complete_url(url_string); - if (url.has_value()) { - transaction.commit(); - return url; - } - return {}; - }; + auto const& component_value = tokens.consume_a_token(); if (component_value.is(Token::Type::Url)) { - auto url_string = component_value.token().url(); - return convert_string_to_url(url_string); + transaction.commit(); + return URL { component_value.token().url().to_string() }; } + if (component_value.is_function("url"sv)) { auto const& function_values = component_value.function().value; // FIXME: Handle url-modifiers. https://www.w3.org/TR/css-values-4/#url-modifiers @@ -2588,8 +2583,8 @@ Optional Parser::parse_url_function(TokenStream& token if (value.is(Token::Type::Whitespace)) continue; if (value.is(Token::Type::String)) { - auto url_string = value.token().string(); - return convert_string_to_url(url_string); + transaction.commit(); + return URL { value.token().string().to_string() }; } break; } @@ -2603,7 +2598,11 @@ RefPtr Parser::parse_url_value(TokenStream& token auto url = parse_url_function(tokens); if (!url.has_value()) return nullptr; - return URLStyleValue::create(*url); + // FIXME: Stop completing the URL here + auto completed_url = complete_url(url->url()); + if (!completed_url.has_value()) + return nullptr; + return URLStyleValue::create(completed_url.release_value()); } // https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius @@ -3681,7 +3680,11 @@ RefPtr Parser::parse_font_source_value(TokenStream [ format()]? [ tech( #)]? auto url = parse_url_function(tokens); - if (!url.has_value() || !url->is_valid()) + if (!url.has_value()) + return nullptr; + // FIXME: Stop completing the URL here + auto completed_url = complete_url(url->url()); + if (!completed_url.has_value()) return nullptr; Optional format; @@ -3719,7 +3722,7 @@ RefPtr Parser::parse_font_source_value(TokenStream#)]? transaction.commit(); - return FontSourceStyleValue::create(url.release_value(), move(format)); + return FontSourceStyleValue::create(completed_url.release_value(), move(format)); } NonnullRefPtr Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved) diff --git a/Libraries/LibWeb/CSS/PseudoElements.json b/Libraries/LibWeb/CSS/PseudoElements.json index 65c93fa6e6d..1e702281795 100644 --- a/Libraries/LibWeb/CSS/PseudoElements.json +++ b/Libraries/LibWeb/CSS/PseudoElements.json @@ -1,33 +1,33 @@ { "-moz-meter-bar": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-moz-progress-bar": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-moz-range-progress": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-moz-range-track": { - "alias-for": "track" + "alias-for": "slider-track" }, "-moz-range-thumb": { - "alias-for": "thumb" + "alias-for": "slider-thumb" }, "-webkit-meter-bar": { - "alias-for": "track" + "alias-for": "slider-track" }, "-webkit-progress-bar": { - "alias-for": "track" + "alias-for": "slider-track" }, "-webkit-progress-value": { - "alias-for": "fill" + "alias-for": "slider-fill" }, "-webkit-slider-runnable-track": { - "alias-for": "track" + "alias-for": "slider-track" }, "-webkit-slider-thumb": { - "alias-for": "thumb" + "alias-for": "slider-thumb" }, "after": { "spec": "https://drafts.csswg.org/css-pseudo-4/#selectordef-after", @@ -47,9 +47,6 @@ "file-selector-button": { "spec": "https://drafts.csswg.org/css-pseudo-4/#selectordef-file-selector-button" }, - "fill": { - "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-fill" - }, "first-letter": { "spec": "https://drafts.csswg.org/css-pseudo-4/#selectordef-first-letter", "property-whitelist": [ @@ -107,11 +104,14 @@ "#custom-properties" ] }, - "thumb": { - "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-thumb" + "slider-fill": { + "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-fill" }, - "track": { - "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-track" + "slider-thumb": { + "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-thumb" + }, + "slider-track": { + "spec": "https://drafts.csswg.org/css-forms-1/#selectordef-slider-track" }, "view-transition": { "spec": "https://drafts.csswg.org/css-view-transitions-1/#selectordef-view-transition" diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index e58c0dfa56f..dda533237c6 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -185,7 +185,7 @@ StyleComputer::StyleComputer(DOM::Document& document) StyleComputer::~StyleComputer() = default; -FontLoader::FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector urls, Function on_load, Function on_fail) +FontLoader::FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector<::URL::URL> urls, Function on_load, Function on_fail) : m_style_computer(style_computer) , m_family_name(move(family_name)) , m_unicode_ranges(move(unicode_ranges)) @@ -3028,11 +3028,11 @@ Optional StyleComputer::load_font_face(ParsedFontFace const& font_f .slope = font_face.slope().value_or(0), }; - Vector urls; + Vector<::URL::URL> urls; for (auto const& source : font_face.sources()) { // FIXME: These should be loaded relative to the stylesheet URL instead of the document URL. - if (source.local_or_url.has()) - urls.append(*m_document->encoding_parse_url(source.local_or_url.get().to_string())); + if (source.local_or_url.has<::URL::URL>()) + urls.append(*m_document->encoding_parse_url(source.local_or_url.get<::URL::URL>().to_string())); // FIXME: Handle local() } diff --git a/Libraries/LibWeb/CSS/StyleComputer.h b/Libraries/LibWeb/CSS/StyleComputer.h index 4fb5ec0cdd1..70747b5909a 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Libraries/LibWeb/CSS/StyleComputer.h @@ -315,7 +315,7 @@ private: class FontLoader : public ResourceClient { public: - FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector urls, ESCAPING Function on_load = {}, ESCAPING Function on_fail = {}); + FontLoader(StyleComputer& style_computer, FlyString family_name, Vector unicode_ranges, Vector<::URL::URL> urls, ESCAPING Function on_load = {}, ESCAPING Function on_fail = {}); virtual ~FontLoader() override; @@ -340,7 +340,7 @@ private: FlyString m_family_name; Vector m_unicode_ranges; RefPtr m_vector_font; - Vector m_urls; + Vector<::URL::URL> m_urls; Function m_on_load; Function m_on_fail; }; diff --git a/Libraries/LibWeb/CSS/StyleSheet.cpp b/Libraries/LibWeb/CSS/StyleSheet.cpp index 2d908e00c15..f1387c98b47 100644 --- a/Libraries/LibWeb/CSS/StyleSheet.cpp +++ b/Libraries/LibWeb/CSS/StyleSheet.cpp @@ -27,6 +27,13 @@ void StyleSheet::visit_edges(Cell::Visitor& visitor) visitor.visit(m_media); } +Optional StyleSheet::href() const +{ + if (m_location.has_value()) + return m_location->to_string(); + return {}; +} + void StyleSheet::set_owner_node(DOM::Element* element) { m_owner_node = element; diff --git a/Libraries/LibWeb/CSS/StyleSheet.h b/Libraries/LibWeb/CSS/StyleSheet.h index 3e73f0e3f75..b85d97fc5e5 100644 --- a/Libraries/LibWeb/CSS/StyleSheet.h +++ b/Libraries/LibWeb/CSS/StyleSheet.h @@ -13,6 +13,7 @@ namespace Web::CSS { +// https://drafts.csswg.org/cssom-1/#the-stylesheet-interface class StyleSheet : public Bindings::PlatformObject { WEB_PLATFORM_OBJECT(StyleSheet, Bindings::PlatformObject); @@ -24,10 +25,10 @@ public: DOM::Element* owner_node() { return m_owner_node; } void set_owner_node(DOM::Element*); - Optional href() const { return m_location; } + Optional href() const; - Optional location() const { return m_location; } - void set_location(Optional location) { m_location = move(location); } + Optional<::URL::URL> location() const { return m_location; } + void set_location(Optional<::URL::URL> location) { m_location = move(location); } String title() const { return m_title; } Optional title_for_bindings() const; @@ -35,7 +36,7 @@ public: void set_type(String type) { m_type_string = move(type); } - MediaList* media() const + GC::Ref media() const { return m_media; } @@ -67,7 +68,7 @@ private: GC::Ptr m_owner_node; GC::Ptr m_parent_style_sheet; - Optional m_location; + Optional<::URL::URL> m_location; String m_title; String m_type_string; diff --git a/Libraries/LibWeb/CSS/StyleSheetList.cpp b/Libraries/LibWeb/CSS/StyleSheetList.cpp index e7e204f8599..be0db396e51 100644 --- a/Libraries/LibWeb/CSS/StyleSheetList.cpp +++ b/Libraries/LibWeb/CSS/StyleSheetList.cpp @@ -60,7 +60,7 @@ void StyleSheetList::add_a_css_style_sheet(CSS::CSSStyleSheet& sheet) } // https://www.w3.org/TR/cssom/#create-a-css-style-sheet -void StyleSheetList::create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet& sheet) +void StyleSheetList::create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional<::URL::URL> location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet& sheet) { // 1. Create a new CSS style sheet object and set its properties as specified. // FIXME: We receive `sheet` from the caller already. This is weird. diff --git a/Libraries/LibWeb/CSS/StyleSheetList.h b/Libraries/LibWeb/CSS/StyleSheetList.h index 317a1865a88..935ab4fca11 100644 --- a/Libraries/LibWeb/CSS/StyleSheetList.h +++ b/Libraries/LibWeb/CSS/StyleSheetList.h @@ -21,7 +21,7 @@ public: void add_a_css_style_sheet(CSS::CSSStyleSheet&); void remove_a_css_style_sheet(CSS::CSSStyleSheet&); - void create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet&); + void create_a_css_style_sheet(String type, DOM::Element* owner_node, String media, String title, bool alternate, bool origin_clean, Optional<::URL::URL> location, CSS::CSSStyleSheet* parent_style_sheet, CSS::CSSRule* owner_rule, CSS::CSSStyleSheet&); Vector> const& sheets() const { return m_sheets; } Vector>& sheets() { return m_sheets; } diff --git a/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp index 439921dd79c..21ab3348a19 100644 --- a/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/CursorStyleValue.cpp @@ -94,7 +94,8 @@ Optional CursorStyleValue::make_image_cursor(Layout::NodeWithS case DisplayListPlayerType::SkiaCPU: { auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(bitmap); Painting::DisplayListPlayerSkia display_list_player; - display_list_player.execute(*display_list, painting_surface); + Painting::ScrollStateSnapshot scroll_state_snapshot; + display_list_player.execute(*display_list, scroll_state_snapshot, painting_surface); break; } } diff --git a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp index dd3b15db4bf..ee8cb1641db 100644 --- a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.cpp @@ -34,7 +34,7 @@ String FontSourceStyleValue::to_string(SerializationMode) const builder.append(')'); return builder.to_string_without_validation(); }, - [this](URL::URL const& url) { + [this](::URL::URL const& url) { // [ format()]? [ tech( #)]? // FIXME: tech() StringBuilder builder; @@ -59,8 +59,8 @@ bool FontSourceStyleValue::properties_equal(FontSourceStyleValue const& other) c } return false; }, - [&other](URL::URL const& url) { - if (auto* other_url = other.m_source.get_pointer()) { + [&other](::URL::URL const& url) { + if (auto* other_url = other.m_source.get_pointer<::URL::URL>()) { return url == *other_url; } return false; diff --git a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h index 732f990bdaa..f088947afff 100644 --- a/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/FontSourceStyleValue.h @@ -16,7 +16,7 @@ public: struct Local { NonnullRefPtr name; }; - using Source = Variant; + using Source = Variant; static ValueComparingNonnullRefPtr create(Source source, Optional format) { diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp index c35ddf8fcde..1e12e5c1569 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.cpp @@ -20,7 +20,7 @@ namespace Web::CSS { -ImageStyleValue::ImageStyleValue(URL::URL const& url) +ImageStyleValue::ImageStyleValue(::URL::URL const& url) : AbstractImageStyleValue(Type::Image) , m_url(url) { diff --git a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h index d643635efe0..9359897dafb 100644 --- a/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/ImageStyleValue.h @@ -25,7 +25,7 @@ class ImageStyleValue final using Base = AbstractImageStyleValue; public: - static ValueComparingNonnullRefPtr create(URL::URL const& url) + static ValueComparingNonnullRefPtr create(::URL::URL const& url) { return adopt_ref(*new (nothrow) ImageStyleValue(url)); } @@ -53,14 +53,14 @@ public: GC::Ptr image_data() const; private: - ImageStyleValue(URL::URL const&); + ImageStyleValue(::URL::URL const&); GC::Ptr m_resource_request; void animate(); Gfx::ImmutableBitmap const* bitmap(size_t frame_index, Gfx::IntSize = {}) const; - URL::URL m_url; + ::URL::URL m_url; WeakPtr m_document; size_t m_current_frame_index { 0 }; diff --git a/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h index 6a7844aa153..8e4b1f45a3f 100644 --- a/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h +++ b/Libraries/LibWeb/CSS/StyleValues/URLStyleValue.h @@ -14,14 +14,14 @@ namespace Web::CSS { class URLStyleValue final : public StyleValueWithDefaultOperators { public: - static ValueComparingNonnullRefPtr create(URL::URL const& url) + static ValueComparingNonnullRefPtr create(::URL::URL const& url) { return adopt_ref(*new (nothrow) URLStyleValue(url)); } virtual ~URLStyleValue() override = default; - URL::URL const& url() const { return m_url; } + ::URL::URL const& url() const { return m_url; } bool properties_equal(URLStyleValue const& other) const { return m_url == other.m_url; } @@ -31,13 +31,13 @@ public: } private: - URLStyleValue(URL::URL const& url) + URLStyleValue(::URL::URL const& url) : StyleValueWithDefaultOperators(Type::URL) , m_url(url) { } - URL::URL m_url; + ::URL::URL m_url; }; } diff --git a/Libraries/LibWeb/CSS/URL.cpp b/Libraries/LibWeb/CSS/URL.cpp new file mode 100644 index 00000000000..a634b6128bc --- /dev/null +++ b/Libraries/LibWeb/CSS/URL.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::CSS { + +URL::URL(String url) + : m_url(move(url)) +{ +} + +// https://drafts.csswg.org/cssom-1/#serialize-a-url +String URL::to_string() const +{ + // To serialize a URL means to create a string represented by "url(", followed by the serialization of the URL as a string, followed by ")". + StringBuilder builder; + builder.append("url("sv); + serialize_a_string(builder, m_url); + builder.append(')'); + + return builder.to_string_without_validation(); +} + +bool URL::operator==(URL const&) const = default; + +} diff --git a/Libraries/LibWeb/CSS/URL.h b/Libraries/LibWeb/CSS/URL.h new file mode 100644 index 00000000000..9380ad2e226 --- /dev/null +++ b/Libraries/LibWeb/CSS/URL.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, Sam Atkins + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS { + +// https://drafts.csswg.org/css-values-4/#urls +class URL { +public: + URL(String url); + + String const& url() const { return m_url; } + + String to_string() const; + bool operator==(URL const&) const; + +private: + String m_url; +}; + +} diff --git a/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp b/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp index 9ac38d71952..a97eff14290 100644 --- a/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp +++ b/Libraries/LibWeb/Crypto/CryptoAlgorithms.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include diff --git a/Libraries/LibWeb/Crypto/SubtleCrypto.cpp b/Libraries/LibWeb/Crypto/SubtleCrypto.cpp index a5ac5afb87f..4fb748f40ff 100644 --- a/Libraries/LibWeb/Crypto/SubtleCrypto.cpp +++ b/Libraries/LibWeb/Crypto/SubtleCrypto.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include diff --git a/Libraries/LibWeb/DOM/AbortSignal.cpp b/Libraries/LibWeb/DOM/AbortSignal.cpp index 633663c0da0..60c24678d20 100644 --- a/Libraries/LibWeb/DOM/AbortSignal.cpp +++ b/Libraries/LibWeb/DOM/AbortSignal.cpp @@ -35,14 +35,22 @@ void AbortSignal::initialize(JS::Realm& realm) } // https://dom.spec.whatwg.org/#abortsignal-add -void AbortSignal::add_abort_algorithm(Function abort_algorithm) +Optional AbortSignal::add_abort_algorithm(Function abort_algorithm) { // 1. If signal is aborted, then return. if (aborted()) - return; + return {}; // 2. Append algorithm to signal’s abort algorithms. - m_abort_algorithms.append(GC::create_function(vm().heap(), move(abort_algorithm))); + m_abort_algorithms.set(++m_next_abort_algorithm_id, GC::create_function(vm().heap(), move(abort_algorithm))); + return m_next_abort_algorithm_id; +} + +// https://dom.spec.whatwg.org/#abortsignal-remove +void AbortSignal::remove_abort_algorithm(AbortAlgorithmID id) +{ + // To remove an algorithm algorithm from an AbortSignal signal, remove algorithm from signal’s abort algorithms. + m_abort_algorithms.remove(id); } // https://dom.spec.whatwg.org/#abortsignal-signal-abort @@ -76,8 +84,8 @@ void AbortSignal::signal_abort(JS::Value reason) // https://dom.spec.whatwg.org/#run-the-abort-steps auto run_the_abort_steps = [](auto& signal) { // 1. For each algorithm in signal’s abort algorithms: run algorithm. - for (auto& algorithm : signal.m_abort_algorithms) - algorithm->function()(); + for (auto const& algorithm : signal.m_abort_algorithms) + algorithm.value->function()(); // 2. Empty signal’s abort algorithms. signal.m_abort_algorithms.clear(); diff --git a/Libraries/LibWeb/DOM/AbortSignal.h b/Libraries/LibWeb/DOM/AbortSignal.h index ac5f518218c..26a654b3476 100644 --- a/Libraries/LibWeb/DOM/AbortSignal.h +++ b/Libraries/LibWeb/DOM/AbortSignal.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -26,7 +27,9 @@ public: virtual ~AbortSignal() override = default; - void add_abort_algorithm(ESCAPING Function); + using AbortAlgorithmID = u64; + Optional add_abort_algorithm(Function); + void remove_abort_algorithm(AbortAlgorithmID); // https://dom.spec.whatwg.org/#dom-abortsignal-aborted // An AbortSignal object is aborted when its abort reason is not undefined. @@ -68,8 +71,8 @@ private: JS::Value m_abort_reason { JS::js_undefined() }; // https://dom.spec.whatwg.org/#abortsignal-abort-algorithms - // FIXME: This should be a set. - Vector>> m_abort_algorithms; + OrderedHashMap>> m_abort_algorithms; + AbortAlgorithmID m_next_abort_algorithm_id { 0 }; // https://dom.spec.whatwg.org/#abortsignal-source-signals // An AbortSignal object has associated source signals (a weak set of AbortSignal objects that the object is dependent on for its aborted state), which is initially empty. diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index e8a3d0b437e..64ff0637b70 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -584,7 +584,8 @@ void Document::visit_edges(Cell::Visitor& visitor) visitor.visit(m_adopted_style_sheets); - visitor.visit(m_shadow_roots); + for (auto& shadow_root : m_shadow_roots) + visitor.visit(shadow_root); visitor.visit(m_top_layer_elements); visitor.visit(m_top_layer_pending_removals); @@ -5879,7 +5880,7 @@ void Document::for_each_active_css_style_sheet(Function find_style_sheet_with_url(String const& url, CSS::CSSStyleSheet& style_sheet) { - if (style_sheet.location() == url) + if (style_sheet.href() == url) return style_sheet; for (auto& import_rule : style_sheet.import_rules()) { @@ -5953,9 +5954,7 @@ void Document::register_shadow_root(Badge, DOM::ShadowRoot& sha void Document::unregister_shadow_root(Badge, DOM::ShadowRoot& shadow_root) { - m_shadow_roots.remove_all_matching([&](auto& item) { - return item.ptr() == &shadow_root; - }); + m_shadow_roots.remove(shadow_root); } void Document::for_each_shadow_root(Function&& callback) @@ -5967,7 +5966,7 @@ void Document::for_each_shadow_root(Function&& callback) void Document::for_each_shadow_root(Function&& callback) const { for (auto& shadow_root : m_shadow_roots) - callback(shadow_root); + callback(const_cast(shadow_root)); } bool Document::is_decoded_svg() const @@ -6297,8 +6296,9 @@ void Document::invalidate_display_list() RefPtr Document::record_display_list(PaintConfig config) { - if (m_cached_display_list && m_cached_display_list_paint_config == config) + if (m_cached_display_list && m_cached_display_list_paint_config == config) { return m_cached_display_list; + } auto display_list = Painting::DisplayList::create(); Painting::DisplayListRecorder display_list_recorder(display_list); @@ -6355,7 +6355,6 @@ RefPtr Document::record_display_list(PaintConfig config) viewport_paintable.paint_all_phases(context); display_list->set_device_pixels_per_css_pixel(page().client().device_pixels_per_css_pixel()); - display_list->set_scroll_state(viewport_paintable.scroll_state()); m_cached_display_list = display_list; m_cached_display_list_paint_config = config; diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index edd6c9c863a..4a11faaef7a 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -1180,7 +1181,7 @@ private: mutable GC::Ptr m_adopted_style_sheets; - Vector> m_shadow_roots; + ShadowRoot::DocumentShadowRootList m_shadow_roots; Optional m_last_modified; diff --git a/Libraries/LibWeb/DOM/DocumentLoading.cpp b/Libraries/LibWeb/DOM/DocumentLoading.cpp index 058ea46d4e8..0bc8f5853b9 100644 --- a/Libraries/LibWeb/DOM/DocumentLoading.cpp +++ b/Libraries/LibWeb/DOM/DocumentLoading.cpp @@ -448,7 +448,7 @@ GC::Ptr load_document(HTML::NavigationParams const& navigation_pa // sourceSnapshotParams, and initiatorOrigin. } - // -> A supported image, video, or audio type + // -> a supported image, video, or audio type if (type.is_image() || type.is_audio_or_video()) { // Return the result of loading a media document given navigationParams and type. diff --git a/Libraries/LibWeb/DOM/Element.cpp b/Libraries/LibWeb/DOM/Element.cpp index c4f9bc70b72..0f238ed60d4 100644 --- a/Libraries/LibWeb/DOM/Element.cpp +++ b/Libraries/LibWeb/DOM/Element.cpp @@ -2548,7 +2548,7 @@ JS::ThrowCompletionOr Element::upgrade_element(GC::Refadd_abort_algorithm([this, &listener] { + (void)listener.signal->add_abort_algorithm([this, &listener] { // 1. Remove an event listener with eventTarget and listener. remove_an_event_listener(listener); }); diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index 56f85c41a4a..e3ec675c27b 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -53,6 +53,7 @@ enum class ShouldComputeRole { X(AdoptedStyleSheetsList) \ X(CSSFontLoaded) \ X(CSSImportRule) \ + X(CSSStylePropertiesTextChange) \ X(CustomElementStateChange) \ X(DidLoseFocus) \ X(DidReceiveFocus) \ diff --git a/Libraries/LibWeb/DOM/NodeIterator.cpp b/Libraries/LibWeb/DOM/NodeIterator.cpp index 61f32451afd..a4f8def926b 100644 --- a/Libraries/LibWeb/DOM/NodeIterator.cpp +++ b/Libraries/LibWeb/DOM/NodeIterator.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibWeb/DOM/ShadowRoot.h b/Libraries/LibWeb/DOM/ShadowRoot.h index b55b483fd2d..6093bae388b 100644 --- a/Libraries/LibWeb/DOM/ShadowRoot.h +++ b/Libraries/LibWeb/DOM/ShadowRoot.h @@ -97,6 +97,11 @@ private: GC::Ptr m_style_sheets; mutable GC::Ptr m_adopted_style_sheets; + + IntrusiveListNode m_list_node; + +public: + using DocumentShadowRootList = IntrusiveList<&ShadowRoot::m_list_node>; }; template<> diff --git a/Libraries/LibWeb/DOM/StyleElementUtils.cpp b/Libraries/LibWeb/DOM/StyleElementUtils.cpp index 6e030bb8a41..bd594f58858 100644 --- a/Libraries/LibWeb/DOM/StyleElementUtils.cpp +++ b/Libraries/LibWeb/DOM/StyleElementUtils.cpp @@ -54,7 +54,8 @@ void StyleElementUtils::update_a_style_block(DOM::Element& style_element) // FIXME: This is a bit awkward, as the spec doesn't actually tell us when to parse the CSS text, // so we just do it here and pass the parsed sheet to create_a_css_style_sheet(). - auto* sheet = parse_css_stylesheet(CSS::Parser::ParsingParams(style_element.document()), style_element.text_content().value_or(String {})); + // AD-HOC: Are we supposed to use the document's URL for the stylesheet's location? Not doing it breaks things. + auto* sheet = parse_css_stylesheet(CSS::Parser::ParsingParams(style_element.document()), style_element.text_content().value_or(String {}), style_element.document().url()); if (!sheet) return; diff --git a/Libraries/LibWeb/DOM/TreeWalker.cpp b/Libraries/LibWeb/DOM/TreeWalker.cpp index e2c6961a447..aeea0ae6f4e 100644 --- a/Libraries/LibWeb/DOM/TreeWalker.cpp +++ b/Libraries/LibWeb/DOM/TreeWalker.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index 2803277488f..a8fe444eb28 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -790,7 +790,7 @@ void dump_font_face_rule(StringBuilder& builder, CSS::CSSFontFaceRule const& rul void dump_import_rule(StringBuilder& builder, CSS::CSSImportRule const& rule, int indent_levels) { indent(builder, indent_levels); - builder.appendff(" Document URL: {}\n", rule.url()); + builder.appendff(" Document URL: {}\n", rule.url().to_string()); } void dump_layer_block_rule(StringBuilder& builder, CSS::CSSLayerBlockRule const& layer_block, int indent_levels) diff --git a/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp b/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp index 95e1f25f50b..cd0d3567d3e 100644 --- a/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp +++ b/Libraries/LibWeb/EventTiming/PerformanceEventTiming.cpp @@ -36,13 +36,13 @@ FlyString const& PerformanceEventTiming::entry_type() const HighResolutionTime::DOMHighResTimeStamp PerformanceEventTiming::processing_end() const { - dbgln("FIXME: Implement PeformanceEventTiming processing_end()"); + dbgln("FIXME: Implement PerformanceEventTiming processing_end()"); return 0; } HighResolutionTime::DOMHighResTimeStamp PerformanceEventTiming::processing_start() const { - dbgln("FIXME: Implement PeformanceEventTiming processing_start()"); + dbgln("FIXME: Implement PerformanceEventTiming processing_start()"); return 0; } @@ -53,20 +53,20 @@ bool PerformanceEventTiming::cancelable() const JS::ThrowCompletionOr> PerformanceEventTiming::target() { - dbgln("FIXME: Implement PerformanceEventTiming::PeformanceEventTiming target()"); + dbgln("FIXME: Implement PerformanceEventTiming::PerformanceEventTiming target()"); return nullptr; } unsigned long long PerformanceEventTiming::interaction_id() { - dbgln("FIXME: Implement PeformanceEventTiming interaction_id()"); + dbgln("FIXME: Implement PerformanceEventTiming interaction_id()"); return 0; } // https://www.w3.org/TR/event-timing/#sec-should-add-performanceeventtiming PerformanceTimeline::ShouldAddEntry PerformanceEventTiming::should_add_performance_event_timing() const { - dbgln("FIXME: Implement PeformanceEventTiming should_add_performance_event_timing()"); + dbgln("FIXME: Implement PerformanceEventTiming should_add_performance_event_timing()"); // 1. If entry’s entryType attribute value equals to "first-input", return true. if (entry_type() == "first-input") return PerformanceTimeline::ShouldAddEntry::Yes; @@ -89,7 +89,7 @@ PerformanceTimeline::ShouldAddEntry PerformanceEventTiming::should_add_performan // the commented out if statement won't compile PerformanceTimeline::AvailableFromTimeline PerformanceEventTiming::available_from_timeline() { - dbgln("FIXME: Implement PeformanceEventTiming available_from_timeline()"); + dbgln("FIXME: Implement PerformanceEventTiming available_from_timeline()"); // if (entry_type() == "first-input") return PerformanceTimeline::AvailableFromTimeline::Yes; } @@ -98,7 +98,7 @@ PerformanceTimeline::AvailableFromTimeline PerformanceEventTiming::available_fro // FIXME: Same issue as available_from_timeline() above Optional PerformanceEventTiming::max_buffer_size() { - dbgln("FIXME: Implement PeformanceEventTiming max_buffer_size()"); + dbgln("FIXME: Implement PerformanceEventTiming max_buffer_size()"); if (true) //(entry_type() == "first-input") return 1; // else return 150; diff --git a/Libraries/LibWeb/Fetch/FetchMethod.cpp b/Libraries/LibWeb/Fetch/FetchMethod.cpp index 64531614ee3..9f83cb3696a 100644 --- a/Libraries/LibWeb/Fetch/FetchMethod.cpp +++ b/Libraries/LibWeb/Fetch/FetchMethod.cpp @@ -130,7 +130,7 @@ GC::Ref fetch(JS::VM& vm, RequestInfo const& input, RequestInit })))); // 11. Add the following abort steps to requestObject’s signal: - request_object->signal()->add_abort_algorithm([locally_aborted, request, controller_holder, promise_capability, request_object, response_object, &relevant_realm] { + (void)request_object->signal()->add_abort_algorithm([locally_aborted, request, controller_holder, promise_capability, request_object, response_object, &relevant_realm] { dbgln_if(WEB_FETCH_DEBUG, "Fetch: Request object signal's abort algorithm called"); // 1. Set locallyAborted to true. diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 557f007dafc..0271a0c1d39 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -269,6 +269,7 @@ class TransformationStyleValue; class TransitionStyleValue; class UnicodeRangeStyleValue; class UnresolvedStyleValue; +class URL; class URLStyleValue; class VisualViewport; @@ -624,6 +625,7 @@ class IDBOpenDBRequest; class IDBRequest; class IDBTransaction; class IDBVersionChangeEvent; +class Index; class ObjectStore; class RequestList; } diff --git a/Libraries/LibWeb/Geometry/DOMQuad.cpp b/Libraries/LibWeb/Geometry/DOMQuad.cpp index e77e0b4bb5a..09363637b01 100644 --- a/Libraries/LibWeb/Geometry/DOMQuad.cpp +++ b/Libraries/LibWeb/Geometry/DOMQuad.cpp @@ -103,17 +103,17 @@ GC::Ref DOMQuad::get_bounds() const } // https://drafts.fxtf.org/geometry/#structured-serialization -WebIDL::ExceptionOr DOMQuad::serialization_steps(HTML::SerializationRecord& serialzied, bool for_storage, HTML::SerializationMemory& memory) +WebIDL::ExceptionOr DOMQuad::serialization_steps(HTML::SerializationRecord& serialized, bool for_storage, HTML::SerializationMemory& memory) { auto& vm = this->vm(); // 1. Set serialized.[[P1]] to the sub-serialization of value’s point 1. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p1, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p1, for_storage, memory))); // 2. Set serialized.[[P2]] to the sub-serialization of value’s point 2. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p2, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p2, for_storage, memory))); // 3. Set serialized.[[P3]] to the sub-serialization of value’s point 3. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p3, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p3, for_storage, memory))); // 4. Set serialized.[[P4]] to the sub-serialization of value’s point 4. - serialzied.extend(TRY(HTML::structured_serialize_internal(vm, m_p4, for_storage, memory))); + serialized.extend(TRY(HTML::structured_serialize_internal(vm, m_p4, for_storage, memory))); return {}; } diff --git a/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp b/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp index dac46083c51..22d34afd70a 100644 --- a/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp +++ b/Libraries/LibWeb/HTML/Canvas/CanvasPath.cpp @@ -81,7 +81,7 @@ void CanvasPath::bezier_curve_to(double cp1x, double cp1y, double cp2x, double c // 2. Ensure there is a subpath for (cp1x, cp1y) ensure_subpath(cp1x, cp1y); - // 3. Connect the last point in the subpath to the given point (x, y) using a cubic Bézier curve with control poits (cp1x, cp1y) and (cp2x, cp2y). + // 3. Connect the last point in the subpath to the given point (x, y) using a cubic Bézier curve with control points (cp1x, cp1y) and (cp2x, cp2y). // 4. Add the point (x, y) to the subpath. m_path.cubic_bezier_curve_to( Gfx::FloatPoint { cp1x, cp1y }, Gfx::FloatPoint { cp2x, cp2y }, Gfx::FloatPoint { x, y }); diff --git a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp index f6f3ab882a0..d6fb63f609e 100644 --- a/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp +++ b/Libraries/LibWeb/HTML/CanvasRenderingContext2D.cpp @@ -263,10 +263,10 @@ Gfx::Path CanvasRenderingContext2D::text_path(StringView text, float x, float y, } // Apply text baseline - // FIXME: Implement CanvasTextBasline::Hanging, Bindings::CanvasTextAlign::Alphabetic and Bindings::CanvasTextAlign::Ideographic for real + // FIXME: Implement CanvasTextBaseline::Hanging, Bindings::CanvasTextAlign::Alphabetic and Bindings::CanvasTextAlign::Ideographic for real // right now they are just handled as textBaseline = top or bottom. // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-textbaseline-hanging - // Default baseline of draw_text is top so do nothing by CanvasTextBaseline::Top and CanvasTextBasline::Hanging + // Default baseline of draw_text is top so do nothing by CanvasTextBaseline::Top and CanvasTextBaseline::Hanging if (drawing_state.text_baseline == Bindings::CanvasTextBaseline::Middle) { transform = Gfx::AffineTransform {}.set_translation({ 0, font->pixel_size() / 2 }).multiply(transform); } diff --git a/Libraries/LibWeb/HTML/CloseWatcher.cpp b/Libraries/LibWeb/HTML/CloseWatcher.cpp index 86edb65bbda..8643e8abead 100644 --- a/Libraries/LibWeb/HTML/CloseWatcher.cpp +++ b/Libraries/LibWeb/HTML/CloseWatcher.cpp @@ -65,7 +65,7 @@ WebIDL::ExceptionOr> CloseWatcher::construct_impl(JS::Real } // 3.2 Add the following steps to options["signal"]: - signal->add_abort_algorithm([close_watcher] { + (void)signal->add_abort_algorithm([close_watcher] { // 3.2.1 Destroy closeWatcher. close_watcher->destroy(); }); diff --git a/Libraries/LibWeb/HTML/Dates.cpp b/Libraries/LibWeb/HTML/Dates.cpp index 574c14f9246..e898898d9a5 100644 --- a/Libraries/LibWeb/HTML/Dates.cpp +++ b/Libraries/LibWeb/HTML/Dates.cpp @@ -233,7 +233,7 @@ static Optional parse_a_month_component(GenericLexer& input) { // 1. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is // not at least four characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. - // Let that number be the year. + // Let year be that number. auto year_string = input.consume_while(is_ascii_digit); if (year_string.length() < 4) return {}; @@ -252,8 +252,8 @@ static Optional parse_a_month_component(GenericLexer& input) return {}; // 4. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is not - // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let that - // number be the month. + // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let month + // be that number. auto month_string = input.consume_while(is_ascii_digit); if (month_string.length() != 2) return {}; @@ -301,7 +301,7 @@ Optional parse_a_week_string(StringView input_view) // 3. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is // not at least four characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. - // Let that number be the year. + // Let year be that number. auto year_string = input.consume_while(is_ascii_digit); if (year_string.length() < 4) return {}; @@ -325,8 +325,8 @@ Optional parse_a_week_string(StringView input_view) return {}; // 7. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is not - // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let that - // number be the week. + // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let week + // be that number. auto week_string = input.consume_while(is_ascii_digit); if (week_string.length() != 2) return {}; @@ -365,8 +365,8 @@ static Optional parse_a_date_component(GenericLexer& input) return {}; // 4. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence is not - // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let that - // number be the day. + // exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. Let day + // be that number. auto day_string = input.consume_while(is_ascii_digit); if (day_string.length() != 2) return {}; @@ -406,7 +406,7 @@ static Optional parse_a_time_component(GenericLexer& input) { // 1. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence // is not exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten - // integer. Let that number be the hour. + // integer. Let hour be that number. auto hour_string = input.consume_while(is_ascii_digit); if (hour_string.length() != 2) return {}; @@ -426,7 +426,7 @@ static Optional parse_a_time_component(GenericLexer& input) // 4. Collect a sequence of code points that are ASCII digits from input given position. If the collected sequence // is not exactly two characters long, then fail. Otherwise, interpret the resulting sequence as a base-ten integer. - // Let that number be the minute. + // Let minute be that number. auto minute_string = input.consume_while(is_ascii_digit); if (minute_string.length() != 2) return {}; diff --git a/Libraries/LibWeb/HTML/EventSource.cpp b/Libraries/LibWeb/HTML/EventSource.cpp index 6bca0823226..324eec8ec51 100644 --- a/Libraries/LibWeb/HTML/EventSource.cpp +++ b/Libraries/LibWeb/HTML/EventSource.cpp @@ -389,7 +389,7 @@ void EventSource::process_field(StringView field, StringView value) { // -> If the field name is "event" if (field == "event"sv) { - // Set the event type buffer to field value. + // Set the event type buffer to the field value. m_event_type = MUST(String::from_utf8(value)); } // -> If the field name is "data" diff --git a/Libraries/LibWeb/HTML/HTMLElement.cpp b/Libraries/LibWeb/HTML/HTMLElement.cpp index a19927021f4..1091597f656 100644 --- a/Libraries/LibWeb/HTML/HTMLElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLElement.cpp @@ -1121,7 +1121,7 @@ WebIDL::ExceptionOr HTMLElement::check_popover_validity(ExpectedToBeShowin // - ignoreDomState is false and element is not connected; // - element's node document is not fully active; // - ignoreDomState is false and expectedDocument is not null and element's node document is not expectedDocument; - // - element is a dialog element and its is modal flage is set to true; or + // - element is a dialog element and its is modal flag is set to true; or // - FIXME: element's fullscreen flag is set, // then: // 3.1 If throwExceptions is true, then throw an "InvalidStateError" DOMException. diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index bff078f009d..8818ec4f76a 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -145,7 +145,7 @@ void HTMLInputElement::adjust_computed_style(CSS::ComputedProperties& style) style.set_property(CSS::PropertyID::Width, CSS::LengthStyleValue::create(CSS::Length(size(), CSS::Length::Type::Ch))); } - // NOTE: The following line-height check is done for web compatability and usability reasons. + // NOTE: The following line-height check is done for web compatibility and usability reasons. // FIXME: The "normal" line-height value should be calculated but assume 1.0 for now. double normal_line_height = 1.0; double current_line_height = style.line_height().to_double(); @@ -1217,15 +1217,15 @@ void HTMLInputElement::create_range_input_shadow_tree() set_shadow_root(shadow_root); m_slider_runnable_track = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_slider_runnable_track->set_use_pseudo_element(CSS::PseudoElement::Track); + m_slider_runnable_track->set_use_pseudo_element(CSS::PseudoElement::SliderTrack); MUST(shadow_root->append_child(*m_slider_runnable_track)); m_slider_progress_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_slider_progress_element->set_use_pseudo_element(CSS::PseudoElement::Fill); + m_slider_progress_element->set_use_pseudo_element(CSS::PseudoElement::SliderFill); MUST(m_slider_runnable_track->append_child(*m_slider_progress_element)); m_slider_thumb = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_slider_thumb->set_use_pseudo_element(CSS::PseudoElement::Thumb); + m_slider_thumb->set_use_pseudo_element(CSS::PseudoElement::SliderThumb); MUST(m_slider_runnable_track->append_child(*m_slider_thumb)); update_slider_shadow_tree_elements(); @@ -2404,7 +2404,7 @@ WebIDL::ExceptionOr> HTMLInputElement::convert_string_to_date( } // https://html.spec.whatwg.org/multipage/input.html#concept-input-value-date-string -String HTMLInputElement::covert_date_to_string(GC::Ref input) const +String HTMLInputElement::convert_date_to_string(GC::Ref input) const { // https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):concept-input-value-date-string if (type_state() == TypeAttributeState::Date) { @@ -2420,7 +2420,7 @@ String HTMLInputElement::covert_date_to_string(GC::Ref input) const return convert_number_to_time_string(input->date_value()); } - dbgln("HTMLInputElement::covert_date_to_string() not implemented for input type {}", type()); + dbgln("HTMLInputElement::convert_date_to_string() not implemented for input type {}", type()); return {}; } @@ -2589,7 +2589,7 @@ WebIDL::ExceptionOr HTMLInputElement::set_value_as_date(Optional> convert_string_to_date(StringView input) const; - String covert_date_to_string(GC::Ref input) const; + String convert_date_to_string(GC::Ref input) const; Optional min() const; Optional max() const; diff --git a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp index d5242517c4c..0adacae602e 100644 --- a/Libraries/LibWeb/HTML/HTMLLinkElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLLinkElement.cpp @@ -280,7 +280,7 @@ HTMLLinkElement::LinkProcessingOptions HTMLLinkElement::create_link_options() options.policy_container = document.policy_container(); // document document options.document = &document; - // FIXME: cryptographic nonce metadata The current value of el's [[CryptographicNonce]] internal slot + // FIXME: cryptographic nonce metadata the current value of el's [[CryptographicNonce]] internal slot // fetch priority the state of el's fetchpriority content attribute options.fetch_priority = Fetch::Infrastructure::request_priority_from_string(get_attribute_value(HTML::AttributeNames::fetchpriority)).value_or(Fetch::Infrastructure::Request::Priority::Auto); @@ -490,9 +490,9 @@ void HTMLLinkElement::process_stylesheet_resource(bool success, Fetch::Infrastru m_loaded_style_sheet = parse_css_stylesheet(CSS::Parser::ParsingParams(document(), *response.url()), decoded_string); if (m_loaded_style_sheet) { - Optional location; + Optional<::URL::URL> location; if (!response.url_list().is_empty()) - location = response.url_list().first().to_string(); + location = response.url_list().first(); document_or_shadow_root_style_sheets().create_a_css_style_sheet( "text/css"_string, diff --git a/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Libraries/LibWeb/HTML/HTMLMediaElement.cpp index 249e6a0fa4c..b7366baf4af 100644 --- a/Libraries/LibWeb/HTML/HTMLMediaElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLMediaElement.cpp @@ -637,11 +637,11 @@ public: { // 2. ⌛ Process candidate: If candidate does not have a src attribute, or if its src attribute's value is the // empty string, then end the synchronous section, and jump down to the failed with elements step below. - String candiate_src; + String candidate_src; if (auto maybe_src = m_candidate->get_attribute(HTML::AttributeNames::src); maybe_src.has_value()) - candiate_src = *maybe_src; + candidate_src = *maybe_src; - if (candiate_src.is_empty()) { + if (candidate_src.is_empty()) { TRY(failed_with_elements()); return {}; } @@ -649,7 +649,7 @@ public: // 3. ⌛ Let urlString and urlRecord be the resulting URL string and the resulting URL record, respectively, that // would have resulted from parsing the URL specified by candidate's src attribute's value relative to the // candidate's node document when the src attribute was last changed. - auto url_record = m_candidate->document().parse_url(candiate_src); + auto url_record = m_candidate->document().parse_url(candidate_src); // 4. ⌛ If urlString was not obtained successfully, then end the synchronous section, and jump down to the failed // with elements step below. @@ -1008,7 +1008,7 @@ WebIDL::ExceptionOr HTMLMediaElement::fetch_resource(URL::URL const& url_r // 6. Let byteRange, which is "entire resource" or a (number, number or "until end") tuple, be the byte range required to satisfy missing data in // media data. This value is implementation-defined and may rely on codec, network conditions or other heuristics. The user-agent may determine // to fetch the resource in full, in which case byteRange would be "entire resource", to fetch from a byte offset until the end, in which case - // byteRange would be (number, "until end"), or to fetch a range between two byte offsets, im which case byteRange would be a (number, number) + // byteRange would be (number, "until end"), or to fetch a range between two byte offsets, in which case byteRange would be a (number, number) // tuple representing the two offsets. ByteRange byte_range = EntireResource {}; diff --git a/Libraries/LibWeb/HTML/HTMLMeterElement.cpp b/Libraries/LibWeb/HTML/HTMLMeterElement.cpp index e1111ba878e..87bcd2a7cdb 100644 --- a/Libraries/LibWeb/HTML/HTMLMeterElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLMeterElement.cpp @@ -197,11 +197,11 @@ void HTMLMeterElement::create_shadow_tree_if_needed() set_shadow_root(shadow_root); auto meter_bar_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - meter_bar_element->set_use_pseudo_element(CSS::PseudoElement::Track); + meter_bar_element->set_use_pseudo_element(CSS::PseudoElement::SliderTrack); MUST(shadow_root->append_child(*meter_bar_element)); m_meter_value_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_meter_value_element->set_use_pseudo_element(CSS::PseudoElement::Fill); + m_meter_value_element->set_use_pseudo_element(CSS::PseudoElement::SliderFill); MUST(meter_bar_element->append_child(*m_meter_value_element)); update_meter_value_element(); } diff --git a/Libraries/LibWeb/HTML/HTMLProgressElement.cpp b/Libraries/LibWeb/HTML/HTMLProgressElement.cpp index 325dd034b48..505ec74128b 100644 --- a/Libraries/LibWeb/HTML/HTMLProgressElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLProgressElement.cpp @@ -118,11 +118,11 @@ void HTMLProgressElement::create_shadow_tree_if_needed() set_shadow_root(shadow_root); auto progress_bar_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - progress_bar_element->set_use_pseudo_element(CSS::PseudoElement::Track); + progress_bar_element->set_use_pseudo_element(CSS::PseudoElement::SliderTrack); MUST(shadow_root->append_child(*progress_bar_element)); m_progress_value_element = MUST(DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML)); - m_progress_value_element->set_use_pseudo_element(CSS::PseudoElement::Fill); + m_progress_value_element->set_use_pseudo_element(CSS::PseudoElement::SliderFill); MUST(progress_bar_element->append_child(*m_progress_value_element)); update_progress_value_element(); } diff --git a/Libraries/LibWeb/HTML/MessagePort.cpp b/Libraries/LibWeb/HTML/MessagePort.cpp index 03fe434b6ec..97d6a808e80 100644 --- a/Libraries/LibWeb/HTML/MessagePort.cpp +++ b/Libraries/LibWeb/HTML/MessagePort.cpp @@ -288,13 +288,9 @@ void MessagePort::post_port_message(SerializedTransferRecord serialize_with_tran void MessagePort::read_from_transport() { - auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& unparsed_message) { - auto& bytes = unparsed_message.bytes; - IPC::UnprocessedFileDescriptors unprocessed_fds; - unprocessed_fds.return_fds_to_front_of_queue(move(unparsed_message.fds)); - - FixedMemoryStream stream { bytes.span(), FixedMemoryStream::Mode::ReadOnly }; - IPC::Decoder decoder { stream, unprocessed_fds }; + auto schedule_shutdown = m_transport->read_as_many_messages_as_possible_without_blocking([this](auto&& raw_message) { + FixedMemoryStream stream { raw_message.bytes.span(), FixedMemoryStream::Mode::ReadOnly }; + IPC::Decoder decoder { stream, raw_message.fds }; auto serialized_transfer_record = MUST(decoder.decode()); diff --git a/Libraries/LibWeb/HTML/MessagePort.h b/Libraries/LibWeb/HTML/MessagePort.h index 0d48fd0adfb..24e3bbeccbf 100644 --- a/Libraries/LibWeb/HTML/MessagePort.h +++ b/Libraries/LibWeb/HTML/MessagePort.h @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/Libraries/LibWeb/HTML/NavigateEvent.cpp b/Libraries/LibWeb/HTML/NavigateEvent.cpp index e14a05b001e..f61e4dc29f6 100644 --- a/Libraries/LibWeb/HTML/NavigateEvent.cpp +++ b/Libraries/LibWeb/HTML/NavigateEvent.cpp @@ -103,7 +103,7 @@ WebIDL::ExceptionOr NavigateEvent::intercept(NavigationInterceptOptions co if (m_focus_reset_behavior.has_value() && *m_focus_reset_behavior != *options.focus_reset) { auto& console = realm.intrinsics().console_object()->console(); console.output_debug_message(JS::Console::LogLevel::Warn, - TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overriden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset))); + TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overridden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset))); } // 2. Set this's focus reset behavior to options["focusReset"]. @@ -118,7 +118,7 @@ WebIDL::ExceptionOr NavigateEvent::intercept(NavigationInterceptOptions co if (m_scroll_behavior.has_value() && *m_scroll_behavior != *options.scroll) { auto& console = realm.intrinsics().console_object()->console(); console.output_debug_message(JS::Console::LogLevel::Warn, - TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overriden (was: {}, now: {})", *m_scroll_behavior, *options.scroll))); + TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overridden (was: {}, now: {})", *m_scroll_behavior, *options.scroll))); } // 2. Set this's scroll behavior to options["scroll"]. diff --git a/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h b/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h index 4a2615e7b93..83eca4e041e 100644 --- a/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h +++ b/Libraries/LibWeb/HTML/NavigatorDeviceMemory.h @@ -18,7 +18,7 @@ public: WebIDL::Double device_memory() const { // The value is calculated by using the actual device memory in MiB then rounding it to the - // nearest number where only the most signicant bit can be set and the rest are zeros + // nearest number where only the most significant bit can be set and the rest are zeros // (nearest power of two). auto memory_in_bytes = Core::System::physical_memory_bytes(); auto memory_in_mib = memory_in_bytes / MiB; diff --git a/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp b/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp index 4d860159b78..a67b9ea0b01 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLEncodingDetection.cpp @@ -384,7 +384,7 @@ ByteString run_encoding_sniffing_algorithm(DOM::Document& document, ByteBuffer c return maybe_transport_encoding; } - // 5. Optionally prescan the byte stream to determine its encoding, with the end condition being when the user agent decides that scanning further bytes would not + // 5. Optionally, prescan the byte stream to determine its encoding, with the end condition being when the user agent decides that scanning further bytes would not // be efficient. User agents are encouraged to only prescan the first 1024 bytes. User agents may decide that scanning any bytes is not efficient, in which case // these substeps are entirely skipped. // The aforementioned algorithm returns either a character encoding or failure. If it returns a character encoding, then return the same encoding, with confidence tentative. diff --git a/Libraries/LibWeb/HTML/RenderingThread.cpp b/Libraries/LibWeb/HTML/RenderingThread.cpp index 41effe7809b..89432dc462b 100644 --- a/Libraries/LibWeb/HTML/RenderingThread.cpp +++ b/Libraries/LibWeb/HTML/RenderingThread.cpp @@ -57,17 +57,17 @@ void RenderingThread::rendering_thread_loop() } auto painting_surface = painting_surface_for_backing_store(task->backing_store); - m_skia_player->execute(*task->display_list, painting_surface); + m_skia_player->execute(*task->display_list, task->scroll_state_snapshot, painting_surface); m_main_thread_event_loop.deferred_invoke([callback = move(task->callback)] { callback(); }); } } -void RenderingThread::enqueue_rendering_task(NonnullRefPtr display_list, NonnullRefPtr backing_store, Function&& callback) +void RenderingThread::enqueue_rendering_task(NonnullRefPtr display_list, Painting::ScrollStateSnapshot&& scroll_state_snapshot, NonnullRefPtr backing_store, Function&& callback) { Threading::MutexLocker const locker { m_rendering_task_mutex }; - m_rendering_tasks.enqueue(Task { move(display_list), move(backing_store), move(callback) }); + m_rendering_tasks.enqueue(Task { move(display_list), move(scroll_state_snapshot), move(backing_store), move(callback) }); m_rendering_task_ready_wake_condition.signal(); } diff --git a/Libraries/LibWeb/HTML/RenderingThread.h b/Libraries/LibWeb/HTML/RenderingThread.h index 2aecc153093..f5e0373d68f 100644 --- a/Libraries/LibWeb/HTML/RenderingThread.h +++ b/Libraries/LibWeb/HTML/RenderingThread.h @@ -28,7 +28,7 @@ public: void start(DisplayListPlayerType); void set_skia_player(OwnPtr&& player) { m_skia_player = move(player); } void set_skia_backend_context(RefPtr context) { m_skia_backend_context = move(context); } - void enqueue_rendering_task(NonnullRefPtr, NonnullRefPtr, Function&& callback); + void enqueue_rendering_task(NonnullRefPtr, Painting::ScrollStateSnapshot&&, NonnullRefPtr, Function&& callback); void clear_bitmap_to_surface_cache(); private: @@ -46,6 +46,7 @@ private: struct Task { NonnullRefPtr display_list; + Painting::ScrollStateSnapshot scroll_state_snapshot; NonnullRefPtr backing_store; Function callback; }; diff --git a/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp b/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp index e800404996b..1801eea755a 100644 --- a/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp +++ b/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp @@ -80,7 +80,7 @@ JS::Completion ClassicScript::run(RethrowErrors rethrow_errors, GC::Ptrrealm(); - // 2. Check if we can run script with realm. If this returns "do not run" then return NormalCompletion(empty). + // 2. Check if we can run script with realm. If this returns "do not run", then return NormalCompletion(empty). if (can_run_script(realm) == RunScriptDecision::DoNotRun) return JS::normal_completion(JS::js_undefined()); diff --git a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp index 24c29999f1d..5c01d278fa7 100644 --- a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp +++ b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp @@ -152,7 +152,7 @@ WebIDL::ExceptionOr resolve_module_specifier(Optional referri } } - // 11. If result is null, set result be the result of resolving an imports match given normalizedSpecifier, asURL, and importMap's imports. + // 11. If result is null, set result to the result of resolving an imports match given normalizedSpecifier, asURL, and importMap's imports. if (!result.has_value()) result = TRY(resolve_imports_match(normalized_specifier.to_byte_string(), as_url, import_map.imports())); @@ -836,37 +836,38 @@ void fetch_descendants_of_and_link_a_module_script(JS::Realm& realm, // 5. Let loadingPromise be record.LoadRequestedModules(state). auto& loading_promise = record->load_requested_modules(state); - // 6. Upon fulfillment of loadingPromise, run the following steps: - WebIDL::upon_fulfillment(loading_promise, GC::create_function(realm.heap(), [&realm, record, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { - // 1. Perform record.Link(). - auto linking_result = record->link(realm.vm()); + WebIDL::react_to_promise(loading_promise, + // 6. Upon fulfillment of loadingPromise, run the following steps: + GC::create_function(realm.heap(), [&realm, record, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { + // 1. Perform record.Link(). + auto linking_result = record->link(realm.vm()); - // If this throws an exception, set result's error to rethrow to that exception. - if (linking_result.is_throw_completion()) - module_script.set_error_to_rethrow(linking_result.release_error().value()); - - // 2. Run onComplete given moduleScript. - on_complete->function()(module_script); - - return JS::js_undefined(); - })); - - // 7. Upon rejection of loadingPromise, run the following steps: - WebIDL::upon_rejection(loading_promise, GC::create_function(realm.heap(), [state, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { - // 1. If state.[[ParseError]] is not null, set moduleScript's error to rethrow to state.[[ParseError]] and run - // onComplete given moduleScript. - if (!state->parse_error.is_null()) { - module_script.set_error_to_rethrow(state->parse_error); + // If this throws an exception, set result's error to rethrow to that exception. + if (linking_result.is_throw_completion()) + module_script.set_error_to_rethrow(linking_result.release_error().value()); + // 2. Run onComplete given moduleScript. on_complete->function()(module_script); - } - // 2. Otherwise, run onComplete given null. - else { - on_complete->function()(nullptr); - } - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), + + // 7. Upon rejection of loadingPromise, run the following steps: + GC::create_function(realm.heap(), [state, &module_script, on_complete](JS::Value) -> WebIDL::ExceptionOr { + // 1. If state.[[ParseError]] is not null, set moduleScript's error to rethrow to state.[[ParseError]] and run + // onComplete given moduleScript. + if (!state->parse_error.is_null()) { + module_script.set_error_to_rethrow(state->parse_error); + + on_complete->function()(module_script); + } + // 2. Otherwise, run onComplete given null. + else { + on_complete->function()(nullptr); + } + + return JS::js_undefined(); + })); clean_up_after_running_callback(realm); diff --git a/Libraries/LibWeb/HTML/StructuredSerialize.cpp b/Libraries/LibWeb/HTML/StructuredSerialize.cpp index cb9cce76474..d9c26e23333 100644 --- a/Libraries/LibWeb/HTML/StructuredSerialize.cpp +++ b/Libraries/LibWeb/HTML/StructuredSerialize.cpp @@ -658,7 +658,7 @@ WebIDL::ExceptionOr serialize_viewed_array_buffer(JS::VM& vm, Vector& auto buffer_serialized = TRY(structured_serialize_internal(vm, buffer, for_storage, memory)); // 4. Assert: bufferSerialized.[[Type]] is "ArrayBuffer", "ResizableArrayBuffer", "SharedArrayBuffer", or "GrowableSharedArrayBuffer". - // NOTE: Object reference + memory check is required when ArrayBuffer is transfered. + // NOTE: Object reference + memory check is required when ArrayBuffer is transferred. auto tag = buffer_serialized[0]; VERIFY(tag == ValueTag::ArrayBuffer || tag == ValueTag::ResizeableArrayBuffer diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.cpp b/Libraries/LibWeb/HTML/TraversableNavigable.cpp index ecae592a888..886858a7896 100644 --- a/Libraries/LibWeb/HTML/TraversableNavigable.cpp +++ b/Libraries/LibWeb/HTML/TraversableNavigable.cpp @@ -1426,7 +1426,8 @@ RefPtr TraversableNavigable::record_display_list(DevicePi void TraversableNavigable::start_display_list_rendering(NonnullRefPtr display_list, NonnullRefPtr backing_store, Function&& callback) { - m_rendering_thread.enqueue_rendering_task(move(display_list), move(backing_store), move(callback)); + auto scroll_state_snapshot = active_document()->paintable()->scroll_state().snapshot(); + m_rendering_thread.enqueue_rendering_task(move(display_list), move(scroll_state_snapshot), move(backing_store), move(callback)); } } diff --git a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index 5d95a07b8ee..2b226197edb 100644 --- a/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -378,7 +378,8 @@ i32 WindowOrWorkerGlobalScopeMixin::run_timer_initialization_steps(TimerHandler })); }; - // 13. Set uniqueHandle to the result of running steps after a timeout given global, "setTimeout/setInterval", timeout, completionStep. + // 13. Set uniqueHandle to the result of running steps after a timeout given global, "setTimeout/setInterval", + // timeout, and completionStep. // FIXME: run_steps_after_a_timeout() needs to be updated to return a unique internal value that can be used here. run_steps_after_a_timeout_impl(timeout, move(completion_step), id); @@ -994,7 +995,7 @@ void WindowOrWorkerGlobalScopeMixin::report_an_exception(JS::Value exception, Om if (false) { // FIXME: 1. Let workerObject be the Worker object associated with global. - // FIXME: 2. Set notHandled be the result of firing an event named error at workerObject, using ErrorEvent, + // FIXME: 2. Set notHandled to the result of firing an event named error at workerObject, using ErrorEvent, // with the cancelable attribute initialized to true, and additional attributes initialized // according to errorInfo. diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp index 808fc11d4bd..2ff917b6179 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -20,6 +21,7 @@ IDBDatabase::IDBDatabase(JS::Realm& realm, Database& db) , m_name(db.name()) , m_associated_database(db) { + m_uuid = MUST(Crypto::generate_random_uuid()); db.associate(*this); m_object_store_set = Vector> { db.object_stores() }; } @@ -42,6 +44,7 @@ void IDBDatabase::visit_edges(Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_object_store_set); visitor.visit(m_associated_database); + visitor.visit(m_transactions); } void IDBDatabase::set_onabort(WebIDL::CallbackType* event_handler) @@ -134,6 +137,9 @@ WebIDL::ExceptionOr> IDBDatabase::create_object_store(St // If keyPath is not null, set the created object store's key path to keyPath. auto object_store = ObjectStore::create(realm, database, name, auto_increment, key_path); + // AD-HOC: Add newly created object store to this's object store set. + add_to_object_store_set(object_store); + // 10. Return a new object store handle associated with store and transaction. return IDBObjectStore::create(realm, object_store, *transaction); } @@ -170,7 +176,7 @@ WebIDL::ExceptionOr IDBDatabase::delete_object_store(String const& name) // 4. Let store be the object store named name in database, or throw a "NotFoundError" DOMException if none. auto store = database->object_store_with_name(name); if (!store) - return WebIDL::NotFoundError::create(realm, "Object store not found"_string); + return WebIDL::NotFoundError::create(realm, "Object store not found while trying to delete"_string); // 5. Remove store from this's object store set. this->remove_from_object_store_set(*store); diff --git a/Libraries/LibWeb/IndexedDB/IDBDatabase.h b/Libraries/LibWeb/IndexedDB/IDBDatabase.h index 69fa58e3b57..73578505ca3 100644 --- a/Libraries/LibWeb/IndexedDB/IDBDatabase.h +++ b/Libraries/LibWeb/IndexedDB/IDBDatabase.h @@ -46,17 +46,22 @@ public: void set_close_pending(bool close_pending) { m_close_pending = close_pending; } void set_state(ConnectionState state) { m_state = state; } + [[nodiscard]] String uuid() const { return m_uuid; } [[nodiscard]] String name() const { return m_name; } [[nodiscard]] u64 version() const { return m_version; } [[nodiscard]] bool close_pending() const { return m_close_pending; } [[nodiscard]] ConnectionState state() const { return m_state; } [[nodiscard]] GC::Ref associated_database() { return m_associated_database; } [[nodiscard]] ReadonlySpan> object_store_set() { return m_object_store_set; } + void add_to_object_store_set(GC::Ref object_store) { m_object_store_set.append(object_store); } void remove_from_object_store_set(GC::Ref object_store) { m_object_store_set.remove_first_matching([&](auto& entry) { return entry == object_store; }); } + [[nodiscard]] ReadonlySpan> transactions() { return m_transactions; } + void add_transaction(GC::Ref transaction) { m_transactions.append(transaction); } + [[nodiscard]] GC::Ref object_store_names(); WebIDL::ExceptionOr> create_object_store(String const&, IDBObjectStoreParameters const&); WebIDL::ExceptionOr delete_object_store(String const&); @@ -95,6 +100,12 @@ private: // NOTE: There is an associated database in the spec, but there is no mention where it is assigned, nor where its from // So we stash the one we have when opening a connection. GC::Ref m_associated_database; + + // NOTE: We need to keep track of what transactions were created by this connection + Vector> m_transactions; + + // NOTE: Used for debug purposes + String m_uuid; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.cpp b/Libraries/LibWeb/IndexedDB/IDBIndex.cpp index 9d11c0ec8dd..86cb1b9be33 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -15,14 +16,17 @@ GC_DEFINE_ALLOCATOR(IDBIndex); IDBIndex::~IDBIndex() = default; -IDBIndex::IDBIndex(JS::Realm& realm) +IDBIndex::IDBIndex(JS::Realm& realm, GC::Ref index, GC::Ref object_store) : PlatformObject(realm) + , m_index(index) + , m_object_store_handle(object_store) + , m_name(index->name()) { } -GC::Ref IDBIndex::create(JS::Realm& realm) +GC::Ref IDBIndex::create(JS::Realm& realm, GC::Ref index, GC::Ref object_store) { - return realm.create(realm); + return realm.create(realm, index, object_store); } void IDBIndex::initialize(JS::Realm& realm) @@ -31,4 +35,70 @@ void IDBIndex::initialize(JS::Realm& realm) WEB_SET_PROTOTYPE_FOR_INTERFACE(IDBIndex); } +void IDBIndex::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_index); + visitor.visit(m_object_store_handle); +} + +// https://w3c.github.io/IndexedDB/#dom-idbindex-name +WebIDL::ExceptionOr IDBIndex::set_name(String const& value) +{ + auto& realm = this->realm(); + + // 1. Let name be the given value. + auto const& name = value; + + // 2. Let transaction be this’s transaction. + auto transaction = this->transaction(); + + // 3. Let index be this’s index. + auto index = this->index(); + + // 4. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException. + if (!transaction->is_upgrade_transaction()) + return WebIDL::InvalidStateError::create(realm, "Transaction is not an upgrade transaction"_string); + + // 5. If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException. + if (transaction->state() != IDBTransaction::TransactionState::Active) + return WebIDL::TransactionInactiveError::create(realm, "Transaction is not active"_string); + + // FIXME: 6. If index or index’s object store has been deleted, throw an "InvalidStateError" DOMException. + + // 7. If index’s name is equal to name, terminate these steps. + if (index->name() == name) + return {}; + + // 8. If an index named name already exists in index’s object store, throw a "ConstraintError" DOMException. + if (index->object_store()->index_set().contains(name)) + return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_string); + + // 9. Set index’s name to name. + index->set_name(name); + + // NOTE: Update the key in the map so it still matches the name + auto old_value = m_object_store_handle->index_set().take(m_name).release_value(); + m_object_store_handle->index_set().set(name, old_value); + + // 10. Set this’s name to name. + m_name = name; + + return {}; +} + +// https://w3c.github.io/IndexedDB/#dom-idbindex-keypath +JS::Value IDBIndex::key_path() const +{ + return m_index->key_path().visit( + [&](String const& value) -> JS::Value { + return JS::PrimitiveString::create(realm().vm(), value); + }, + [&](Vector const& value) -> JS::Value { + return JS::Array::create_from(realm(), value.span(), [&](auto const& entry) -> JS::Value { + return JS::PrimitiveString::create(realm().vm(), entry); + }); + }); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.h b/Libraries/LibWeb/IndexedDB/IDBIndex.h index 3e1b080a6db..ba8ef9f4815 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.h +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.h @@ -8,6 +8,8 @@ #include #include +#include +#include namespace Web::IndexedDB { @@ -18,11 +20,32 @@ class IDBIndex : public Bindings::PlatformObject { public: virtual ~IDBIndex() override; - [[nodiscard]] static GC::Ref create(JS::Realm&); + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, GC::Ref); + + WebIDL::ExceptionOr set_name(String const& value); + String name() const { return m_name; } + GC::Ref object_store() { return m_object_store_handle; } + JS::Value key_path() const; + bool multi_entry() const { return m_index->multi_entry(); } + bool unique() const { return m_index->unique(); } + + // The transaction of an index handle is the transaction of its associated object store handle. + GC::Ref transaction() { return m_object_store_handle->transaction(); } + GC::Ref index() { return m_index; } + GC::Ref store() { return m_object_store_handle; } protected: - explicit IDBIndex(JS::Realm&); + explicit IDBIndex(JS::Realm&, GC::Ref, GC::Ref); virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor& visitor) override; + +private: + // An index handle has an associated index and an associated object store handle. + GC::Ref m_index; + GC::Ref m_object_store_handle; + + // An index handle has a name, which is initialized to the name of the associated index when the index handle is created. + String m_name; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBIndex.idl b/Libraries/LibWeb/IndexedDB/IDBIndex.idl index a096122f40d..85e0ae37745 100644 --- a/Libraries/LibWeb/IndexedDB/IDBIndex.idl +++ b/Libraries/LibWeb/IndexedDB/IDBIndex.idl @@ -2,11 +2,11 @@ [Exposed=(Window,Worker)] interface IDBIndex { - [FIXME] attribute DOMString name; - [FIXME, SameObject] readonly attribute IDBObjectStore objectStore; - [FIXME] readonly attribute any keyPath; - [FIXME] readonly attribute boolean multiEntry; - [FIXME] readonly attribute boolean unique; + attribute DOMString name; + [SameObject] readonly attribute IDBObjectStore objectStore; + readonly attribute any keyPath; + readonly attribute boolean multiEntry; + readonly attribute boolean unique; [FIXME, NewObject] IDBRequest get(any query); [FIXME, NewObject] IDBRequest getKey(any query); [FIXME, NewObject] IDBRequest getAll(optional any query, optional [EnforceRange] unsigned long count); diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp index fbbcbe1c205..aed485032f7 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Web::IndexedDB { @@ -41,6 +42,7 @@ void IDBObjectStore::visit_edges(Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_store); visitor.visit(m_transaction); + visitor.visit(m_indexes); } // https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath @@ -101,4 +103,123 @@ WebIDL::ExceptionOr IDBObjectStore::set_name(String const& value) return {}; } +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-createindex +WebIDL::ExceptionOr> IDBObjectStore::create_index(String const& name, KeyPath key_path, IDBIndexParameters options) +{ + auto& realm = this->realm(); + + // 1. Let transaction be this's transaction. + auto transaction = this->transaction(); + + // 2. Let store be this's object store. + auto store = this->store(); + + // 3. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException. + if (transaction->mode() != Bindings::IDBTransactionMode::Versionchange) + return WebIDL::InvalidStateError::create(realm, "Transaction is not an upgrade transaction"_string); + + // FIXME: 4. If store has been deleted, throw an "InvalidStateError" DOMException. + + // 5. If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException. + if (transaction->state() != IDBTransaction::TransactionState::Active) + return WebIDL::TransactionInactiveError::create(realm, "Transaction is not active while creating index"_string); + + // 6. If an index named name already exists in store, throw a "ConstraintError" DOMException. + if (store->index_set().contains(name)) + return WebIDL::ConstraintError::create(realm, "An index with the given name already exists"_string); + + // 7. If keyPath is not a valid key path, throw a "SyntaxError" DOMException. + if (!is_valid_key_path(key_path)) + return WebIDL::SyntaxError::create(realm, "Key path is not valid"_string); + + // 8. Let unique be options’s unique member. + auto unique = options.unique; + + // 9. Let multiEntry be options’s multiEntry member. + auto multi_entry = options.multi_entry; + + // 10. If keyPath is a sequence and multiEntry is true, throw an "InvalidAccessError" DOMException. + if (key_path.has>() && multi_entry) + return WebIDL::InvalidAccessError::create(realm, "Key path is a sequence and multiEntry is true"_string); + + // 11. Let index be a new index in store. + // Set index’s name to name, key path to keyPath, unique flag to unique, and multiEntry flag to multiEntry. + auto index = Index::create(realm, store, name, key_path, unique, multi_entry); + + // 12. Add index to this's index set. + this->index_set().set(name, index); + + // 13. Return a new index handle associated with index and this. + return IDBIndex::create(realm, index, *this); +} + +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames +GC::Ref IDBObjectStore::index_names() +{ + // 1. Let names be a list of the names of the indexes in this's index set. + Vector names; + for (auto const& [name, index] : m_indexes) + names.append(name); + + // 2. Return the result (a DOMStringList) of creating a sorted name list with names. + return create_a_sorted_name_list(realm(), names); +} + +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index +WebIDL::ExceptionOr> IDBObjectStore::index(String const& name) +{ + // 1. Let transaction be this’s transaction. + auto transaction = this->transaction(); + + // 2. Let store be this’s object store. + [[maybe_unused]] auto store = this->store(); + + // FIXME: 3. If store has been deleted, throw an "InvalidStateError" DOMException. + + // 4. If transaction’s state is finished, then throw an "InvalidStateError" DOMException. + if (transaction->state() == IDBTransaction::TransactionState::Finished) + return WebIDL::InvalidStateError::create(realm(), "Transaction is finished"_string); + + // 5. Let index be the index named name in this’s index set if one exists, or throw a "NotFoundError" DOMException otherwise. + auto index = m_indexes.get(name); + if (!index.has_value()) + return WebIDL::NotFoundError::create(realm(), "Index not found"_string); + + // 6. Return an index handle associated with index and this. + return IDBIndex::create(realm(), *index, *this); +} + +// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-deleteindex +WebIDL::ExceptionOr IDBObjectStore::delete_index(String const& name) +{ + // 1. Let transaction be this’s transaction. + auto transaction = this->transaction(); + + // 2. Let store be this’s object store. + auto store = this->store(); + + // 3. If transaction is not an upgrade transaction, throw an "InvalidStateError" DOMException. + if (transaction->mode() != Bindings::IDBTransactionMode::Versionchange) + return WebIDL::InvalidStateError::create(realm(), "Transaction is not an upgrade transaction"_string); + + // FIXME: 4. If store has been deleted, throw an "InvalidStateError" DOMException. + + // 5. If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException. + if (transaction->state() != IDBTransaction::TransactionState::Active) + return WebIDL::TransactionInactiveError::create(realm(), "Transaction is not active"_string); + + // 6. Let index be the index named name in store if one exists, or throw a "NotFoundError" DOMException otherwise. + auto index = m_indexes.get(name); + if (!index.has_value()) + return WebIDL::NotFoundError::create(realm(), "Index not found"_string); + + // 7. Remove index from this’s index set. + m_indexes.remove(name); + + // 8. Destroy index. + store->index_set().remove(name); + + return {}; +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h index c22fd5081ee..0fab61694be 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -13,6 +14,11 @@ namespace Web::IndexedDB { +struct IDBIndexParameters { + bool unique { false }; + bool multi_entry { false }; +}; + // https://w3c.github.io/IndexedDB/#object-store-interface // https://w3c.github.io/IndexedDB/#object-store-handle-construct class IDBObjectStore : public Bindings::PlatformObject { @@ -23,14 +29,20 @@ public: virtual ~IDBObjectStore() override; [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, GC::Ref); - JS::Value key_path() const; - GC::Ref transaction() const { return m_transaction; } - // https://w3c.github.io/IndexedDB/#dom-idbobjectstore-autoincrement // The autoIncrement getter steps are to return true if this’s object store has a key generator, and false otherwise. bool auto_increment() const { return m_store->key_generator().has_value(); } + JS::Value key_path() const; String name() const { return m_name; } WebIDL::ExceptionOr set_name(String const& value); + GC::Ref transaction() const { return m_transaction; } + GC::Ref store() const { return m_store; } + AK::HashMap>& index_set() { return m_indexes; } + + WebIDL::ExceptionOr> create_index(String const&, KeyPath, IDBIndexParameters options); + [[nodiscard]] GC::Ref index_names(); + WebIDL::ExceptionOr> index(String const&); + WebIDL::ExceptionOr delete_index(String const&); protected: explicit IDBObjectStore(JS::Realm&, GC::Ref, GC::Ref); @@ -44,6 +56,9 @@ private: // An object store handle has a name, which is initialized to the name of the associated object store when the object store handle is created. String m_name; + + // An object store handle has an index set + AK::HashMap> m_indexes; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl index 66dd3ef722d..90d51d3d686 100644 --- a/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl +++ b/Libraries/LibWeb/IndexedDB/IDBObjectStore.idl @@ -6,7 +6,7 @@ interface IDBObjectStore { attribute DOMString name; readonly attribute any keyPath; - [FIXME] readonly attribute DOMStringList indexNames; + readonly attribute DOMStringList indexNames; [SameObject] readonly attribute IDBTransaction transaction; readonly attribute boolean autoIncrement; @@ -21,9 +21,9 @@ interface IDBObjectStore { [FIXME, NewObject] IDBRequest count(optional any query); [FIXME, NewObject] IDBRequest openCursor(optional any query, optional IDBCursorDirection direction = "next"); [FIXME, NewObject] IDBRequest openKeyCursor(optional any query, optional IDBCursorDirection direction = "next"); - [FIXME] IDBIndex index(DOMString name); - [FIXME, NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence) keyPath, optional IDBIndexParameters options = {}); - [FIXME] undefined deleteIndex(DOMString name); + IDBIndex index(DOMString name); + [NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence) keyPath, optional IDBIndexParameters options = {}); + undefined deleteIndex(DOMString name); }; dictionary IDBIndexParameters { diff --git a/Libraries/LibWeb/IndexedDB/IDBRequest.cpp b/Libraries/LibWeb/IndexedDB/IDBRequest.cpp index c1d5b58f29b..25bb6febff2 100644 --- a/Libraries/LibWeb/IndexedDB/IDBRequest.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBRequest.cpp @@ -7,8 +7,10 @@ */ #include +#include #include #include +#include namespace Web::IndexedDB { @@ -20,6 +22,7 @@ IDBRequest::IDBRequest(JS::Realm& realm, IDBRequestSource source) : EventTarget(realm) , m_source(source) { + m_uuid = MUST(Crypto::generate_random_uuid()); } void IDBRequest::initialize(JS::Realm& realm) diff --git a/Libraries/LibWeb/IndexedDB/IDBRequest.h b/Libraries/LibWeb/IndexedDB/IDBRequest.h index e3031a22ccb..a1c6509952f 100644 --- a/Libraries/LibWeb/IndexedDB/IDBRequest.h +++ b/Libraries/LibWeb/IndexedDB/IDBRequest.h @@ -10,7 +10,7 @@ #include #include -#include +#include namespace Web::IndexedDB { @@ -30,6 +30,7 @@ public: [[nodiscard]] bool processed() const { return m_processed; } [[nodiscard]] IDBRequestSource source() const { return m_source; } [[nodiscard]] GC::Ptr transaction() const { return m_transaction; } + [[nodiscard]] String uuid() const { return m_uuid; } [[nodiscard]] Bindings::IDBRequestReadyState ready_state() const; [[nodiscard]] WebIDL::ExceptionOr> error() const; @@ -56,15 +57,22 @@ protected: private: // A request has a processed flag which is initially false. bool m_processed { false }; + // A request has a done flag which is initially false. bool m_done { false }; + // A request has a result and an error JS::Value m_result; GC::Ptr m_error; + // A request has a source object. IDBRequestSource m_source; + // A request has a transaction which is initially null. GC::Ptr m_transaction; + + // NOTE: Used for debug purposes + String m_uuid; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index e58115f3edd..6dc8ffcfccf 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -1,11 +1,13 @@ /* - * Copyright (c) 2024, stelar7 + * Copyright (c) 2024-2025, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include #include +#include #include #include @@ -15,15 +17,20 @@ GC_DEFINE_ALLOCATOR(IDBTransaction); IDBTransaction::~IDBTransaction() = default; -IDBTransaction::IDBTransaction(JS::Realm& realm, GC::Ref database) +IDBTransaction::IDBTransaction(JS::Realm& realm, GC::Ref connection, Bindings::IDBTransactionMode mode, Bindings::IDBTransactionDurability durability, Vector> scopes) : EventTarget(realm) - , m_connection(database) + , m_connection(connection) + , m_mode(mode) + , m_durability(durability) + , m_scope(move(scopes)) { + m_uuid = MUST(Crypto::generate_random_uuid()); + connection->add_transaction(*this); } -GC::Ref IDBTransaction::create(JS::Realm& realm, GC::Ref database) +GC::Ref IDBTransaction::create(JS::Realm& realm, GC::Ref connection, Bindings::IDBTransactionMode mode, Bindings::IDBTransactionDurability durability = Bindings::IDBTransactionDurability::Default, Vector> scopes = {}) { - return realm.create(realm, database); + return realm.create(realm, connection, mode, durability, move(scopes)); } void IDBTransaction::initialize(JS::Realm& realm) @@ -38,6 +45,8 @@ void IDBTransaction::visit_edges(Visitor& visitor) visitor.visit(m_connection); visitor.visit(m_error); visitor.visit(m_associated_request); + visitor.visit(m_scope); + visitor.visit(m_cleanup_event_loop); } void IDBTransaction::set_onabort(WebIDL::CallbackType* event_handler) @@ -70,6 +79,7 @@ WebIDL::CallbackType* IDBTransaction::onerror() return event_handler_attribute(HTML::EventNames::error); } +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-abort WebIDL::ExceptionOr IDBTransaction::abort() { // 1. If this's state is committing or finished, then throw an "InvalidStateError" DOMException. @@ -82,4 +92,59 @@ WebIDL::ExceptionOr IDBTransaction::abort() return {}; } +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstorenames +GC::Ref IDBTransaction::object_store_names() +{ + // 1. Let names be a list of the names of the object stores in this's scope. + Vector names; + for (auto const& object_store : this->scope()) + names.append(object_store->name()); + + // 2. Return the result (a DOMStringList) of creating a sorted name list with names. + return create_a_sorted_name_list(realm(), names); +} + +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit +WebIDL::ExceptionOr IDBTransaction::commit() +{ + auto& realm = this->realm(); + + // 1. If this's state is not active, then throw an "InvalidStateError" DOMException. + if (m_state != TransactionState::Active) + return WebIDL::InvalidStateError::create(realm, "Transaction is not active while commiting"_string); + + // 2. Run commit a transaction with this. + commit_a_transaction(realm, *this); + + return {}; +} + +GC::Ptr IDBTransaction::object_store_named(String const& name) const +{ + for (auto const& store : m_scope) { + if (store->name() == name) + return store; + } + + return nullptr; +} + +// https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore +WebIDL::ExceptionOr> IDBTransaction::object_store(String const& name) +{ + auto& realm = this->realm(); + + // 1. If this's state is finished, then throw an "InvalidStateError" DOMException. + if (m_state == TransactionState::Finished) + return WebIDL::InvalidStateError::create(realm, "Transaction is finished"_string); + + // 2. Let store be the object store named name in this's scope, or throw a "NotFoundError" DOMException if none. + auto store = object_store_named(name); + if (!store) + return WebIDL::NotFoundError::create(realm, "Object store not found in transactions scope"_string); + + // 3. Return an object store handle associated with store and this. + return IDBObjectStore::create(realm, *store, *this); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index 14881a69dd0..1894771363a 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -1,17 +1,22 @@ /* - * Copyright (c) 2024, stelar7 + * Copyright (c) 2024-2025, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include #include #include #include #include #include +#include #include +#include +#include +#include namespace Web::IndexedDB { @@ -30,7 +35,7 @@ class IDBTransaction : public DOM::EventTarget { public: virtual ~IDBTransaction() override; - [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref); + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector>); [[nodiscard]] Bindings::IDBTransactionMode mode() const { return m_mode; } [[nodiscard]] TransactionState state() const { return m_state; } [[nodiscard]] GC::Ptr error() const { return m_error; } @@ -38,18 +43,27 @@ public: [[nodiscard]] Bindings::IDBTransactionDurability durability() const { return m_durability; } [[nodiscard]] GC::Ptr associated_request() const { return m_associated_request; } [[nodiscard]] bool aborted() const { return m_aborted; } + [[nodiscard]] GC::Ref object_store_names(); + [[nodiscard]] RequestList& request_list() { return m_request_list; } + [[nodiscard]] ReadonlySpan> scope() const { return m_scope; } + [[nodiscard]] String uuid() const { return m_uuid; } void set_mode(Bindings::IDBTransactionMode mode) { m_mode = mode; } - void set_state(TransactionState state) { m_state = state; } void set_error(GC::Ptr error) { m_error = error; } void set_associated_request(GC::Ptr request) { m_associated_request = request; } void set_aborted(bool aborted) { m_aborted = aborted; } + void set_state(TransactionState state) { m_state = state; } [[nodiscard]] bool is_upgrade_transaction() const { return m_mode == Bindings::IDBTransactionMode::Versionchange; } [[nodiscard]] bool is_readonly() const { return m_mode == Bindings::IDBTransactionMode::Readonly; } [[nodiscard]] bool is_readwrite() const { return m_mode == Bindings::IDBTransactionMode::Readwrite; } + [[nodiscard]] bool is_finished() const { return m_state == TransactionState::Finished; } + + GC::Ptr object_store_named(String const& name) const; WebIDL::ExceptionOr abort(); + WebIDL::ExceptionOr commit(); + WebIDL::ExceptionOr> object_store(String const& name); void set_onabort(WebIDL::CallbackType*); WebIDL::CallbackType* onabort(); @@ -59,18 +73,42 @@ public: WebIDL::CallbackType* onerror(); protected: - explicit IDBTransaction(JS::Realm&, GC::Ref); + explicit IDBTransaction(JS::Realm&, GC::Ref, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector>); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Visitor& visitor) override; private: + // AD-HOC: The transaction has a connection GC::Ref m_connection; + + // A transaction has a mode that determines which types of interactions can be performed upon that transaction. Bindings::IDBTransactionMode m_mode; + + // A transaction has a durability hint. This is a hint to the user agent of whether to prioritize performance or durability when committing the transaction. Bindings::IDBTransactionDurability m_durability { Bindings::IDBTransactionDurability::Default }; + + // A transaction has a state TransactionState m_state; + + // A transaction has a error which is set if the transaction is aborted. GC::Ptr m_error; + // A transaction has an associated upgrade request GC::Ptr m_associated_request; + + // AD-HOC: We need to track abort state separately, since we cannot rely on only the error. bool m_aborted { false }; + + // A transaction has a scope which is a set of object stores that the transaction may interact with. + Vector> m_scope; + + // A transaction has a request list of pending requests which have been made against the transaction. + RequestList m_request_list; + + // A transaction optionally has a cleanup event loop which is an event loop. + GC::Ptr m_cleanup_event_loop; + + // NOTE: Used for debug purposes + String m_uuid; }; } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl index bba6179e240..d31e70bf604 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.idl +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.idl @@ -4,13 +4,13 @@ [Exposed=(Window,Worker)] interface IDBTransaction : EventTarget { - [FIXME] readonly attribute DOMStringList objectStoreNames; + readonly attribute DOMStringList objectStoreNames; readonly attribute IDBTransactionMode mode; readonly attribute IDBTransactionDurability durability; - [FIXME, SameObject] readonly attribute IDBDatabase db; + [SameObject, ImplementedAs=connection] readonly attribute IDBDatabase db; readonly attribute DOMException? error; - [FIXME] IDBObjectStore objectStore(DOMString name); - [FIXME] undefined commit(); + IDBObjectStore objectStore(DOMString name); + undefined commit(); undefined abort(); attribute EventHandler onabort; diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 6708cae50c8..bcc208cac5e 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, stelar7 + * Copyright (c) 2024-2025, stelar7 * * SPDX-License-Identifier: BSD-2-Clause */ @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -36,9 +38,18 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 2. Add request to queue. queue.append(request); + dbgln_if(IDB_DEBUG, "open_a_database_connection: added request {} to queue", request->uuid()); // 3. Wait until all previous requests in queue have been processed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [queue, request]() { + if constexpr (IDB_DEBUG) { + dbgln("open_a_database_connection: waiting for step 3"); + dbgln("requests in queue:"); + for (auto const& item : queue) { + dbgln("[{}] - {} = {}", item == request ? "x"sv : " "sv, item->uuid(), item->processed() ? "processed"sv : "not processed"sv); + } + } + return queue.all_previous_requests_processed(request); })); @@ -71,6 +82,7 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 8. Let connection be a new connection to db. auto connection = IDBDatabase::create(realm, *db); + dbgln_if(IDB_DEBUG, "Created new connection with UUID: {}", connection->uuid()); // 9. Set connection’s version to version. connection->set_version(version); @@ -97,6 +109,11 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 3. Wait for all of the events to be fired. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [&events_to_fire, &events_fired]() { + if constexpr (IDB_DEBUG) { + dbgln("open_a_database_connection: waiting for step 10.3"); + dbgln("events_fired: {}, events_to_fire: {}", events_fired, events_to_fire); + } + return events_fired == events_to_fire; })); @@ -112,6 +129,14 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& // 5. Wait until all connections in openConnections are closed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [open_connections]() { + if constexpr (IDB_DEBUG) { + dbgln("open_a_database_connection: waiting for step 10.5"); + dbgln("open connections: {}", open_connections.size()); + for (auto const& connection : open_connections) { + dbgln(" - {}", connection->uuid()); + } + } + for (auto const& entry : open_connections) { if (entry->state() != IDBDatabase::ConnectionState::Closed) { return false; @@ -126,14 +151,13 @@ WebIDL::ExceptionOr> open_a_database_connection(JS::Realm& auto upgrade_transaction = upgrade_a_database(realm, connection, version, request); // 7. If connection was closed, return a newly created "AbortError" DOMException and abort these steps. - if (connection->state() == IDBDatabase::ConnectionState::Closed) { + if (connection->state() == IDBDatabase::ConnectionState::Closed) return WebIDL::AbortError::create(realm, "Connection was closed"_string); - } // 8. If the upgrade transaction was aborted, run the steps to close a database connection with connection, // return a newly created "AbortError" DOMException and abort these steps. if (upgrade_transaction->aborted()) { - close_a_database_connection(*connection, true); + close_a_database_connection(*connection); return WebIDL::AbortError::create(realm, "Upgrade transaction was aborted"_string); } } @@ -291,18 +315,43 @@ ErrorOr> convert_a_value_to_a_key(JS::Realm& realm, JS::Value input } // https://w3c.github.io/IndexedDB/#close-a-database-connection -void close_a_database_connection(IDBDatabase& connection, bool forced) +void close_a_database_connection(GC::Ref connection, bool forced) { - // 1. Set connection’s close pending flag to true. - connection.set_close_pending(true); + auto& realm = connection->realm(); - // FIXME: 2. If the forced flag is true, then for each transaction created using connection run abort a transaction with transaction and newly created "AbortError" DOMException. - // FIXME: 3. Wait for all transactions created using connection to complete. Once they are complete, connection is closed. - connection.set_state(IDBDatabase::ConnectionState::Closed); + // 1. Set connection’s close pending flag to true. + connection->set_close_pending(true); + + // 2. If the forced flag is true, then for each transaction created using connection run abort a transaction with transaction and newly created "AbortError" DOMException. + if (forced) { + for (auto const& transaction : connection->transactions()) { + abort_a_transaction(*transaction, WebIDL::AbortError::create(realm, "Connection was closed"_string)); + } + } + + // 3. Wait for all transactions created using connection to complete. Once they are complete, connection is closed. + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [connection]() { + if constexpr (IDB_DEBUG) { + dbgln("close_a_database_connection: waiting for step 3"); + dbgln("transactions created using connection:"); + for (auto const& transaction : connection->transactions()) { + dbgln(" - {} - {}", transaction->uuid(), (u8)transaction->state()); + } + } + + for (auto const& transaction : connection->transactions()) { + if (!transaction->is_finished()) + return false; + } + + return true; + })); + + connection->set_state(IDBDatabase::ConnectionState::Closed); // 4. If the forced flag is true, then fire an event named close at connection. if (forced) - connection.dispatch_event(DOM::Event::create(connection.realm(), HTML::EventNames::close)); + connection->dispatch_event(DOM::Event::create(realm, HTML::EventNames::close)); } // https://w3c.github.io/IndexedDB/#upgrade-a-database @@ -312,9 +361,9 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refassociated_database(); // 2. Let transaction be a new upgrade transaction with connection used as connection. - auto transaction = IDBTransaction::create(realm, connection); - - // FIXME: 3. Set transaction’s scope to connection’s object store set. + // 3. Set transaction’s scope to connection’s object store set. + auto transaction = IDBTransaction::create(realm, connection, Bindings::IDBTransactionMode::Versionchange, Bindings::IDBTransactionDurability::Default, Vector> { connection->object_store_set() }); + dbgln_if(IDB_DEBUG, "Created new upgrade transaction with UUID: {}", transaction->uuid()); // 4. Set db’s upgrade transaction to transaction. db->set_upgrade_transaction(transaction); @@ -334,8 +383,7 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refset_processed(true); // 10. Queue a task to run these steps: - IGNORE_USE_IN_ESCAPING_LAMBDA bool wait_for_transaction = true; - HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(realm.vm().heap(), [&realm, request, connection, transaction, old_version, version, &wait_for_transaction]() { + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(realm.vm().heap(), [&realm, request, connection, transaction, old_version, version]() { // 1. Set request’s result to connection. request->set_result(connection); @@ -353,19 +401,31 @@ GC::Ref upgrade_a_database(JS::Realm& realm, GC::Refset_state(IDBTransaction::TransactionState::Inactive); + // AD-HOC: If the transaction was aborted by the event, then DONT set the transaction back to inactive. + // https://github.com/w3c/IndexedDB/issues/436#issuecomment-2791113467 + if (transaction->state() != IDBTransaction::TransactionState::Finished) { - // 7. If didThrow is true, run abort a transaction with transaction and a newly created "AbortError" DOMException. - if (did_throw) - abort_a_transaction(*transaction, WebIDL::AbortError::create(realm, "Version change event threw an exception"_string)); + // 6. Set transaction’s state to inactive. + transaction->set_state(IDBTransaction::TransactionState::Inactive); - wait_for_transaction = false; + // 7. If didThrow is true, run abort a transaction with transaction and a newly created "AbortError" DOMException. + if (did_throw) + abort_a_transaction(transaction, WebIDL::AbortError::create(realm, "Version change event threw an exception"_string)); + + // AD-HOC: + // The implementation must attempt to commit a transaction when all requests placed against the transaction have completed + // and their returned results handled, + // no new requests have been placed against the transaction, + // and the transaction has not been aborted. + if (transaction->state() == IDBTransaction::TransactionState::Inactive && transaction->request_list().is_empty() && !transaction->aborted()) + commit_a_transaction(realm, transaction); + } })); // 11. Wait for transaction to finish. - HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [&wait_for_transaction]() { - return !wait_for_transaction; + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [transaction]() { + dbgln_if(IDB_DEBUG, "upgrade_a_database: waiting for step 11"); + return transaction->is_finished(); })); return transaction; @@ -379,9 +439,18 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage // 2. Add request to queue. queue.append(request); + dbgln_if(IDB_DEBUG, "delete_a_database: added request {} to queue", request->uuid()); // 3. Wait until all previous requests in queue have been processed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [queue, request]() { + if constexpr (IDB_DEBUG) { + dbgln("delete_a_database: waiting for step 3"); + dbgln("requests in queue:"); + for (auto const& item : queue) { + dbgln("[{}] - {} = {}", item == request ? "x"sv : " "sv, item->uuid(), item->processed() ? "processed"sv : "not processed"sv); + } + } + return queue.all_previous_requests_processed(request); })); @@ -412,6 +481,11 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage // 7. Wait for all of the events to be fired. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [&events_to_fire, &events_fired]() { + if constexpr (IDB_DEBUG) { + dbgln("delete_a_database: waiting for step 7"); + dbgln("events_fired: {}, events_to_fire: {}", events_fired, events_to_fire); + } + return events_fired == events_to_fire; })); @@ -426,6 +500,14 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage // 9. Wait until all connections in openConnections are closed. HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [open_connections]() { + if constexpr (IDB_DEBUG) { + dbgln("delete_a_database: waiting for step 9"); + dbgln("open connections: {}", open_connections.size()); + for (auto const& connection : open_connections) { + dbgln(" - {}", connection->uuid()); + } + } + for (auto const& entry : open_connections) { if (entry->state() != IDBDatabase::ConnectionState::Closed) { return false; @@ -448,10 +530,11 @@ WebIDL::ExceptionOr delete_a_database(JS::Realm& realm, StorageAPI::Storage } // https://w3c.github.io/IndexedDB/#abort-a-transaction -void abort_a_transaction(IDBTransaction& transaction, GC::Ptr error) +void abort_a_transaction(GC::Ref transaction, GC::Ptr error) { // NOTE: This is not spec'ed anywhere, but we need to know IF the transaction was aborted. - transaction.set_aborted(true); + transaction->set_aborted(true); + dbgln_if(IDB_DEBUG, "abort_a_transaction: transaction {} is aborting", transaction->uuid()); // FIXME: 1. All the changes made to the database by the transaction are reverted. // For upgrade transactions this includes changes to the set of object stores and indexes, as well as the change to the version. @@ -462,43 +545,59 @@ void abort_a_transaction(IDBTransaction& transaction, GC::Ptrset_state(IDBTransaction::TransactionState::Finished); // 4. If error is not null, set transaction’s error to error. if (error) - transaction.set_error(error); + transaction->set_error(error); - // FIXME: 5. For each request of transaction’s request list, abort the steps to asynchronously execute a request for request, - // set request’s processed flag to true, and queue a task to run these steps: - // FIXME: 1. Set request’s done flag to true. - // FIXME: 2. Set request’s result to undefined. - // FIXME: 3. Set request’s error to a newly created "AbortError" DOMException. - // FIXME: 4. Fire an event named error at request with its bubbles and cancelable attributes initialized to true. + // 5. For each request of transaction’s request list, + for (auto const& request : transaction->request_list()) { + // FIXME: abort the steps to asynchronously execute a request for request, + + // set request’s processed flag to true + request->set_processed(true); + + // and queue a task to run these steps: + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction->realm().vm().heap(), [request]() { + // 1. Set request’s done flag to true. + request->set_done(true); + + // 2. Set request’s result to undefined. + request->set_result(JS::js_undefined()); + + // 3. Set request’s error to a newly created "AbortError" DOMException. + request->set_error(WebIDL::AbortError::create(request->realm(), "Transaction was aborted"_string)); + + // 4. Fire an event named error at request with its bubbles and cancelable attributes initialized to true. + request->dispatch_event(DOM::Event::create(request->realm(), HTML::EventNames::error, { .bubbles = true, .cancelable = true })); + })); + } // 6. Queue a task to run these steps: - HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction.realm().vm().heap(), [&transaction]() { + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction->realm().vm().heap(), [transaction]() { // 1. If transaction is an upgrade transaction, then set transaction’s connection's associated database's upgrade transaction to null. - if (transaction.is_upgrade_transaction()) - transaction.connection()->associated_database()->set_upgrade_transaction(nullptr); + if (transaction->is_upgrade_transaction()) + transaction->connection()->associated_database()->set_upgrade_transaction(nullptr); // 2. Fire an event named abort at transaction with its bubbles attribute initialized to true. - transaction.dispatch_event(DOM::Event::create(transaction.realm(), HTML::EventNames::abort, { .bubbles = true })); + transaction->dispatch_event(DOM::Event::create(transaction->realm(), HTML::EventNames::abort, { .bubbles = true })); // 3. If transaction is an upgrade transaction, then: - if (transaction.is_upgrade_transaction()) { + if (transaction->is_upgrade_transaction()) { // 1. Let request be the open request associated with transaction. - auto request = transaction.associated_request(); + auto request = transaction->associated_request(); // 2. Set request’s transaction to null. // NOTE: Clear the two-way binding. request->set_transaction(nullptr); - transaction.set_associated_request(nullptr); + transaction->set_associated_request(nullptr); // 3. Set request’s result to undefined. request->set_result(JS::js_undefined()); // 4. Set request’s processed flag to false. - request->set_processed(false); + // FIXME: request->set_processed(false); // 5. Set request’s done flag to false. request->set_done(false); @@ -635,4 +734,60 @@ GC::Ref create_a_sorted_name_list(JS::Realm& realm, Vector< return HTML::DOMStringList::create(realm, names); } +// https://w3c.github.io/IndexedDB/#commit-a-transaction +void commit_a_transaction(JS::Realm& realm, GC::Ref transaction) +{ + // 1. Set transaction’s state to committing. + transaction->set_state(IDBTransaction::TransactionState::Committing); + + dbgln_if(IDB_DEBUG, "commit_a_transaction: transaction {} is committing", transaction->uuid()); + + // 2. Run the following steps in parallel: + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, transaction]() { + HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); + + // 1. Wait until every item in transaction’s request list is processed. + HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [transaction]() { + if constexpr (IDB_DEBUG) { + dbgln("commit_a_transaction: waiting for step 1"); + dbgln("requests in queue:"); + for (auto const& request : transaction->request_list()) { + dbgln(" - {} = {}", request->uuid(), request->processed() ? "processed"sv : "not processed"sv); + } + } + + return transaction->request_list().all_requests_processed(); + })); + + // 2. If transaction’s state is no longer committing, then terminate these steps. + if (transaction->state() != IDBTransaction::TransactionState::Committing) + return; + + // FIXME: 3. Attempt to write any outstanding changes made by transaction to the database, considering transaction’s durability hint. + // FIXME: 4. If an error occurs while writing the changes to the database, then run abort a transaction with transaction and an appropriate type for the error, for example "QuotaExceededError" or "UnknownError" DOMException, and terminate these steps. + + // 5. Queue a task to run these steps: + HTML::queue_a_task(HTML::Task::Source::DatabaseAccess, nullptr, nullptr, GC::create_function(transaction->realm().vm().heap(), [transaction]() { + // 1. If transaction is an upgrade transaction, then set transaction’s connection's associated database's upgrade transaction to null. + if (transaction->is_upgrade_transaction()) + transaction->connection()->associated_database()->set_upgrade_transaction(nullptr); + + // 2. Set transaction’s state to finished. + transaction->set_state(IDBTransaction::TransactionState::Finished); + + // 3. Fire an event named complete at transaction. + transaction->dispatch_event(DOM::Event::create(transaction->realm(), HTML::EventNames::complete)); + + // 4. If transaction is an upgrade transaction, then let request be the request associated with transaction and set request’s transaction to null. + if (transaction->is_upgrade_transaction()) { + auto request = transaction->associated_request(); + request->set_transaction(nullptr); + + // Ad-hoc: Clear the two-way binding. + transaction->set_associated_request(nullptr); + } + })); + })); +} + } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h index 74e5b3f022f..7d7540e5fe7 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.h @@ -20,12 +20,13 @@ using KeyPath = Variant>; WebIDL::ExceptionOr> open_a_database_connection(JS::Realm&, StorageAPI::StorageKey, String, Optional, GC::Ref); bool fire_a_version_change_event(JS::Realm&, FlyString const&, GC::Ref, u64, Optional); ErrorOr> convert_a_value_to_a_key(JS::Realm&, JS::Value, Vector = {}); -void close_a_database_connection(IDBDatabase&, bool forced = false); +void close_a_database_connection(GC::Ref, bool forced = false); GC::Ref upgrade_a_database(JS::Realm&, GC::Ref, u64, GC::Ref); WebIDL::ExceptionOr delete_a_database(JS::Realm&, StorageAPI::StorageKey, String, GC::Ref); -void abort_a_transaction(IDBTransaction&, GC::Ptr); +void abort_a_transaction(GC::Ref, GC::Ptr); JS::Value convert_a_key_to_a_value(JS::Realm&, GC::Ref); bool is_valid_key_path(KeyPath const&); GC::Ref create_a_sorted_name_list(JS::Realm&, Vector); +void commit_a_transaction(JS::Realm&, GC::Ref); } diff --git a/Libraries/LibWeb/IndexedDB/Internal/Database.cpp b/Libraries/LibWeb/IndexedDB/Internal/Database.cpp index e02d0e38c52..cf4f5fd945c 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Database.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Database.cpp @@ -4,8 +4,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include +#include namespace Web::IndexedDB { diff --git a/Libraries/LibWeb/IndexedDB/Internal/Index.cpp b/Libraries/LibWeb/IndexedDB/Internal/Index.cpp new file mode 100644 index 00000000000..d347f1dd49f --- /dev/null +++ b/Libraries/LibWeb/IndexedDB/Internal/Index.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025, stelar7 + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::IndexedDB { + +GC_DEFINE_ALLOCATOR(Index); + +Index::~Index() = default; + +GC::Ref Index::create(JS::Realm& realm, GC::Ref store, String name, KeyPath const& key_path, bool unique, bool multi_entry) +{ + return realm.create(store, name, key_path, unique, multi_entry); +} + +Index::Index(GC::Ref store, String name, KeyPath const& key_path, bool unique, bool multi_entry) + : m_object_store(store) + , m_name(move(name)) + , m_unique(unique) + , m_multi_entry(multi_entry) + , m_key_path(key_path) +{ + store->index_set().set(name, *this); +} + +void Index::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_object_store); + + for (auto& record : m_records) { + visitor.visit(record.key); + visitor.visit(record.value); + } +} + +void Index::set_name(String name) +{ + // NOTE: Update the key in the map so it still matches the name + auto old_value = m_object_store->index_set().take(m_name).release_value(); + m_object_store->index_set().set(name, old_value); + + m_name = move(name); +} + +} diff --git a/Libraries/LibWeb/IndexedDB/Internal/Index.h b/Libraries/LibWeb/IndexedDB/Internal/Index.h new file mode 100644 index 00000000000..6296c8b805c --- /dev/null +++ b/Libraries/LibWeb/IndexedDB/Internal/Index.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, stelar7 + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Web::IndexedDB { + +using KeyPath = Variant>; + +// https://w3c.github.io/IndexedDB/#index-list-of-records +struct IndexRecord { + GC::Ref key; + GC::Ref value; +}; + +// https://w3c.github.io/IndexedDB/#index-construct +class Index : public JS::Cell { + GC_CELL(Index, JS::Cell); + GC_DECLARE_ALLOCATOR(Index); + +public: + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, String, KeyPath const&, bool, bool); + virtual ~Index(); + + void set_name(String name); + [[nodiscard]] String name() const { return m_name; } + [[nodiscard]] bool unique() const { return m_unique; } + [[nodiscard]] bool multi_entry() const { return m_multi_entry; } + [[nodiscard]] GC::Ref object_store() const { return m_object_store; } + [[nodiscard]] AK::ReadonlySpan records() const { return m_records; } + [[nodiscard]] KeyPath const& key_path() const { return m_key_path; } + +protected: + virtual void visit_edges(Visitor&) override; + +private: + Index(GC::Ref, String, KeyPath const&, bool, bool); + + // An index [...] has a referenced object store. + GC::Ref m_object_store; + + // The index has a list of records which hold the data stored in the index. + Vector m_records; + + // An index has a name, which is a name. At any one time, the name is unique within index’s referenced object store. + String m_name; + + // An index has a unique flag. When true, the index enforces that no two records in the index has the same key. + bool m_unique { false }; + + // An index has a multiEntry flag. This flag affects how the index behaves when the result of evaluating the index’s key path yields an array key. + bool m_multi_entry { false }; + + // The keys are derived from the referenced object store’s values using a key path. + KeyPath m_key_path; +}; + +} diff --git a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp index 2169030f821..3fe00e26cd2 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.cpp @@ -32,6 +32,7 @@ void ObjectStore::visit_edges(Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_database); + visitor.visit(m_indexes); } } diff --git a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h index 99b7de285ba..810c33eb8fb 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h +++ b/Libraries/LibWeb/IndexedDB/Internal/ObjectStore.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include namespace Web::IndexedDB { @@ -36,6 +38,7 @@ public: bool uses_inline_keys() const { return m_key_path.has_value(); } bool uses_out_of_line_keys() const { return !m_key_path.has_value(); } Optional key_generator() const { return m_key_generator; } + AK::HashMap>& index_set() { return m_indexes; } GC::Ref database() const { return m_database; } @@ -48,6 +51,9 @@ private: // AD-HOC: An ObjectStore needs to know what Database it belongs to... GC::Ref m_database; + // AD-HOC: An Index has referenced ObjectStores, we also need the reverse mapping + AK::HashMap> m_indexes; + // An object store has a name, which is a name. At any one time, the name is unique within the database to which it belongs. String m_name; diff --git a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp index aa93dbbbfcc..c302731501f 100644 --- a/Libraries/LibWeb/Layout/BlockFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/BlockFormattingContext.cpp @@ -95,7 +95,7 @@ void BlockFormattingContext::run(AvailableSpace const& available_space) // FIXME: this should take writing modes into consideration. auto legend_height = legend_state.border_box_height(); auto new_y = -((legend_height) / 2) - fieldset_state.padding_top; - legend_state.set_content_offset({ legend_state.offset.x(), new_y }); + legend_state.set_content_y(new_y); // If the computed value of 'inline-size' is 'auto', // then the used value is the fit-content inline size. @@ -955,7 +955,7 @@ void BlockFormattingContext::place_block_level_element_in_normal_flow_vertically { auto& box_state = m_state.get_mutable(child_box); y += box_state.border_box_top(); - box_state.set_content_offset(CSSPixelPoint { box_state.offset.x(), y }); + box_state.set_content_y(y); for (auto const& float_box : m_left_floats.all_boxes) float_box->margin_box_rect_in_root_coordinate_space = margin_box_rect_in_ancestor_coordinate_space(float_box->used_values, root()); @@ -1004,7 +1004,7 @@ void BlockFormattingContext::place_block_level_element_in_normal_flow_horizontal x += box_state.margin_box_left(); } - box_state.set_content_offset({ x, box_state.offset.y() }); + box_state.set_content_x(x); } void BlockFormattingContext::layout_viewport(AvailableSpace const& available_space) @@ -1089,8 +1089,7 @@ void BlockFormattingContext::layout_floating_box(Box const& box, BlockContainer // Walk all currently tracked floats on the side we're floating towards. // We're looking for the innermost preceding float that intersects vertically with `box`. for (auto& preceding_float : side_data.current_boxes.in_reverse()) { - auto const preceding_float_rect = margin_box_rect_in_ancestor_coordinate_space(preceding_float.used_values, root()); - if (!preceding_float_rect.contains_vertically(y_in_root)) + if (!preceding_float.margin_box_rect_in_root_coordinate_space.contains_vertically(y_in_root)) continue; // We found a preceding float that intersects vertically with the current float. // Now we need to find out if there's enough inline-axis space to stack them next to each other. diff --git a/Libraries/LibWeb/Layout/FormattingContext.cpp b/Libraries/LibWeb/Layout/FormattingContext.cpp index 9424150d6a6..b7d3882890e 100644 --- a/Libraries/LibWeb/Layout/FormattingContext.cpp +++ b/Libraries/LibWeb/Layout/FormattingContext.cpp @@ -1828,12 +1828,12 @@ CSSPixels FormattingContext::box_baseline(Box const& box) const { return { { - -used_values.margin_box_left(), - -used_values.margin_box_top(), + -max(used_values.margin_box_left(), 0), + -max(used_values.margin_box_top(), 0), }, { - used_values.margin_box_left() + used_values.content_width() + used_values.margin_box_right(), - used_values.margin_box_top() + used_values.content_height() + used_values.margin_box_bottom(), + max(used_values.margin_box_left(), 0) + used_values.content_width() + max(used_values.margin_box_right(), 0), + max(used_values.margin_box_top(), 0) + used_values.content_height() + max(used_values.margin_box_bottom(), 0), }, }; } diff --git a/Libraries/LibWeb/Layout/GridFormattingContext.cpp b/Libraries/LibWeb/Layout/GridFormattingContext.cpp index a1b5aabcbad..b3e8caf61e6 100644 --- a/Libraries/LibWeb/Layout/GridFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/GridFormattingContext.cpp @@ -1160,7 +1160,7 @@ void GridFormattingContext::expand_flexible_tracks(GridDimension dimension) auto& tracks_and_gaps = dimension == GridDimension::Column ? m_grid_columns_and_gaps : m_grid_rows_and_gaps; auto& tracks = dimension == GridDimension::Column ? m_grid_columns : m_grid_rows; auto& available_size = dimension == GridDimension::Column ? m_available_space->width : m_available_space->height; - // FIXME: This should idealy take a Span, as that is more idomatic, but Span does not yet support holding references + // FIXME: This should ideally take a Span, as that is more idomatic, but Span does not yet support holding references auto find_the_size_of_an_fr = [&](Vector const& tracks, CSSPixels space_to_fill) -> CSSPixelFraction { // https://www.w3.org/TR/css-grid-2/#algo-find-fr-size auto treat_track_as_inflexiable = MUST(AK::Bitmap::create(tracks.size(), false)); @@ -2398,23 +2398,23 @@ CSSPixels GridFormattingContext::calculate_min_content_contribution(GridItem con return should_treat_height_as_auto(item.box, available_space_for_item); }(); - auto maxium_size = CSSPixels::max(); + auto maximum_size = CSSPixels::max(); if (auto const& css_maximum_size = item.maximum_size(dimension); css_maximum_size.is_length()) { - maxium_size = css_maximum_size.length().to_px(item.box); + maximum_size = css_maximum_size.length().to_px(item.box); } if (should_treat_preferred_size_as_auto) { auto result = item.add_margin_box_sizes(calculate_min_content_size(item, dimension), dimension); - return min(result, maxium_size); + return min(result, maximum_size); } auto preferred_size = item.preferred_size(dimension); if (dimension == GridDimension::Column) { auto width = calculate_inner_width(item.box, m_available_space->width, preferred_size); - return min(item.add_margin_box_sizes(width, dimension), maxium_size); + return min(item.add_margin_box_sizes(width, dimension), maximum_size); } auto height = calculate_inner_height(item.box, *m_available_space, preferred_size); - return min(item.add_margin_box_sizes(height, dimension), maxium_size); + return min(item.add_margin_box_sizes(height, dimension), maximum_size); } CSSPixels GridFormattingContext::calculate_max_content_contribution(GridItem const& item, GridDimension dimension) const @@ -2427,21 +2427,21 @@ CSSPixels GridFormattingContext::calculate_max_content_contribution(GridItem con return should_treat_height_as_auto(item.box, available_space_for_item); }(); - auto maxium_size = CSSPixels::max(); + auto maximum_size = CSSPixels::max(); if (auto const& css_maximum_size = item.maximum_size(dimension); css_maximum_size.is_length()) { - maxium_size = css_maximum_size.length().to_px(item.box); + maximum_size = css_maximum_size.length().to_px(item.box); } auto preferred_size = item.preferred_size(dimension); if (should_treat_preferred_size_as_auto || preferred_size.is_fit_content()) { auto fit_content_size = dimension == GridDimension::Column ? calculate_fit_content_width(item.box, available_space_for_item) : calculate_fit_content_height(item.box, available_space_for_item); auto result = item.add_margin_box_sizes(fit_content_size, dimension); - return min(result, maxium_size); + return min(result, maximum_size); } auto containing_block_size = containing_block_size_for_item(item, dimension); auto result = item.add_margin_box_sizes(preferred_size.to_px(grid_container(), containing_block_size), dimension); - return min(result, maxium_size); + return min(result, maximum_size); } CSSPixels GridFormattingContext::calculate_limited_min_content_contribution(GridItem const& item, GridDimension dimension) const diff --git a/Libraries/LibWeb/Layout/TableFormattingContext.cpp b/Libraries/LibWeb/Layout/TableFormattingContext.cpp index c9e80f33522..c4277db4b66 100644 --- a/Libraries/LibWeb/Layout/TableFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/TableFormattingContext.cpp @@ -295,15 +295,15 @@ void TableFormattingContext::compute_intrinsic_percentage(size_t max_cell_span) // that the cell spans. If this gives a negative result, change it to 0%. // 3. Multiply by the ratio of the column’s non-spanning max-content width to the sum of the non-spanning max-content widths of all // columns spanned by the cell that have an intrinsic percentage width of the column based on cells of span up to N-1 equal to 0%. - CSSPixels ajusted_cell_contribution; + CSSPixels adjusted_cell_contribution; if (width_sum_of_columns_with_zero_intrinsic_percentage != 0) { - ajusted_cell_contribution = cell_contribution.scaled(rows_or_columns[rc_index].max_size / static_cast(width_sum_of_columns_with_zero_intrinsic_percentage)); + adjusted_cell_contribution = cell_contribution.scaled(rows_or_columns[rc_index].max_size / static_cast(width_sum_of_columns_with_zero_intrinsic_percentage)); } else { // However, if this ratio is undefined because the denominator is zero, instead use the 1 divided by the number of columns // spanned by the cell that have an intrinsic percentage width of the column based on cells of span up to N-1 equal to zero. - ajusted_cell_contribution = cell_contribution * 1 / number_of_columns_with_zero_intrinsic_percentage; + adjusted_cell_contribution = cell_contribution * 1 / number_of_columns_with_zero_intrinsic_percentage; } - intrinsic_percentage_contribution_by_index[rc_index] = max(static_cast(ajusted_cell_contribution), intrinsic_percentage_contribution_by_index[rc_index]); + intrinsic_percentage_contribution_by_index[rc_index] = max(static_cast(adjusted_cell_contribution), intrinsic_percentage_contribution_by_index[rc_index]); } } for (size_t rc_index = 0; rc_index < rows_or_columns.size(); ++rc_index) { diff --git a/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp b/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp index 5f2fce950c3..d256b698037 100644 --- a/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp +++ b/Libraries/LibWeb/Loader/GeneratedPagesLoader.cpp @@ -45,7 +45,7 @@ ErrorOr load_error_page(URL::URL const& url, StringView error_message) ErrorOr load_file_directory_page(URL::URL const& url) { // Generate HTML contents entries table - auto lexical_path = LexicalPath(URL::percent_decode(url.serialize_path())); + auto lexical_path = LexicalPath(url.file_path()); Core::DirIterator dt(lexical_path.string(), Core::DirIterator::Flags::SkipParentAndBaseDir); Vector names; while (dt.has_next()) diff --git a/Libraries/LibWeb/Loader/Resource.cpp b/Libraries/LibWeb/Loader/Resource.cpp index 075cd7e378f..3b2bf088e6f 100644 --- a/Libraries/LibWeb/Loader/Resource.cpp +++ b/Libraries/LibWeb/Loader/Resource.cpp @@ -102,7 +102,7 @@ void Resource::did_load(Badge, ReadonlyBytes data, HTTP::HeaderM if (content_type_options.value_or("").equals_ignoring_ascii_case("nosniff"sv)) { m_mime_type = "text/plain"; } else { - m_mime_type = Core::guess_mime_type_based_on_filename(URL::percent_decode(url().serialize_path())); + m_mime_type = Core::guess_mime_type_based_on_filename(url().file_path()); } } diff --git a/Libraries/LibWeb/Loader/ResourceLoader.cpp b/Libraries/LibWeb/Loader/ResourceLoader.cpp index 08fd211fb67..6b417ea5262 100644 --- a/Libraries/LibWeb/Loader/ResourceLoader.cpp +++ b/Libraries/LibWeb/Loader/ResourceLoader.cpp @@ -321,7 +321,7 @@ void ResourceLoader::load(LoadRequest& request, GC::Root succes } auto data = resource.value()->data(); - auto response_headers = response_headers_for_file(URL::percent_decode(url.serialize_path()), resource.value()->modified_time()); + auto response_headers = response_headers_for_file(url.file_path(), resource.value()->modified_time()); // FIXME: Implement timing info for resource requests. Requests::RequestTimingInfo fixme_implement_timing_info {}; @@ -339,7 +339,7 @@ void ResourceLoader::load(LoadRequest& request, GC::Root succes return; } - FileRequest file_request(URL::percent_decode(url.serialize_path()), [this, success_callback, error_callback, request, respond_directory_page](ErrorOr file_or_error) { + FileRequest file_request(url.file_path(), [this, success_callback, error_callback, request, respond_directory_page](ErrorOr file_or_error) { --m_pending_loads; if (on_load_counter_change) on_load_counter_change(); @@ -387,7 +387,7 @@ void ResourceLoader::load(LoadRequest& request, GC::Root succes } auto data = maybe_data.release_value(); - auto response_headers = response_headers_for_file(URL::percent_decode(request.url().serialize_path()), st_or_error.value().st_mtime); + auto response_headers = response_headers_for_file(request.url().file_path(), st_or_error.value().st_mtime); // FIXME: Implement timing info for file requests. Requests::RequestTimingInfo fixme_implement_timing_info {}; diff --git a/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp b/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp index b958b635fe3..e229d4e3bee 100644 --- a/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp +++ b/Libraries/LibWeb/MediaCapabilitiesAPI/MediaCapabilities.cpp @@ -238,7 +238,7 @@ GC::Ref MediaCapabilitiesDecodingInfo::to_object(JS::Realm& realm) MUST(object->create_data_property("supported"_fly_string, JS::BooleanObject::create(realm, supported))); MUST(object->create_data_property("smooth"_fly_string, JS::BooleanObject::create(realm, smooth))); - MUST(object->create_data_property("powerEfficent"_fly_string, JS::BooleanObject::create(realm, power_efficient))); + MUST(object->create_data_property("powerEfficient"_fly_string, JS::BooleanObject::create(realm, power_efficient))); return object; } diff --git a/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp b/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp index 7a966821fbe..36d7003af9f 100644 --- a/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp +++ b/Libraries/LibWeb/Page/DragAndDropEventHandler.cpp @@ -568,8 +568,8 @@ GC::Ref DragAndDropEventHandler::fire_a_drag_and_drop_event( if (!name.is_one_of(HTML::EventNames::dragleave, HTML::EventNames::dragend)) event_init.cancelable = true; - // 11. Initialize event's mouse and key attributes initialized according to the state of the input devices as they - // would be for user interaction events. + // 11. Initialize event's mouse and key attributes according to the state of the input devices as they would be for + // user interaction events. event_init.ctrl_key = (modifiers & UIEvents::Mod_Ctrl) != 0; event_init.shift_key = (modifiers & UIEvents::Mod_Shift) != 0; event_init.alt_key = (modifiers & UIEvents::Mod_Alt) != 0; diff --git a/Libraries/LibWeb/Painting/BorderPainting.cpp b/Libraries/LibWeb/Painting/BorderPainting.cpp index ec7fe534f07..bfb4848891f 100644 --- a/Libraries/LibWeb/Painting/BorderPainting.cpp +++ b/Libraries/LibWeb/Painting/BorderPainting.cpp @@ -213,7 +213,7 @@ void paint_border(DisplayListRecorder& painter, BorderEdge edge, DevicePixelRect VERIFY_NOT_REACHED(); } } - // FIXME: this middle point rule seems not exacly the same as main browsers + // FIXME: this middle point rule seems not exactly the same as main browsers // compute the midpoint based on point whose tangent slope of 1 // https://math.stackexchange.com/questions/3325134/find-the-points-on-the-ellipse-where-the-slope-of-the-tangent-line-is-1 return Gfx::FloatPoint( diff --git a/Libraries/LibWeb/Painting/Command.h b/Libraries/LibWeb/Painting/Command.h index ac0fd887d6d..f8b3dbb4d37 100644 --- a/Libraries/LibWeb/Painting/Command.h +++ b/Libraries/LibWeb/Painting/Command.h @@ -8,13 +8,11 @@ #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -34,6 +32,7 @@ #include #include #include +#include namespace Web::Painting { @@ -392,6 +391,7 @@ struct AddMask { struct PaintNestedDisplayList { RefPtr display_list; + ScrollStateSnapshot scroll_state_snapshot; Gfx::IntRect rect; [[nodiscard]] Gfx::IntRect bounding_rect() const { return rect; } diff --git a/Libraries/LibWeb/Painting/DisplayList.cpp b/Libraries/LibWeb/Painting/DisplayList.cpp index 0494a10fe7d..36ef0073c2f 100644 --- a/Libraries/LibWeb/Painting/DisplayList.cpp +++ b/Libraries/LibWeb/Painting/DisplayList.cpp @@ -35,18 +35,18 @@ static bool command_is_clip_or_mask(Command const& command) }); } -void DisplayListPlayer::execute(DisplayList& display_list, RefPtr surface) +void DisplayListPlayer::execute(DisplayList& display_list, ScrollStateSnapshot const& scroll_state, RefPtr surface) { if (surface) { surface->lock_context(); } - execute_impl(display_list, surface); + execute_impl(display_list, scroll_state, surface); if (surface) { surface->unlock_context(); } } -void DisplayListPlayer::execute_impl(DisplayList& display_list, RefPtr surface) +void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnapshot const& scroll_state, RefPtr surface) { if (surface) m_surfaces.append(*surface); @@ -56,7 +56,6 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, RefPtr); + void execute(DisplayList&, ScrollStateSnapshot const&, RefPtr); protected: Gfx::PaintingSurface& surface() const { return m_surfaces.last(); } - void execute_impl(DisplayList&, RefPtr); + void execute_impl(DisplayList&, ScrollStateSnapshot const& scroll_state, RefPtr); private: virtual void flush() = 0; @@ -93,9 +93,6 @@ public: AK::SegmentedVector const& commands() const { return m_commands; } - void set_scroll_state(ScrollState scroll_state) { m_scroll_state = move(scroll_state); } - ScrollState const& scroll_state() const { return m_scroll_state; } - void set_device_pixels_per_css_pixel(double device_pixels_per_css_pixel) { m_device_pixels_per_css_pixel = device_pixels_per_css_pixel; } double device_pixels_per_css_pixel() const { return m_device_pixels_per_css_pixel; } @@ -103,7 +100,6 @@ private: DisplayList() = default; AK::SegmentedVector m_commands; - ScrollState m_scroll_state; double m_device_pixels_per_css_pixel; }; diff --git a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp index 89e96654024..c440c34a9d4 100644 --- a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp +++ b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp @@ -972,7 +972,8 @@ void DisplayListPlayerSkia::add_mask(AddMask const& command) auto mask_surface = Gfx::PaintingSurface::create_with_size(m_context, rect.size(), Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied); - execute_impl(*command.display_list, mask_surface); + ScrollStateSnapshot scroll_state_snapshot; + execute_impl(*command.display_list, scroll_state_snapshot, mask_surface); SkMatrix mask_matrix; mask_matrix.setTranslate(rect.x(), rect.y()); @@ -985,7 +986,7 @@ void DisplayListPlayerSkia::paint_nested_display_list(PaintNestedDisplayList con { auto& canvas = surface().canvas(); canvas.translate(command.rect.x(), command.rect.y()); - execute_impl(*command.display_list, {}); + execute_impl(*command.display_list, command.scroll_state_snapshot, {}); } void DisplayListPlayerSkia::paint_scrollbar(PaintScrollBar const& command) diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index 8e4c219c919..e00e7ac04f4 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp @@ -24,9 +24,9 @@ void DisplayListRecorder::append(Command&& command) m_command_list.append(move(command), scroll_frame_id); } -void DisplayListRecorder::paint_nested_display_list(RefPtr display_list, Gfx::IntRect rect) +void DisplayListRecorder::paint_nested_display_list(RefPtr display_list, ScrollStateSnapshot&& scroll_state_snapshot, Gfx::IntRect rect) { - append(PaintNestedDisplayList { move(display_list), rect }); + append(PaintNestedDisplayList { move(display_list), move(scroll_state_snapshot), rect }); } void DisplayListRecorder::add_rounded_rect_clip(CornerRadii corner_radii, Gfx::IntRect border_rect, CornerClip corner_clip) diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.h b/Libraries/LibWeb/Painting/DisplayListRecorder.h index 0da68e1ea24..86a0c022379 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.h +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.h @@ -129,7 +129,7 @@ public: void push_stacking_context(PushStackingContextParams params); void pop_stacking_context(); - void paint_nested_display_list(RefPtr display_list, Gfx::IntRect rect); + void paint_nested_display_list(RefPtr display_list, ScrollStateSnapshot&&, Gfx::IntRect rect); void add_rounded_rect_clip(CornerRadii corner_radii, Gfx::IntRect border_rect, CornerClip corner_clip); void add_mask(RefPtr display_list, Gfx::IntRect rect); diff --git a/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp b/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp index 097334e6147..bd0c675f66c 100644 --- a/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp +++ b/Libraries/LibWeb/Painting/NavigableContainerViewportPaintable.cpp @@ -60,7 +60,8 @@ void NavigableContainerViewportPaintable::paint(PaintContext& context, PaintPhas paint_config.should_show_line_box_borders = context.should_show_line_box_borders(); paint_config.has_focus = context.has_focus(); auto display_list = const_cast(hosted_document)->record_display_list(paint_config); - context.display_list_recorder().paint_nested_display_list(display_list, context.enclosing_device_rect(absolute_rect).to_type()); + auto scroll_state_snapshot = hosted_document->paintable()->scroll_state().snapshot(); + context.display_list_recorder().paint_nested_display_list(display_list, move(scroll_state_snapshot), context.enclosing_device_rect(absolute_rect).to_type()); context.display_list_recorder().restore(); diff --git a/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp b/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp index 11103e7d7c9..9a6cd727e10 100644 --- a/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp +++ b/Libraries/LibWeb/Painting/RadioButtonPaintable.cpp @@ -39,7 +39,7 @@ void RadioButtonPaintable::paint(PaintContext& context, PaintPhase phase) const return; auto draw_circle = [&](auto const& rect, Color color) { - // Note: Doing this is a bit more forgiving than draw_circle() which will round to the nearset even radius. + // Note: Doing this is a bit more forgiving than draw_circle() which will round to the nearest even radius. // This will fudge it (which works better here). context.display_list_recorder().fill_rect_with_rounded_corners(rect, color, rect.width() / 2); }; diff --git a/Libraries/LibWeb/Painting/SVGMaskable.cpp b/Libraries/LibWeb/Painting/SVGMaskable.cpp index 7185c380587..4d9e066bed1 100644 --- a/Libraries/LibWeb/Painting/SVGMaskable.cpp +++ b/Libraries/LibWeb/Painting/SVGMaskable.cpp @@ -100,7 +100,8 @@ RefPtr SVGMaskable::calculate_mask_of_svg(PaintContext& co StackingContext::paint_svg(paint_context, paintable, PaintPhase::Foreground); auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(*mask_bitmap); DisplayListPlayerSkia display_list_player; - display_list_player.execute(display_list, painting_surface); + ScrollStateSnapshot scroll_state_snapshot; + display_list_player.execute(display_list, scroll_state_snapshot, painting_surface); return mask_bitmap; }; RefPtr mask_bitmap = {}; diff --git a/Libraries/LibWeb/Painting/ScrollFrame.h b/Libraries/LibWeb/Painting/ScrollFrame.h index f5c0c9ac408..c017173d086 100644 --- a/Libraries/LibWeb/Painting/ScrollFrame.h +++ b/Libraries/LibWeb/Painting/ScrollFrame.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include diff --git a/Libraries/LibWeb/Painting/ScrollState.cpp b/Libraries/LibWeb/Painting/ScrollState.cpp new file mode 100644 index 00000000000..c817b325dff --- /dev/null +++ b/Libraries/LibWeb/Painting/ScrollState.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025, Aliaksandr Kalenik + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace Web::Painting { + +ScrollStateSnapshot ScrollStateSnapshot::create(Vector> const& scroll_frames) +{ + ScrollStateSnapshot snapshot; + snapshot.entries.ensure_capacity(scroll_frames.size()); + for (auto const& scroll_frame : scroll_frames) + snapshot.entries.append({ scroll_frame->cumulative_offset(), scroll_frame->own_offset() }); + return snapshot; +} + +} diff --git a/Libraries/LibWeb/Painting/ScrollState.h b/Libraries/LibWeb/Painting/ScrollState.h index ddadbc550a0..dde14bf1ea4 100644 --- a/Libraries/LibWeb/Painting/ScrollState.h +++ b/Libraries/LibWeb/Painting/ScrollState.h @@ -1,15 +1,42 @@ /* - * Copyright (c) 2024, Aliaksandr Kalenik + * Copyright (c) 2024-2025, Aliaksandr Kalenik * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include #include namespace Web::Painting { +class ScrollStateSnapshot { +public: + static ScrollStateSnapshot create(Vector> const& scroll_frames); + + CSSPixelPoint cumulative_offset_for_frame_with_id(size_t id) const + { + if (id >= entries.size()) + return {}; + return entries[id].cumulative_offset; + } + + CSSPixelPoint own_offset_for_frame_with_id(size_t id) const + { + if (id >= entries.size()) + return {}; + return entries[id].own_offset; + } + +private: + struct Entry { + CSSPixelPoint cumulative_offset; + CSSPixelPoint own_offset; + }; + Vector entries; +}; + class ScrollState { public: NonnullRefPtr create_scroll_frame_for(PaintableBox const& paintable_box, RefPtr parent) @@ -56,6 +83,11 @@ public: } } + ScrollStateSnapshot snapshot() const + { + return ScrollStateSnapshot::create(m_scroll_frames); + } + private: Vector> m_scroll_frames; }; diff --git a/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp b/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp index a7d6fba1cb3..60158f38bac 100644 --- a/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp +++ b/Libraries/LibWeb/SVG/SVGDecodedImageData.cpp @@ -106,7 +106,8 @@ RefPtr SVGDecodedImageData::render(Gfx::IntSize size) const case DisplayListPlayerType::SkiaCPU: { auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(*bitmap); Painting::DisplayListPlayerSkia display_list_player; - display_list_player.execute(*display_list, painting_surface); + Painting::ScrollStateSnapshot scroll_state_snapshot; + display_list_player.execute(*display_list, scroll_state_snapshot, painting_surface); break; } default: diff --git a/Libraries/LibWeb/SVG/SVGElement.cpp b/Libraries/LibWeb/SVG/SVGElement.cpp index 45756e3b639..f7a8d26ad25 100644 --- a/Libraries/LibWeb/SVG/SVGElement.cpp +++ b/Libraries/LibWeb/SVG/SVGElement.cpp @@ -115,7 +115,7 @@ void SVGElement::update_use_elements_that_reference_this() // An unconnected node cannot have valid references. // This also prevents searches for elements that are in the process of being constructed - as clones. || !this->is_connected() - // Each use element already listens for the completely_loaded event and then clones its referece, + // Each use element already listens for the completely_loaded event and then clones its reference, // we do not have to also clone it in the process of initial DOM building. || !document().is_completely_loaded()) { diff --git a/Libraries/LibWeb/SVG/SVGUseElement.cpp b/Libraries/LibWeb/SVG/SVGUseElement.cpp index bcac845abdd..6e55ede6333 100644 --- a/Libraries/LibWeb/SVG/SVGUseElement.cpp +++ b/Libraries/LibWeb/SVG/SVGUseElement.cpp @@ -82,14 +82,14 @@ void SVGUseElement::process_the_url(Optional const& href) if (!m_href.has_value()) return; - if (is_referrenced_element_same_document()) { + if (is_referenced_element_same_document()) { clone_element_tree_as_our_shadow_tree(referenced_element()); } else { fetch_the_document(*m_href); } } -bool SVGUseElement::is_referrenced_element_same_document() const +bool SVGUseElement::is_referenced_element_same_document() const { return m_href->equals(document().url(), URL::ExcludeFragment::Yes); } @@ -121,7 +121,7 @@ void SVGUseElement::svg_element_changed(SVGElement& svg_element) void SVGUseElement::svg_element_removed(SVGElement& svg_element) { - if (!m_href.has_value() || !m_href->fragment().has_value() || !is_referrenced_element_same_document()) { + if (!m_href.has_value() || !m_href->fragment().has_value() || !is_referenced_element_same_document()) { return; } @@ -139,7 +139,7 @@ GC::Ptr SVGUseElement::referenced_element() if (!m_href->fragment().has_value()) return nullptr; - if (is_referrenced_element_same_document()) + if (is_referenced_element_same_document()) return document().get_element_by_id(*m_href->fragment()); if (!m_resource_request) diff --git a/Libraries/LibWeb/SVG/SVGUseElement.h b/Libraries/LibWeb/SVG/SVGUseElement.h index 7a1dea33859..d69a25e3b6e 100644 --- a/Libraries/LibWeb/SVG/SVGUseElement.h +++ b/Libraries/LibWeb/SVG/SVGUseElement.h @@ -57,7 +57,7 @@ private: GC::Ptr referenced_element(); void fetch_the_document(URL::URL const& url); - bool is_referrenced_element_same_document() const; + bool is_referenced_element_same_document() const; void clone_element_tree_as_our_shadow_tree(Element* to_clone); bool is_valid_reference_element(Element const& reference_element) const; diff --git a/Libraries/LibWeb/StorageAPI/StorageEndpoint.h b/Libraries/LibWeb/StorageAPI/StorageEndpoint.h index 450f0c32e2e..3f54c1bd771 100644 --- a/Libraries/LibWeb/StorageAPI/StorageEndpoint.h +++ b/Libraries/LibWeb/StorageAPI/StorageEndpoint.h @@ -25,7 +25,7 @@ struct StorageEndpoint { // https://storage.spec.whatwg.org/#storage-endpoint-types // A storage endpoint also has types, which is a set of storage types. - // NOTE: We do not implement this as a set as it is not neccessary in the current implementation. + // NOTE: We do not implement this as a set as it is not necessary in the current implementation. StorageType type; // https://storage.spec.whatwg.org/#storage-endpoint-quota diff --git a/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Libraries/LibWeb/Streams/AbstractOperations.cpp index 1960bd6a409..dc3dba99711 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -289,8 +289,383 @@ bool readable_stream_has_default_reader(ReadableStream const& stream) return false; } +// https://streams.spec.whatwg.org/#ref-for-in-parallel +class ReadableStreamPipeTo final : public JS::Cell { + GC_CELL(ReadableStreamPipeTo, JS::Cell); + GC_DECLARE_ALLOCATOR(ReadableStreamPipeTo); + +public: + void process() + { + if (check_for_error_and_close_states()) + return; + + auto ready_promise = m_writer->ready(); + + if (ready_promise && WebIDL::is_promise_fulfilled(*ready_promise)) { + read_chunk(); + return; + } + + auto when_ready = GC::create_function(m_realm->heap(), [this](JS::Value) -> WebIDL::ExceptionOr { + read_chunk(); + return JS::js_undefined(); + }); + + auto shutdown = GC::create_function(heap(), [this](JS::Value) -> WebIDL::ExceptionOr { + check_for_error_and_close_states(); + return JS::js_undefined(); + }); + + if (ready_promise) + WebIDL::react_to_promise(*ready_promise, when_ready, shutdown); + if (auto promise = m_reader->closed()) + WebIDL::react_to_promise(*promise, shutdown, shutdown); + } + + void set_abort_signal(GC::Ref signal, DOM::AbortSignal::AbortSignal::AbortAlgorithmID signal_id) + { + m_signal = signal; + m_signal_id = signal_id; + } + + // https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action + void shutdown_with_action(GC::Ref()>> action, Optional original_error = {}) + { + // 1. If shuttingDown is true, abort these substeps. + if (m_shutting_down) + return; + + // 2. Set shuttingDown to true. + m_shutting_down = true; + + auto on_pending_writes_complete = [this, action, original_error = move(original_error)]() mutable { + HTML::TemporaryExecutionContext execution_context { m_realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 4. Let p be the result of performing action. + auto promise = action->function()(); + + WebIDL::react_to_promise(promise, + // 5. Upon fulfillment of p, finalize, passing along originalError if it was given. + GC::create_function(heap(), [this, original_error = move(original_error)](JS::Value) mutable -> WebIDL::ExceptionOr { + finish(move(original_error)); + return JS::js_undefined(); + }), + + // 6. Upon rejection of p with reason newError, finalize with newError. + GC::create_function(heap(), [this](JS::Value new_error) -> WebIDL::ExceptionOr { + finish(new_error); + return JS::js_undefined(); + })); + }; + + // 3. If dest.[[state]] is "writable" and ! WritableStreamCloseQueuedOrInFlight(dest) is false, + if (m_destination->state() == WritableStream::State::Writable && !writable_stream_close_queued_or_in_flight(m_destination)) { + // 1. If any chunks have been read but not yet written, write them to dest. + write_unwritten_chunks(); + + // 2. Wait until every chunk that has been read has been written (i.e. the corresponding promises have settled). + wait_for_pending_writes_to_complete(move(on_pending_writes_complete)); + } else { + on_pending_writes_complete(); + } + } + + // https://streams.spec.whatwg.org/#rs-pipeTo-shutdown + void shutdown(Optional error = {}) + { + // 1. If shuttingDown is true, abort these substeps. + if (m_shutting_down) + return; + + // 2. Set shuttingDown to true. + m_shutting_down = true; + + auto on_pending_writes_complete = [this, error = move(error)]() mutable { + HTML::TemporaryExecutionContext execution_context { m_realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 4. Finalize, passing along error if it was given. + finish(move(error)); + }; + + // 3. If dest.[[state]] is "writable" and ! WritableStreamCloseQueuedOrInFlight(dest) is false, + if (m_destination->state() == WritableStream::State::Writable && !writable_stream_close_queued_or_in_flight(m_destination)) { + // 1. If any chunks have been read but not yet written, write them to dest. + write_unwritten_chunks(); + + // 2. Wait until every chunk that has been read has been written (i.e. the corresponding promises have settled). + wait_for_pending_writes_to_complete(move(on_pending_writes_complete)); + } else { + on_pending_writes_complete(); + } + } + +private: + ReadableStreamPipeTo( + GC::Ref realm, + GC::Ref promise, + GC::Ref source, + GC::Ref destination, + GC::Ref reader, + GC::Ref writer, + bool prevent_close, + bool prevent_abort, + bool prevent_cancel) + : m_realm(realm) + , m_promise(promise) + , m_source(source) + , m_destination(destination) + , m_reader(reader) + , m_writer(writer) + , m_prevent_close(prevent_close) + , m_prevent_abort(prevent_abort) + , m_prevent_cancel(prevent_cancel) + { + m_reader->set_readable_stream_pipe_to_operation({}, this); + } + + virtual void visit_edges(Cell::Visitor& visitor) override + { + Base::visit_edges(visitor); + visitor.visit(m_realm); + visitor.visit(m_promise); + visitor.visit(m_source); + visitor.visit(m_destination); + visitor.visit(m_reader); + visitor.visit(m_writer); + visitor.visit(m_signal); + visitor.visit(m_pending_writes); + visitor.visit(m_unwritten_chunks); + } + + void read_chunk() + { + // Shutdown must stop activity: if shuttingDown becomes true, the user agent must not initiate further reads from + // reader, and must only perform writes of already-read chunks, as described below. In particular, the user agent + // must check the below conditions before performing any reads or writes, since they might lead to immediate shutdown. + if (check_for_error_and_close_states()) + return; + + auto when_ready = GC::create_function(heap(), [this](JS::Value value) -> WebIDL::ExceptionOr { + auto& vm = this->vm(); + + VERIFY(value.is_object()); + auto& object = value.as_object(); + + auto done = MUST(JS::iterator_complete(vm, object)); + + if (done) { + if (!check_for_error_and_close_states()) + finish(); + } else { + auto chunk = MUST(JS::iterator_value(vm, object)); + m_unwritten_chunks.append(chunk); + + write_chunk(); + process(); + } + + return JS::js_undefined(); + }); + + auto shutdown = GC::create_function(heap(), [this](JS::Value) -> WebIDL::ExceptionOr { + check_for_error_and_close_states(); + return JS::js_undefined(); + }); + + WebIDL::react_to_promise(m_reader->read(), when_ready, shutdown); + + if (auto promise = m_writer->closed()) + WebIDL::react_to_promise(*promise, shutdown, shutdown); + } + + void write_chunk() + { + // Shutdown must stop activity: if shuttingDown becomes true, the user agent must not initiate further reads from + // reader, and must only perform writes of already-read chunks, as described below. In particular, the user agent + // must check the below conditions before performing any reads or writes, since they might lead to immediate shutdown. + if (!m_shutting_down && check_for_error_and_close_states()) + return; + + auto promise = m_writer->write(m_unwritten_chunks.take_first()); + WebIDL::mark_promise_as_handled(promise); + + m_pending_writes.append(promise); + } + + void write_unwritten_chunks() + { + while (!m_unwritten_chunks.is_empty()) + write_chunk(); + } + + void wait_for_pending_writes_to_complete(Function on_complete) + { + auto handler = GC::create_function(heap(), [this, on_complete = move(on_complete)]() { + m_pending_writes.clear(); + on_complete(); + }); + + auto success_steps = [handler](Vector const&) { handler->function()(); }; + auto failure_steps = [handler](JS::Value) { handler->function()(); }; + + WebIDL::wait_for_all(m_realm, m_pending_writes, move(success_steps), move(failure_steps)); + } + + // https://streams.spec.whatwg.org/#rs-pipeTo-finalize + // We call this `finish` instead of `finalize` to avoid conflicts with GC::Cell::finalize. + void finish(Optional error = {}) + { + // 1. Perform ! WritableStreamDefaultWriterRelease(writer). + writable_stream_default_writer_release(m_writer); + + // 2. If reader implements ReadableStreamBYOBReader, perform ! ReadableStreamBYOBReaderRelease(reader). + // 3. Otherwise, perform ! ReadableStreamDefaultReaderRelease(reader). + readable_stream_default_reader_release(m_reader); + + // 4. If signal is not undefined, remove abortAlgorithm from signal. + if (m_signal) + m_signal->remove_abort_algorithm(m_signal_id); + + // 5. If error was given, reject promise with error. + if (error.has_value()) { + WebIDL::reject_promise(m_realm, m_promise, *error); + } + // 6. Otherwise, resolve promise with undefined. + else { + WebIDL::resolve_promise(m_realm, m_promise, JS::js_undefined()); + } + + m_reader->set_readable_stream_pipe_to_operation({}, nullptr); + } + + bool check_for_error_and_close_states() + { + // Error and close states must be propagated: the following conditions must be applied in order. + return m_shutting_down + || check_for_forward_errors() + || check_for_backward_errors() + || check_for_forward_close() + || check_for_backward_close(); + } + + bool check_for_forward_errors() + { + // 1. Errors must be propagated forward: if source.[[state]] is or becomes "errored", then + if (m_source->state() == ReadableStream::State::Errored) { + // 1. If preventAbort is false, shutdown with an action of ! WritableStreamAbort(dest, source.[[storedError]]) + // and with source.[[storedError]]. + if (!m_prevent_abort) { + auto action = GC::create_function(heap(), [this]() { + return writable_stream_abort(m_destination, m_source->stored_error()); + }); + + shutdown_with_action(action, m_source->stored_error()); + } + // 2. Otherwise, shutdown with source.[[storedError]]. + else { + shutdown(m_source->stored_error()); + } + } + + return m_shutting_down; + } + + bool check_for_backward_errors() + { + // 2. Errors must be propagated backward: if dest.[[state]] is or becomes "errored", then + if (m_destination->state() == WritableStream::State::Errored) { + // 1. If preventCancel is false, shutdown with an action of ! ReadableStreamCancel(source, dest.[[storedError]]) + // and with dest.[[storedError]]. + if (!m_prevent_cancel) { + auto action = GC::create_function(heap(), [this]() { + return readable_stream_cancel(m_source, m_destination->stored_error()); + }); + + shutdown_with_action(action, m_destination->stored_error()); + } + // 2. Otherwise, shutdown with dest.[[storedError]]. + else { + shutdown(m_destination->stored_error()); + } + } + + return m_shutting_down; + } + + bool check_for_forward_close() + { + // 3. Closing must be propagated forward: if source.[[state]] is or becomes "closed", then + if (m_source->state() == ReadableStream::State::Closed) { + // 1. If preventClose is false, shutdown with an action of ! WritableStreamDefaultWriterCloseWithErrorPropagation(writer). + if (!m_prevent_close) { + auto action = GC::create_function(heap(), [this]() { + return writable_stream_default_writer_close_with_error_propagation(m_writer); + }); + + shutdown_with_action(action); + } + // 2. Otherwise, shutdown. + else { + shutdown(); + } + } + + return m_shutting_down; + } + + bool check_for_backward_close() + { + // 4. Closing must be propagated backward: if ! WritableStreamCloseQueuedOrInFlight(dest) is true or dest.[[state]] is "closed", then + if (writable_stream_close_queued_or_in_flight(m_destination) || m_destination->state() == WritableStream::State::Closed) { + // 1. Assert: no chunks have been read or written. + + // 2. Let destClosed be a new TypeError. + auto destination_closed = JS::TypeError::create(m_realm, "Destination stream was closed during piping operation"sv); + + // 3. If preventCancel is false, shutdown with an action of ! ReadableStreamCancel(source, destClosed) and with destClosed. + if (!m_prevent_cancel) { + auto action = GC::create_function(heap(), [this, destination_closed]() { + return readable_stream_cancel(m_source, destination_closed); + }); + + shutdown_with_action(action, destination_closed); + } + // 4. Otherwise, shutdown with destClosed. + else { + shutdown(destination_closed); + } + } + + return m_shutting_down; + } + + GC::Ref m_realm; + GC::Ref m_promise; + + GC::Ref m_source; + GC::Ref m_destination; + + GC::Ref m_reader; + GC::Ref m_writer; + + GC::Ptr m_signal; + DOM::AbortSignal::AbortAlgorithmID m_signal_id { 0 }; + + Vector> m_pending_writes; + Vector m_unwritten_chunks; + + bool m_prevent_close { false }; + bool m_prevent_abort { false }; + bool m_prevent_cancel { false }; + + bool m_shutting_down { false }; +}; + +GC_DEFINE_ALLOCATOR(ReadableStreamPipeTo); + // https://streams.spec.whatwg.org/#readable-stream-pipe-to -GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool, bool, bool, JS::Value signal) +GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool prevent_close, bool prevent_abort, bool prevent_cancel, GC::Ptr signal) { auto& realm = source.realm(); @@ -299,10 +674,7 @@ GC::Ref readable_stream_pipe_to(ReadableStream& source, Writabl // 3. Assert: preventClose, preventAbort, and preventCancel are all booleans. // 4. If signal was not given, let signal be undefined. - // NOTE: Done by default argument - // 5. Assert: either signal is undefined, or signal implements AbortSignal. - VERIFY(signal.is_undefined() || (signal.is_object() && is(signal.as_object()))); // 6. Assert: ! IsReadableStreamLocked(source) is false. VERIFY(!is_readable_stream_locked(source)); @@ -324,57 +696,81 @@ GC::Ref readable_stream_pipe_to(ReadableStream& source, Writabl // 11. Set source.[[disturbed]] to true. source.set_disturbed(true); - // FIXME: 12. Let shuttingDown be false. + // 12. Let shuttingDown be false. + // NOTE: This is internal to the ReadableStreamPipeTo class. // 13. Let promise be a new promise. auto promise = WebIDL::create_promise(realm); - // FIXME 14. If signal is not undefined, - // 1. Let abortAlgorithm be the following steps: - // 1. Let error be signal’s abort reason. - // 2. Let actions be an empty ordered set. - // 3. If preventAbort is false, append the following action to actions: - // 1. If dest.[[state]] is "writable", return ! WritableStreamAbort(dest, error). - // 2. Otherwise, return a promise resolved with undefined. - // 4. If preventCancel is false, append the following action to actions: - // 1. If source.[[state]] is "readable", return ! ReadableStreamCancel(source, error). - // 2. Otherwise, return a promise resolved with undefined. - // 5. Shutdown with an action consisting of getting a promise to wait for all of the actions in actions, and with error. - // 2. If signal is aborted, perform abortAlgorithm and return promise. - // 3. Add abortAlgorithm to signal. + auto operation = realm.heap().allocate(realm, promise, source, dest, reader, writer, prevent_close, prevent_abort, prevent_cancel); - // 15. In parallel but not really; see #905, using reader and writer, read all chunks from source and write them to - // dest. Due to the locking provided by the reader and writer, the exact manner in which this happens is not - // observable to author code, and so there is flexibility in how this is done. The following constraints apply - // regardless of the exact algorithm used: - // - Public API must not be used: while reading or writing, or performing any of the operations below, the - // JavaScript-modifiable reader, writer, and stream APIs (i.e. methods on the appropriate prototypes) must not - // be used. Instead, the streams must be manipulated directly. + // 14. If signal is not undefined, + if (signal) { + // 1. Let abortAlgorithm be the following steps: + auto abort_algorithm = [&realm, operation, source = GC::Ref { source }, dest = GC::Ref { dest }, prevent_abort, prevent_cancel, signal]() { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; - // FIXME: Currently a naive implementation that uses ReadableStreamDefaultReader::read_all_chunks() to read all chunks - // from the source and then through the callback success_steps writes those chunks to the destination. - auto chunk_steps = GC::create_function(realm.heap(), [&realm, writer](JS::Value chunk) { - auto promise = writable_stream_default_writer_write(writer, chunk); - WebIDL::resolve_promise(realm, promise, JS::js_undefined()); - }); + // 1. Let error be signal’s abort reason. + auto error = signal->reason(); - auto success_steps = GC::create_function(realm.heap(), [promise, &realm, reader, writer]() { - // Make sure we close the acquired writer. - WebIDL::resolve_promise(realm, writable_stream_default_writer_close(*writer), JS::js_undefined()); - readable_stream_default_reader_release(*reader); + // 2. Let actions be an empty ordered set. + GC::Ptr()>> abort_destination; + GC::Ptr()>> cancel_source; - WebIDL::resolve_promise(realm, promise, JS::js_undefined()); - }); + // 3. If preventAbort is false, append the following action to actions: + if (!prevent_abort) { + abort_destination = GC::create_function(realm.heap(), [&realm, dest, error]() { + // 1. If dest.[[state]] is "writable", return ! WritableStreamAbort(dest, error). + if (dest->state() == WritableStream::State::Writable) + return writable_stream_abort(dest, error); - auto failure_steps = GC::create_function(realm.heap(), [promise, &realm, reader, writer](JS::Value error) { - // Make sure we close the acquired writer. - WebIDL::resolve_promise(realm, writable_stream_default_writer_close(*writer), JS::js_undefined()); - readable_stream_default_reader_release(*reader); + // 2. Otherwise, return a promise resolved with undefined. + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + }); + } - WebIDL::reject_promise(realm, promise, error); - }); + // 4. If preventCancel is false, append the following action action to actions: + if (!prevent_cancel) { + cancel_source = GC::create_function(realm.heap(), [&realm, source, error]() { + // 1. If source.[[state]] is "readable", return ! ReadableStreamCancel(source, error). + if (source->state() == ReadableStream::State::Readable) + return readable_stream_cancel(source, error); - reader->read_all_chunks(chunk_steps, success_steps, failure_steps); + // 2. Otherwise, return a promise resolved with undefined. + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + }); + } + + // 5. Shutdown with an action consisting of getting a promise to wait for all of the actions in actions, and with error. + auto action = GC::create_function(realm.heap(), [&realm, abort_destination, cancel_source]() { + GC::RootVector> actions(realm.heap()); + + if (abort_destination) + actions.append(abort_destination->function()()); + if (cancel_source) + actions.append(cancel_source->function()()); + + return WebIDL::get_promise_for_wait_for_all(realm, actions); + }); + + operation->shutdown_with_action(action, error); + }; + + // 2. If signal is aborted, perform abortAlgorithm and return promise. + if (signal->aborted()) { + abort_algorithm(); + return promise; + } + + // 3. Add abortAlgorithm to signal. + auto signal_id = signal->add_abort_algorithm(move(abort_algorithm)); + operation->set_abort_signal(*signal, signal_id.value()); + } + + // 15. In parallel (but not really; see #905), using reader and writer, read all chunks from source and write them + // to dest. Due to the locking provided by the reader and writer, the exact manner in which this happens is not + // observable to author code, and so there is flexibility in how this is done. + operation->process(); // 16. Return promise. return promise; @@ -2201,30 +2597,31 @@ void readable_stream_default_controller_can_pull_if_needed(ReadableStreamDefault // 6. Let pullPromise be the result of performing controller.[[pullAlgorithm]]. auto pull_promise = controller.pull_algorithm()->function()(); - // 7. Upon fulfillment of pullPromise, - WebIDL::upon_fulfillment(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[pulling]] to false. - controller.set_pulling(false); + WebIDL::react_to_promise(pull_promise, + // 7. Upon fulfillment of pullPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[pulling]] to false. + controller.set_pulling(false); - // 2. If controller.[[pullAgain]] is true, - if (controller.pull_again()) { - // 1. Set controller.[[pullAgain]] to false. - controller.set_pull_again(false); + // 2. If controller.[[pullAgain]] is true, + if (controller.pull_again()) { + // 1. Set controller.[[pullAgain]] to false. + controller.set_pull_again(false); - // 2. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). - readable_stream_default_controller_can_pull_if_needed(controller); - } + // 2. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + readable_stream_default_controller_can_pull_if_needed(controller); + } - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 8. Upon rejection of pullPromise with reason e, - WebIDL::upon_rejection(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value e) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableStreamDefaultControllerError(controller, e). - readable_stream_default_controller_error(controller, e); + // 8. Upon rejection of pullPromise with reason e, + GC::create_function(controller.heap(), [&controller](JS::Value e) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableStreamDefaultControllerError(controller, e). + readable_stream_default_controller_error(controller, e); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#readable-stream-default-controller-should-call-pull @@ -2628,30 +3025,31 @@ WebIDL::ExceptionOr set_up_readable_stream_default_controller(ReadableStre // 10. Let startPromise be a promise resolved with startResult. auto start_promise = WebIDL::create_resolved_promise(realm, start_result); - // 11. Upon fulfillment of startPromise, - WebIDL::upon_fulfillment(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[started]] to true. - controller.set_started(true); + WebIDL::react_to_promise(start_promise, + // 11. Upon fulfillment of startPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[started]] to true. + controller.set_started(true); - // 2. Assert: controller.[[pulling]] is false. - VERIFY(!controller.pulling()); + // 2. Assert: controller.[[pulling]] is false. + VERIFY(!controller.pulling()); - // 3. Assert: controller.[[pullAgain]] is false. - VERIFY(!controller.pull_again()); + // 3. Assert: controller.[[pullAgain]] is false. + VERIFY(!controller.pull_again()); - // 4. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). - readable_stream_default_controller_can_pull_if_needed(controller); + // 4. Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + readable_stream_default_controller_can_pull_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 12. Upon rejection of startPromise with reason r, - WebIDL::upon_rejection(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableStreamDefaultControllerError(controller, r). - readable_stream_default_controller_error(controller, r); + // 12. Upon rejection of startPromise with reason r, + GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableStreamDefaultControllerError(controller, r). + readable_stream_default_controller_error(controller, r); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); return {}; } @@ -2737,30 +3135,31 @@ void readable_byte_stream_controller_call_pull_if_needed(ReadableByteStreamContr // 6. Let pullPromise be the result of performing controller.[[pullAlgorithm]]. auto pull_promise = controller.pull_algorithm()->function()(); - // 7. Upon fulfillment of pullPromise, - WebIDL::upon_fulfillment(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[pulling]] to false. - controller.set_pulling(false); + WebIDL::react_to_promise(pull_promise, + // 7. Upon fulfillment of pullPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[pulling]] to false. + controller.set_pulling(false); - // 2. If controller.[[pullAgain]] is true, - if (controller.pull_again()) { - // 1. Set controller.[[pullAgain]] to false. - controller.set_pull_again(false); + // 2. If controller.[[pullAgain]] is true, + if (controller.pull_again()) { + // 1. Set controller.[[pullAgain]] to false. + controller.set_pull_again(false); - // 2. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - readable_byte_stream_controller_call_pull_if_needed(controller); - } + // 2. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + readable_byte_stream_controller_call_pull_if_needed(controller); + } - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 8. Upon rejection of pullPromise with reason e, - WebIDL::upon_rejection(*pull_promise, GC::create_function(controller.heap(), [&controller](JS::Value error) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableByteStreamControllerError(controller, e). - readable_byte_stream_controller_error(controller, error); + // 8. Upon rejection of pullPromise with reason e, + GC::create_function(controller.heap(), [&controller](JS::Value error) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableByteStreamControllerError(controller, e). + readable_byte_stream_controller_error(controller, error); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#readable-byte-stream-controller-clear-algorithms @@ -3244,30 +3643,31 @@ WebIDL::ExceptionOr set_up_readable_byte_stream_controller(ReadableStream& // 15. Let startPromise be a promise resolved with startResult. auto start_promise = WebIDL::create_resolved_promise(realm, start_result); - // 16. Upon fulfillment of startPromise, - WebIDL::upon_fulfillment(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { - // 1. Set controller.[[started]] to true. - controller.set_started(true); + WebIDL::react_to_promise(start_promise, + // 16. Upon fulfillment of startPromise, + GC::create_function(controller.heap(), [&controller](JS::Value) -> WebIDL::ExceptionOr { + // 1. Set controller.[[started]] to true. + controller.set_started(true); - // 2. Assert: controller.[[pulling]] is false. - VERIFY(!controller.pulling()); + // 2. Assert: controller.[[pulling]] is false. + VERIFY(!controller.pulling()); - // 3. Assert: controller.[[pullAgain]] is false. - VERIFY(!controller.pull_again()); + // 3. Assert: controller.[[pullAgain]] is false. + VERIFY(!controller.pull_again()); - // 4. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - readable_byte_stream_controller_call_pull_if_needed(controller); + // 4. Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + readable_byte_stream_controller_call_pull_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 17. Upon rejection of startPromise with reason r, - WebIDL::upon_rejection(start_promise, GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { - // 1. Perform ! ReadableByteStreamControllerError(controller, r). - readable_byte_stream_controller_error(controller, r); + // 17. Upon rejection of startPromise with reason r, + GC::create_function(controller.heap(), [&controller](JS::Value r) -> WebIDL::ExceptionOr { + // 1. Perform ! ReadableByteStreamControllerError(controller, r). + readable_byte_stream_controller_error(controller, r); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); return {}; } @@ -3784,27 +4184,28 @@ void writable_stream_finish_erroring(WritableStream& stream) // 12. Let promise be ! stream.[[controller]].[[AbortSteps]](abortRequest’s reason). auto promise = stream.controller()->abort_steps(abort_request.reason); - // 13. Upon fulfillment of promise, - WebIDL::upon_fulfillment(*promise, GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value) -> WebIDL::ExceptionOr { - // 1. Resolve abortRequest’s promise with undefined. - WebIDL::resolve_promise(realm, abort_promise, JS::js_undefined()); + WebIDL::react_to_promise(promise, + // 13. Upon fulfillment of promise, + GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value) -> WebIDL::ExceptionOr { + // 1. Resolve abortRequest’s promise with undefined. + WebIDL::resolve_promise(realm, abort_promise, JS::js_undefined()); - // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - writable_stream_reject_close_and_closed_promise_if_needed(stream); + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 14. Upon rejection of promise with reason reason, - WebIDL::upon_rejection(*promise, GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. Reject abortRequest’s promise with reason. - WebIDL::reject_promise(realm, abort_promise, reason); + // 14. Upon rejection of promise with reason reason, + GC::create_function(realm.heap(), [&realm, &stream, abort_promise = abort_request.promise](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Reject abortRequest’s promise with reason. + WebIDL::reject_promise(realm, abort_promise, reason); - // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - writable_stream_reject_close_and_closed_promise_if_needed(stream); + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#writable-stream-finish-in-flight-close @@ -4092,6 +4493,35 @@ GC::Ref writable_stream_default_writer_close(WritableStreamDefa return writable_stream_close(*stream); } +// https://streams.spec.whatwg.org/#writable-stream-default-writer-close-with-error-propagation +GC::Ref writable_stream_default_writer_close_with_error_propagation(WritableStreamDefaultWriter& writer) +{ + auto& realm = writer.realm(); + + // 1. Let stream be writer.[[stream]]. + auto stream = writer.stream(); + + // 2. Assert: stream is not undefined. + VERIFY(stream); + + // 3. Let state be stream.[[state]]. + auto state = stream->state(); + + // 4. If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state is "closed", return a promise resolved with undefined. + if (writable_stream_close_queued_or_in_flight(*stream) || state == WritableStream::State::Closed) + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); + + // 5. If state is "errored", return a promise rejected with stream.[[storedError]]. + if (state == WritableStream::State::Errored) + return WebIDL::create_rejected_promise(realm, stream->stored_error()); + + // 6. Assert: state is "writable" or "erroring". + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + + // 7. Return ! WritableStreamDefaultWriterClose(writer). + return writable_stream_default_writer_close(writer); +} + // https://streams.spec.whatwg.org/#writable-stream-default-writer-ensure-closed-promise-rejected void writable_stream_default_writer_ensure_closed_promise_rejected(WritableStreamDefaultWriter& writer, JS::Value error) { @@ -4286,35 +4716,36 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller(WritableStre // 16. Let startPromise be a promise resolved with startResult. auto start_promise = WebIDL::create_resolved_promise(realm, start_result); - // 17. Upon fulfillment of startPromise, - WebIDL::upon_fulfillment(*start_promise, GC::create_function(realm.heap(), [&controller, &stream](JS::Value) -> WebIDL::ExceptionOr { - // 1. Assert: stream.[[state]] is "writable" or "erroring". - auto state = stream.state(); - VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + WebIDL::react_to_promise(start_promise, + // 17. Upon fulfillment of startPromise, + GC::create_function(realm.heap(), [&controller, &stream](JS::Value) -> WebIDL::ExceptionOr { + // 1. Assert: stream.[[state]] is "writable" or "erroring". + auto state = stream.state(); + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); - // 2. Set controller.[[started]] to true. - controller.set_started(true); + // 2. Set controller.[[started]] to true. + controller.set_started(true); - // 3. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - writable_stream_default_controller_advance_queue_if_needed(controller); + // 3. Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + writable_stream_default_controller_advance_queue_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 18. Upon rejection of startPromise with reason r, - WebIDL::upon_rejection(*start_promise, GC::create_function(realm.heap(), [&stream, &controller](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. Assert: stream.[[state]] is "writable" or "erroring". - auto state = stream.state(); - VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + // 18. Upon rejection of startPromise with reason r, + GC::create_function(realm.heap(), [&stream, &controller](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Assert: stream.[[state]] is "writable" or "erroring". + auto state = stream.state(); + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); - // 2. Set controller.[[started]] to true. - controller.set_started(true); + // 2. Set controller.[[started]] to true. + controller.set_started(true); - // 3. Perform ! WritableStreamDealWithRejection(stream, r). - writable_stream_deal_with_rejection(stream, reason); + // 3. Perform ! WritableStreamDealWithRejection(stream, r). + writable_stream_deal_with_rejection(stream, reason); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); return {}; } @@ -4345,15 +4776,19 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller_from_underly return WebIDL::create_resolved_promise(realm, JS::js_undefined()); }); - // 6. If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["start"] with argument list « controller » and callback this value underlyingSink. + // 6. If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of + // invoking underlyingSinkDict["start"] with argument list « controller », exception behavior "rethrow", and + // callback this value underlyingSink. if (underlying_sink.start) { start_algorithm = GC::create_function(realm.heap(), [controller, underlying_sink_value, callback = underlying_sink.start]() -> WebIDL::ExceptionOr { // Note: callback does not return a promise, so invoke_callback may return an abrupt completion - return TRY(WebIDL::invoke_callback(*callback, underlying_sink_value, controller)); + return TRY(WebIDL::invoke_callback(*callback, underlying_sink_value, WebIDL::ExceptionBehavior::Rethrow, controller)); }); } - // 7. If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which takes an argument chunk and returns the result of invoking underlyingSinkDict["write"] with argument list « chunk, controller » and callback this value underlyingSink. + // 7. If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which takes an argument chunk + // and returns the result of invoking underlyingSinkDict["write"] with argument list « chunk, controller » and + // callback this value underlyingSink. if (underlying_sink.write) { write_algorithm = GC::create_function(realm.heap(), [&realm, controller, underlying_sink_value, callback = underlying_sink.write](JS::Value chunk) { // Note: callback returns a promise, so invoke_callback will never return an abrupt completion @@ -4362,7 +4797,8 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller_from_underly }); } - // 8. If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["close"] with argument list «» and callback this value underlyingSink. + // 8. If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which returns the result of + // invoking underlyingSinkDict["close"] with argument list «» and callback this value underlyingSink. if (underlying_sink.close) { close_algorithm = GC::create_function(realm.heap(), [&realm, underlying_sink_value, callback = underlying_sink.close]() { // Note: callback returns a promise, so invoke_callback will never return an abrupt completion @@ -4371,7 +4807,9 @@ WebIDL::ExceptionOr set_up_writable_stream_default_controller_from_underly }); } - // 9. If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSinkDict["abort"] with argument list « reason » and callback this value underlyingSink. + // 9. If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which takes an argument reason + // and returns the result of invoking underlyingSinkDict["abort"] with argument list « reason » and callback this + // value underlyingSink. if (underlying_sink.abort) { abort_algorithm = GC::create_function(realm.heap(), [&realm, underlying_sink_value, callback = underlying_sink.abort](JS::Value reason) { // Note: callback returns a promise, so invoke_callback will never return an abrupt completion @@ -4548,21 +4986,22 @@ void writable_stream_default_controller_process_close(WritableStreamDefaultContr // 6. Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). writable_stream_default_controller_clear_algorithms(controller); - // 7. Upon fulfillment of sinkClosePromise, - WebIDL::upon_fulfillment(*sink_close_promise, GC::create_function(controller.heap(), [stream](JS::Value) -> WebIDL::ExceptionOr { - // 1. Perform ! WritableStreamFinishInFlightClose(stream). - writable_stream_finish_in_flight_close(*stream); + WebIDL::react_to_promise(sink_close_promise, + // 7. Upon fulfillment of sinkClosePromise, + GC::create_function(controller.heap(), [stream](JS::Value) -> WebIDL::ExceptionOr { + // 1. Perform ! WritableStreamFinishInFlightClose(stream). + writable_stream_finish_in_flight_close(*stream); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 8. Upon rejection of sinkClosePromise with reason reason, - WebIDL::upon_rejection(*sink_close_promise, GC::create_function(controller.heap(), [stream = stream](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. Perform ! WritableStreamFinishInFlightCloseWithError(stream, reason). - writable_stream_finish_in_flight_close_with_error(*stream, reason); + // 8. Upon rejection of sinkClosePromise with reason reason, + GC::create_function(controller.heap(), [stream = stream](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. Perform ! WritableStreamFinishInFlightCloseWithError(stream, reason). + writable_stream_finish_in_flight_close_with_error(*stream, reason); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#writable-stream-default-controller-process-write @@ -4577,46 +5016,47 @@ void writable_stream_default_controller_process_write(WritableStreamDefaultContr // 3. Let sinkWritePromise be the result of performing controller.[[writeAlgorithm]], passing in chunk. auto sink_write_promise = controller.write_algorithm()->function()(chunk); - // 4. Upon fulfillment of sinkWritePromise, - WebIDL::upon_fulfillment(*sink_write_promise, GC::create_function(controller.heap(), [&controller, stream](JS::Value) -> WebIDL::ExceptionOr { - // 1. Perform ! WritableStreamFinishInFlightWrite(stream). - writable_stream_finish_in_flight_write(*stream); + WebIDL::react_to_promise(sink_write_promise, + // 4. Upon fulfillment of sinkWritePromise, + GC::create_function(controller.heap(), [&controller, stream](JS::Value) -> WebIDL::ExceptionOr { + // 1. Perform ! WritableStreamFinishInFlightWrite(stream). + writable_stream_finish_in_flight_write(*stream); - // 2. Let state be stream.[[state]]. - auto state = stream->state(); + // 2. Let state be stream.[[state]]. + auto state = stream->state(); - // 3. Assert: state is "writable" or "erroring". - VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); + // 3. Assert: state is "writable" or "erroring". + VERIFY(state == WritableStream::State::Writable || state == WritableStream::State::Erroring); - // 4. Perform ! DequeueValue(controller). - dequeue_value(controller); + // 4. Perform ! DequeueValue(controller). + dequeue_value(controller); - // 5. If ! WritableStreamCloseQueuedOrInFlight(stream) is false and state is "writable", - if (!writable_stream_close_queued_or_in_flight(*stream) && state == WritableStream::State::Writable) { - // 1. Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). - auto backpressure = writable_stream_default_controller_get_backpressure(controller); + // 5. If ! WritableStreamCloseQueuedOrInFlight(stream) is false and state is "writable", + if (!writable_stream_close_queued_or_in_flight(*stream) && state == WritableStream::State::Writable) { + // 1. Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). + auto backpressure = writable_stream_default_controller_get_backpressure(controller); - // 2. Perform ! WritableStreamUpdateBackpressure(stream, backpressure). - writable_stream_update_backpressure(*stream, backpressure); - } + // 2. Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + writable_stream_update_backpressure(*stream, backpressure); + } - // 6 .Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - writable_stream_default_controller_advance_queue_if_needed(controller); + // 6 .Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + writable_stream_default_controller_advance_queue_if_needed(controller); - return JS::js_undefined(); - })); + return JS::js_undefined(); + }), - // 5. Upon rejection of sinkWritePromise with reason, - WebIDL::upon_rejection(*sink_write_promise, GC::create_function(controller.heap(), [&controller, stream](JS::Value reason) -> WebIDL::ExceptionOr { - // 1. If stream.[[state]] is "writable", perform ! WritableStreamDefaultControllerClearAlgorithms(controller). - if (stream->state() == WritableStream::State::Writable) - writable_stream_default_controller_clear_algorithms(controller); + // 5. Upon rejection of sinkWritePromise with reason, + GC::create_function(controller.heap(), [&controller, stream](JS::Value reason) -> WebIDL::ExceptionOr { + // 1. If stream.[[state]] is "writable", perform ! WritableStreamDefaultControllerClearAlgorithms(controller). + if (stream->state() == WritableStream::State::Writable) + writable_stream_default_controller_clear_algorithms(controller); - // 2. Perform ! WritableStreamFinishInFlightWriteWithError(stream, reason). - writable_stream_finish_in_flight_write_with_error(*stream, reason); + // 2. Perform ! WritableStreamFinishInFlightWriteWithError(stream, reason). + writable_stream_finish_in_flight_write_with_error(*stream, reason); - return JS::js_undefined(); - })); + return JS::js_undefined(); + })); } // https://streams.spec.whatwg.org/#writable-stream-default-controller-write diff --git a/Libraries/LibWeb/Streams/AbstractOperations.h b/Libraries/LibWeb/Streams/AbstractOperations.h index 91e5f711646..7683c8d5e56 100644 --- a/Libraries/LibWeb/Streams/AbstractOperations.h +++ b/Libraries/LibWeb/Streams/AbstractOperations.h @@ -40,7 +40,7 @@ size_t readable_stream_get_num_read_requests(ReadableStream const&); bool readable_stream_has_byob_reader(ReadableStream const&); bool readable_stream_has_default_reader(ReadableStream const&); -GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool prevent_close, bool prevent_abort, bool prevent_cancel, JS::Value signal = JS::js_undefined()); +GC::Ref readable_stream_pipe_to(ReadableStream& source, WritableStream& dest, bool prevent_close, bool prevent_abort, bool prevent_cancel, GC::Ptr signal = {}); WebIDL::ExceptionOr readable_stream_tee(JS::Realm&, ReadableStream&, bool clone_for_branch2); WebIDL::ExceptionOr readable_stream_default_tee(JS::Realm& realm, ReadableStream& stream, bool clone_for_branch2); @@ -135,6 +135,7 @@ void writable_stream_update_backpressure(WritableStream&, bool backpressure); GC::Ref writable_stream_default_writer_abort(WritableStreamDefaultWriter&, JS::Value reason); GC::Ref writable_stream_default_writer_close(WritableStreamDefaultWriter&); +GC::Ref writable_stream_default_writer_close_with_error_propagation(WritableStreamDefaultWriter&); void writable_stream_default_writer_ensure_closed_promise_rejected(WritableStreamDefaultWriter&, JS::Value error); void writable_stream_default_writer_ensure_ready_promise_rejected(WritableStreamDefaultWriter&, JS::Value error); Optional writable_stream_default_writer_get_desired_size(WritableStreamDefaultWriter const&); diff --git a/Libraries/LibWeb/Streams/ReadableStream.cpp b/Libraries/LibWeb/Streams/ReadableStream.cpp index 91e1373e084..c67fc50e1e0 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.cpp +++ b/Libraries/LibWeb/Streams/ReadableStream.cpp @@ -135,7 +135,7 @@ WebIDL::ExceptionOr> ReadableStream::pipe_through(Readab return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Failed to execute 'pipeThrough' on 'ReadableStream': parameter 1's 'writable' is locked"sv }; // 3. Let signal be options["signal"] if it exists, or undefined otherwise. - auto signal = options.signal ? JS::Value(options.signal) : JS::js_undefined(); + auto signal = options.signal; // 4. Let promise be ! ReadableStreamPipeTo(this, transform["writable"], options["preventClose"], options["preventAbort"], options["preventCancel"], signal). auto promise = readable_stream_pipe_to(*this, *transform.writable, options.prevent_close, options.prevent_abort, options.prevent_cancel, signal); @@ -164,7 +164,7 @@ GC::Ref ReadableStream::pipe_to(WritableStream& destination, St } // 3. Let signal be options["signal"] if it exists, or undefined otherwise. - auto signal = options.signal ? JS::Value(options.signal) : JS::js_undefined(); + auto signal = options.signal; // 4. Return ! ReadableStreamPipeTo(this, destination, options["preventClose"], options["preventAbort"], options["preventCancel"], signal). return readable_stream_pipe_to(*this, destination, options.prevent_close, options.prevent_abort, options.prevent_cancel, signal); @@ -427,7 +427,7 @@ void ReadableStream::set_up_with_byte_reading_support(GC::Ptr pul } // https://streams.spec.whatwg.org/#readablestream-pipe-through -GC::Ref ReadableStream::piped_through(GC::Ref transform, bool prevent_close, bool prevent_abort, bool prevent_cancel, JS::Value signal) +GC::Ref ReadableStream::piped_through(GC::Ref transform, bool prevent_close, bool prevent_abort, bool prevent_cancel, GC::Ptr signal) { // 1. Assert: ! IsReadableStreamLocked(readable) is false. VERIFY(!is_readable_stream_locked(*this)); diff --git a/Libraries/LibWeb/Streams/ReadableStream.h b/Libraries/LibWeb/Streams/ReadableStream.h index af7e9f4fd8c..a82c7b161e6 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.h +++ b/Libraries/LibWeb/Streams/ReadableStream.h @@ -109,7 +109,7 @@ public: WebIDL::ExceptionOr pull_from_bytes(ByteBuffer); WebIDL::ExceptionOr enqueue(JS::Value chunk); void set_up_with_byte_reading_support(GC::Ptr = {}, GC::Ptr = {}, double high_water_mark = 0); - GC::Ref piped_through(GC::Ref, bool prevent_close = false, bool prevent_abort = false, bool prevent_cancel = false, JS::Value signal = JS::js_undefined()); + GC::Ref piped_through(GC::Ref, bool prevent_close = false, bool prevent_abort = false, bool prevent_cancel = false, GC::Ptr signal = {}); GC::Ptr current_byob_request_view(); diff --git a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp index 8ab80faecb1..1aa1bde0560 100644 --- a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp +++ b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.cpp @@ -66,6 +66,7 @@ void ReadableStreamDefaultReader::visit_edges(Cell::Visitor& visitor) ReadableStreamGenericReaderMixin::visit_edges(visitor); for (auto& request : m_read_requests) visitor.visit(request); + visitor.visit(m_readable_stream_pipe_to_operation); } // https://streams.spec.whatwg.org/#read-loop @@ -212,42 +213,6 @@ void ReadableStreamDefaultReader::read_all_bytes(GC::Ref chunk_steps, GC::Ref success_steps, GC::Ref failure_steps) -{ - // AD-HOC: Some spec steps direct us to "read all chunks" from a stream, but there isn't an AO defined to do that. - // We implement those steps by continuously making default read requests, which is an identity transformation, - // with a custom callback to receive each chunk that is read. This is done until the controller signals - // that there are no more chunks to consume. - // This function is based on "read_all_bytes" above. - auto promise_capability = read(); - - WebIDL::react_to_promise( - promise_capability, - GC::create_function(heap(), [this, chunk_steps, success_steps, failure_steps](JS::Value value) -> WebIDL::ExceptionOr { - auto& vm = this->vm(); - - VERIFY(value.is_object()); - auto& value_object = value.as_object(); - - auto done = MUST(JS::iterator_complete(vm, value_object)); - - if (!done) { - auto chunk = MUST(JS::iterator_value(vm, value_object)); - chunk_steps->function()(chunk); - - read_all_chunks(chunk_steps, success_steps, failure_steps); - } else { - success_steps->function()(); - } - - return JS::js_undefined(); - }), - GC::create_function(heap(), [failure_steps](JS::Value error) -> WebIDL::ExceptionOr { - failure_steps->function()(error); - return JS::js_undefined(); - })); -} - // FIXME: This function is a promise-based wrapper around "read all bytes". The spec changed this function to not use promises // in https://github.com/whatwg/streams/commit/f894acdd417926a2121710803cef593e15127964 - however, it seems that the // FileAPI blob specification has not been updated to match, see: https://github.com/w3c/FileAPI/issues/187. diff --git a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h index 8e37bf476e7..de71cb17c39 100644 --- a/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h +++ b/Libraries/LibWeb/Streams/ReadableStreamDefaultReader.h @@ -20,6 +20,8 @@ struct ReadableStreamReadResult { bool done; }; +class ReadableStreamPipeTo; + class ReadRequest : public JS::Cell { GC_CELL(ReadRequest, JS::Cell); @@ -91,13 +93,14 @@ public: void read_a_chunk(Fetch::Infrastructure::IncrementalReadLoopReadRequest& read_request); void read_all_bytes(GC::Ref, GC::Ref); - void read_all_chunks(GC::Ref, GC::Ref, GC::Ref); GC::Ref read_all_bytes_deprecated(); void release_lock(); SinglyLinkedList>& read_requests() { return m_read_requests; } + void set_readable_stream_pipe_to_operation(Badge, GC::Ptr readable_stream_pipe_to_operation) { m_readable_stream_pipe_to_operation = readable_stream_pipe_to_operation; } + private: explicit ReadableStreamDefaultReader(JS::Realm&); @@ -106,6 +109,8 @@ private: virtual void visit_edges(Cell::Visitor&) override; SinglyLinkedList> m_read_requests; + + GC::Ptr m_readable_stream_pipe_to_operation; }; } diff --git a/Libraries/LibWeb/Streams/WritableStreamDefaultController.h b/Libraries/LibWeb/Streams/WritableStreamDefaultController.h index 5a0fa8fafb4..4ffc70f3663 100644 --- a/Libraries/LibWeb/Streams/WritableStreamDefaultController.h +++ b/Libraries/LibWeb/Streams/WritableStreamDefaultController.h @@ -38,8 +38,8 @@ public: bool started() const { return m_started; } void set_started(bool value) { m_started = value; } - size_t strategy_hwm() const { return m_strategy_hwm; } - void set_strategy_hwm(size_t value) { m_strategy_hwm = value; } + double strategy_hwm() const { return m_strategy_hwm; } + void set_strategy_hwm(double value) { m_strategy_hwm = value; } GC::Ptr strategy_size_algorithm() { return m_strategy_size_algorithm; } void set_strategy_size_algorithm(GC::Ptr value) { m_strategy_size_algorithm = value; } @@ -86,7 +86,7 @@ private: // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-strategyhwm // A number supplied by the creator of the stream as part of the stream’s queuing strategy, indicating the point at which the stream will apply backpressure to its underlying sink - size_t m_strategy_hwm { 0 }; + double m_strategy_hwm { 0 }; // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-strategysizealgorithm // An algorithm to calculate the size of enqueued chunks, as part of the stream’s queuing strategy diff --git a/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp b/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp index fda31a480fb..ab12ba49080 100644 --- a/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp +++ b/Libraries/LibWeb/UIEvents/KeyboardEvent.cpp @@ -307,7 +307,7 @@ static ErrorOr get_event_key(KeyCode platform_key, u32 code_point) // FIXME: 4. Else, if the key event has any modifier keys other than glyph modifier keys, then // FIXME: 1. Set key to the key string that would have been generated by this event if it had been typed with all - // modifer keys removed except for glyph modifier keys. + // modifier keys removed except for glyph modifier keys. // 5. Return key as the key attribute value for this key event. return "Unidentified"_string; diff --git a/Libraries/LibWeb/WebAssembly/WebAssembly.cpp b/Libraries/LibWeb/WebAssembly/WebAssembly.cpp index 89613052c6b..50eb5262e39 100644 --- a/Libraries/LibWeb/WebAssembly/WebAssembly.cpp +++ b/Libraries/LibWeb/WebAssembly/WebAssembly.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include diff --git a/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp b/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp index e181a6b44bc..9d654a3722c 100644 --- a/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp +++ b/Libraries/LibWeb/WebAudio/ChannelSplitterNode.cpp @@ -77,7 +77,7 @@ WebIDL::ExceptionOr ChannelSplitterNode::set_channel_count_mode(Bindings:: WebIDL::ExceptionOr ChannelSplitterNode::set_channel_interpretation(Bindings::ChannelInterpretation channel_interpretation) { // https://webaudio.github.io/web-audio-api/#audionode-channelinterpretation-constraints - // The channel intepretation can not be changed from "discrete" and a InvalidStateError exception MUST be thrown for any attempt to change the value. + // The channel interpretation can not be changed from "discrete" and a InvalidStateError exception MUST be thrown for any attempt to change the value. if (channel_interpretation != Bindings::ChannelInterpretation::Discrete) return WebIDL::InvalidStateError::create(realm(), "Channel interpretation must be 'discrete'"_string); diff --git a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp index a7db8feccc4..e4951b8c10a 100644 --- a/Libraries/LibWeb/WebDriver/ExecuteScript.cpp +++ b/Libraries/LibWeb/WebDriver/ExecuteScript.cpp @@ -21,6 +21,19 @@ namespace Web::WebDriver { +// https://w3ctag.github.io/promises-guide/#should-promise-call +static GC::Ref promise_call(JS::Realm& realm, JS::ThrowCompletionOr result) +{ + // If the developer supplies you with a function that you expect to return a promise, you should also allow it to + // return a thenable or non-promise value, or even throw an exception, and treat all these cases as if they had + // returned an analogous promise. This should be done by converting the returned value to a promise, as if by using + // Promise.resolve(), and catching thrown exceptions and converting those into a promise as if by using + // Promise.reject(). We call this "promise-calling" the function. + if (result.is_error()) + return WebIDL::create_rejected_promise(realm, result.error_value()); + return WebIDL::create_resolved_promise(realm, result.release_value()); +} + // https://w3c.github.io/webdriver/#dfn-execute-a-function-body static JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingContext const& browsing_context, StringView body, ReadonlySpan parameters) { @@ -34,6 +47,8 @@ static JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingCo auto& realm = environment_settings.realm(); auto& global_scope = realm.global_environment(); + // FIXME: This does not handle scripts which contain `await` statements. It is not as as simple as declaring this + // function async, unfortunately. See: https://github.com/w3c/webdriver/issues/1436 auto source_text = ByteString::formatted( R"~~~(function() {{ {} @@ -89,6 +104,22 @@ static JS::ThrowCompletionOr execute_a_function_body(HTML::BrowsingCo return completion; } +static void fire_completion_when_resolved(GC::Ref promise, GC::Ref timer, GC::Ref on_complete) +{ + auto reaction_steps = GC::create_function(promise->heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { + if (timer->is_timed_out()) + return JS::js_undefined(); + timer->stop(); + + auto const& underlying_promise = as(*promise->promise()); + on_complete->function()({ underlying_promise.state(), underlying_promise.result() }); + + return JS::js_undefined(); + }); + + WebIDL::react_to_promise(promise, reaction_steps, reaction_steps); +} + void execute_script(HTML::BrowsingContext const& browsing_context, String body, GC::RootVector arguments, Optional const& timeout_ms, GC::Ref on_complete) { auto const* document = browsing_context.active_document(); @@ -114,36 +145,29 @@ void execute_script(HTML::BrowsingContext const& browsing_context, String body, // 8. Run the following substeps in parallel: Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, &browsing_context, promise, body = move(body), arguments = move(arguments)]() mutable { - HTML::TemporaryExecutionContext execution_context { realm }; + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 1. Let scriptPromise be the result of promise-calling execute a function body, with arguments body and arguments. - auto script_result = execute_a_function_body(browsing_context, body, move(arguments)); + auto script_promise = promise_call(realm, execute_a_function_body(browsing_context, body, arguments)); - // FIXME: This isn't right, we should be reacting to this using WebIDL::react_to_promise() - // 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v. - if (script_result.has_value()) { - WebIDL::resolve_promise(realm, promise, script_result.release_value()); - } + WebIDL::react_to_promise(script_promise, + // 2. Upon fulfillment of scriptPromise with value v, resolve promise with value v. + GC::create_function(realm.heap(), [&realm, promise](JS::Value value) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::resolve_promise(realm, promise, value); + return JS::js_undefined(); + }), - // 3. Upon rejection of scriptPromise with value r, reject promise with value r. - if (script_result.is_throw_completion()) { - WebIDL::reject_promise(realm, promise, script_result.throw_completion().value()); - } + // 3. Upon rejection of scriptPromise with value r, reject promise with value r. + GC::create_function(realm.heap(), [&realm, promise](JS::Value reason) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::reject_promise(realm, promise, reason); + return JS::js_undefined(); + })); })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. - auto reaction_steps = GC::create_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { - if (timer->is_timed_out()) - return JS::js_undefined(); - timer->stop(); - - auto promise_promise = GC::Ref { as(*promise->promise()) }; - on_complete->function()({ promise_promise->state(), promise_promise->result() }); - - return JS::js_undefined(); - }); - - WebIDL::react_to_promise(promise, reaction_steps, reaction_steps); + fire_completion_when_resolved(promise, timer, on_complete); } // https://w3c.github.io/webdriver/#execute-async-script @@ -168,15 +192,14 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 7. Let promise be a new Promise. - auto promise_capability = WebIDL::create_promise(realm); - GC::Ref promise { as(*promise_capability->promise()) }; + auto promise = WebIDL::create_promise(realm); // 8. Run the following substeps in parallel: - Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&vm, &realm, &browsing_context, timer, promise_capability, promise, body = move(body), arguments = move(arguments)]() mutable { - HTML::TemporaryExecutionContext execution_context { realm }; + Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&vm, &realm, &browsing_context, promise, body = move(body), arguments = move(arguments)]() mutable { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; // 1. Let resolvingFunctions be CreateResolvingFunctions(promise). - auto resolving_functions = promise->create_resolving_functions(); + auto resolving_functions = as(*promise->promise()).create_resolving_functions(); // 2. Append resolvingFunctions.[[Resolve]] to arguments. arguments.append(resolving_functions.resolve); @@ -189,7 +212,7 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String // In order to preserve legacy behavior, the return value only influences the command if it is a // "thenable" object or if determining this produces an exception. if (script_result.is_throw_completion()) { - promise->reject(script_result.throw_completion().value()); + WebIDL::reject_promise(realm, promise, script_result.error_value()); return; } @@ -202,7 +225,7 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String // 7. If then.[[Type]] is not normal, then reject promise with value then.[[Value]], and abort these steps. if (then.is_throw_completion()) { - promise->reject(then.throw_completion().value()); + WebIDL::reject_promise(realm, promise, then.error_value()); return; } @@ -211,35 +234,26 @@ void execute_async_script(HTML::BrowsingContext const& browsing_context, String return; // 9. Let scriptPromise be PromiseResolve(Promise, scriptResult.[[Value]]). - auto script_promise_or_error = JS::promise_resolve(vm, realm.intrinsics().promise_constructor(), script_result.value()); - if (script_promise_or_error.is_throw_completion()) - return; - auto& script_promise = static_cast(*script_promise_or_error.value()); + auto script_promise = WebIDL::create_resolved_promise(realm, script_result.value()); - vm.custom_data()->spin_event_loop_until(GC::create_function(vm.heap(), [timer, &script_promise]() { - return timer->is_timed_out() || script_promise.state() != JS::Promise::State::Pending; - })); + WebIDL::react_to_promise(script_promise, + // 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v. + GC::create_function(realm.heap(), [&realm, promise](JS::Value value) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::resolve_promise(realm, promise, value); + return JS::js_undefined(); + }), - // 10. Upon fulfillment of scriptPromise with value v, resolve promise with value v. - if (script_promise.state() == JS::Promise::State::Fulfilled) - WebIDL::resolve_promise(realm, promise_capability, script_promise.result()); - - // 11. Upon rejection of scriptPromise with value r, reject promise with value r. - if (script_promise.state() == JS::Promise::State::Rejected) - WebIDL::reject_promise(realm, promise_capability, script_promise.result()); + // 11. Upon rejection of scriptPromise with value r, reject promise with value r. + GC::create_function(realm.heap(), [&realm, promise](JS::Value reason) -> WebIDL::ExceptionOr { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + WebIDL::reject_promise(realm, promise, reason); + return JS::js_undefined(); + })); })); // 9. Wait until promise is resolved, or timer's timeout fired flag is set, whichever occurs first. - auto reaction_steps = GC::create_function(vm.heap(), [promise, timer, on_complete](JS::Value) -> WebIDL::ExceptionOr { - if (timer->is_timed_out()) - return JS::js_undefined(); - timer->stop(); - - on_complete->function()({ promise->state(), promise->result() }); - return JS::js_undefined(); - }); - - WebIDL::react_to_promise(promise_capability, reaction_steps, reaction_steps); + fire_completion_when_resolved(promise, timer, on_complete); } } diff --git a/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl b/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl index 96870119577..82636b16a1e 100644 --- a/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl +++ b/Libraries/LibWeb/WebGL/WebGLRenderingContextBase.idl @@ -333,7 +333,7 @@ interface mixin WebGLRenderingContextBase { const GLenum SAMPLE_COVERAGE_VALUE = 0x80AA; const GLenum SAMPLE_COVERAGE_INVERT = 0x80AB; - // GetTexureParameter + // GetTextureParameter const GLenum COMPRESSED_TEXTURE_FORMATS = 0x86A3; // HintMode diff --git a/Libraries/LibWeb/WebIDL/Promise.cpp b/Libraries/LibWeb/WebIDL/Promise.cpp index cf1180e5d0c..df5733d9db0 100644 --- a/Libraries/LibWeb/WebIDL/Promise.cpp +++ b/Libraries/LibWeb/WebIDL/Promise.cpp @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace Web::WebIDL { @@ -173,8 +175,14 @@ GC::Ref upon_rejection(Promise const& promise, GC::Ref s void mark_promise_as_handled(Promise const& promise) { // To mark as handled a Promise promise, set promise.[[Promise]].[[PromiseIsHandled]] to true. - auto promise_object = as(promise.promise().ptr()); - promise_object->set_is_handled(); + auto& promise_object = as(*promise.promise()); + promise_object.set_is_handled(); +} + +bool is_promise_fulfilled(Promise const& promise) +{ + auto const& promise_object = as(*promise.promise()); + return promise_object.state() == JS::Promise::State::Fulfilled; } struct WaitForAllResults : JS::Cell { @@ -290,6 +298,36 @@ void wait_for_all(JS::Realm& realm, Vector> const& promises, Fu } } +// https://webidl.spec.whatwg.org/#waiting-for-all-promise +GC::Ref get_promise_for_wait_for_all(JS::Realm& realm, Vector> const& promises) +{ + // 1. Let promise be a new promise of type Promise> in realm. + auto promise = create_promise(realm); + + // 2. Let successSteps be the following steps, given results: + auto success_steps = [&realm, promise](Vector const& results) { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. Resolve promise with results. + auto sequence = JS::Array::create_from(realm, results); + resolve_promise(realm, promise, sequence); + }; + + // 3. Let failureSteps be the following steps, given reason: + auto failure_steps = [&realm, promise](JS::Value reason) { + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. Reject promise with reason. + reject_promise(realm, promise, reason); + }; + + // 4. Wait for all with promises, given successSteps and failureSteps. + wait_for_all(realm, promises, move(success_steps), move(failure_steps)); + + // 5. Return promise. + return promise; +} + GC::Ref create_rejected_promise_from_exception(JS::Realm& realm, Exception exception) { auto throw_completion = Bindings::exception_to_throw_completion(realm.vm(), move(exception)); diff --git a/Libraries/LibWeb/WebIDL/Promise.h b/Libraries/LibWeb/WebIDL/Promise.h index 555c6f31c3b..6fdfbb49799 100644 --- a/Libraries/LibWeb/WebIDL/Promise.h +++ b/Libraries/LibWeb/WebIDL/Promise.h @@ -29,7 +29,9 @@ GC::Ref react_to_promise(Promise const&, GC::Ptr on_fulf GC::Ref upon_fulfillment(Promise const&, GC::Ref); GC::Ref upon_rejection(Promise const&, GC::Ref); void mark_promise_as_handled(Promise const&); +bool is_promise_fulfilled(Promise const&); void wait_for_all(JS::Realm&, Vector> const& promises, Function const&)> success_steps, Function failure_steps); +GC::Ref get_promise_for_wait_for_all(JS::Realm&, Vector> const& promises); // Non-spec, convenience method. GC::Ref create_rejected_promise_from_exception(JS::Realm&, Exception); diff --git a/Libraries/LibWeb/WebSockets/WebSocket.cpp b/Libraries/LibWeb/WebSockets/WebSocket.cpp index 35258f9bb3c..d4868bbb860 100644 --- a/Libraries/LibWeb/WebSockets/WebSocket.cpp +++ b/Libraries/LibWeb/WebSockets/WebSocket.cpp @@ -193,9 +193,9 @@ ErrorOr WebSocket::establish_web_socket_connection(URL::URL const& url_rec auto& window_or_worker = as(client.global_object()); auto origin_string = window_or_worker.origin().to_byte_string(); - Vector protcol_byte_strings; + Vector protocol_byte_strings; for (auto const& protocol : protocols) - TRY(protcol_byte_strings.try_append(protocol.to_byte_string())); + TRY(protocol_byte_strings.try_append(protocol.to_byte_string())); HTTP::HeaderMap additional_headers; @@ -213,7 +213,7 @@ ErrorOr WebSocket::establish_web_socket_connection(URL::URL const& url_rec additional_headers.set("Cookie", cookies.to_byte_string()); } - m_websocket = ResourceLoader::the().request_client().websocket_connect(url_record, origin_string, protcol_byte_strings, {}, additional_headers); + m_websocket = ResourceLoader::the().request_client().websocket_connect(url_record, origin_string, protocol_byte_strings, {}, additional_headers); m_websocket->on_open = [weak_this = make_weak_ptr()] { if (!weak_this) diff --git a/Libraries/LibWeb/Worker/WebWorkerClient.cpp b/Libraries/LibWeb/Worker/WebWorkerClient.cpp index c4a63fae79a..1778595ff39 100644 --- a/Libraries/LibWeb/Worker/WebWorkerClient.cpp +++ b/Libraries/LibWeb/Worker/WebWorkerClient.cpp @@ -11,7 +11,7 @@ namespace Web::HTML { void WebWorkerClient::die() { - // FIXME: Notify WorkerAgent that the worker is ded + // FIXME: Notify WorkerAgent that the worker is dead } void WebWorkerClient::did_close_worker() diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index 8faee7a4ce3..3c5a448afc8 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -512,8 +512,8 @@ static void edit_dom_node(DevTools::TabDescription const& description, Applicati return; } - view->on_finshed_editing_dom_node = [&view = *view, on_complete = move(on_complete)](auto node_id) { - view.on_finshed_editing_dom_node = nullptr; + view->on_finished_editing_dom_node = [&view = *view, on_complete = move(on_complete)](auto node_id) { + view.on_finished_editing_dom_node = nullptr; if (node_id.has_value()) on_complete(*node_id); diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index 5d823ff0591..6c83ad5ac3d 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -688,7 +688,7 @@ NonnullRefPtr> ViewImplementation::take_screenshot(Sc auto promise = Core::Promise::construct(); if (m_pending_screenshot) { - // For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need + // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need // to allow spamming screenshot requests for some reason. promise->reject(Error::from_string_literal("A screenshot request is already in progress")); return promise; @@ -720,7 +720,7 @@ NonnullRefPtr> ViewImplementation::take_dom_node_scre auto promise = Core::Promise::construct(); if (m_pending_screenshot) { - // For simplicitly, only allow taking one screenshot at a time for now. Revisit if we need + // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need // to allow spamming screenshot requests for some reason. promise->reject(Error::from_string_literal("A screenshot request is already in progress")); return promise; @@ -749,7 +749,7 @@ NonnullRefPtr> ViewImplementation::request_internal_page_i auto promise = Core::Promise::construct(); if (m_pending_info_request) { - // For simplicitly, only allow one info request at a time for now. + // For simplicity, only allow one info request at a time for now. promise->reject(Error::from_string_literal("A page info request is already in progress")); return promise; } diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 5a60dcefdfd..d43bc370c47 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -201,7 +201,7 @@ public: Function on_received_accessibility_tree; Function on_received_hovered_node_id; Function on_dom_mutation_received; - Function const& node_id)> on_finshed_editing_dom_node; + Function const& node_id)> on_finished_editing_dom_node; Function on_received_dom_node_html; Function)> on_received_style_sheet_list; Function on_received_style_sheet_source; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index ede5a7fadc2..ebffe484135 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -341,8 +341,8 @@ void WebContentClient::did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID no void WebContentClient::did_finish_editing_dom_node(u64 page_id, Optional node_id) { if (auto view = view_for_page_id(page_id); view.has_value()) { - if (view->on_finshed_editing_dom_node) - view->on_finshed_editing_dom_node(node_id); + if (view->on_finished_editing_dom_node) + view->on_finished_editing_dom_node(node_id); } } diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 47d2db25da5..7e947c28df9 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -24,6 +24,7 @@ set(HTML_SCRIPT_DEBUG ON) set(HTTPJOB_DEBUG ON) set(HUNKS_DEBUG ON) set(ICO_DEBUG ON) +set(IDB_DEBUG ON) set(IDL_DEBUG ON) set(IMAGE_DECODER_DEBUG ON) set(IMAGE_LOADER_DEBUG ON) diff --git a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp index 4d041620171..c7fe62dfa4a 100644 --- a/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/IPCCompiler/main.cpp @@ -404,7 +404,7 @@ public:)~~~"); static i32 static_message_id() { return (int)MessageID::@message.pascal_name@; } virtual const char* message_name() const override { return "@endpoint.name@::@message.pascal_name@"; } - static ErrorOr> decode(Stream& stream, IPC::UnprocessedFileDescriptors& files) + static ErrorOr> decode(Stream& stream, Queue& files) { IPC::Decoder decoder { stream, files };)~~~"); @@ -649,7 +649,7 @@ void generate_proxy_method(SourceGenerator& message_generator, Endpoint const& e } } else { message_generator.append(R"~~~()); - MUST(m_connection.post_message(@endpoint.magic@, move(message_buffer))); )~~~"); + MUST(m_connection.post_message(move(message_buffer))); )~~~"); } message_generator.appendln(R"~~~( @@ -720,7 +720,7 @@ public: static u32 static_magic() { return @endpoint.magic@; } - static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] IPC::UnprocessedFileDescriptors& files) + static ErrorOr> decode_message(ReadonlyBytes buffer, [[maybe_unused]] Queue& files) { FixedMemoryStream stream { buffer }; auto message_endpoint_magic = TRY(stream.read_value());)~~~"); @@ -757,11 +757,6 @@ public: do_decode_message(message.response_name()); } - generator.append(R"~~~( - case (int)IPC::LargeMessageWrapper::MESSAGE_ID: - return TRY(IPC::LargeMessageWrapper::decode(message_endpoint_magic, stream, files)); -)~~~"); - generator.append(R"~~~( default:)~~~"); if constexpr (GENERATE_DEBUG) { @@ -903,7 +898,6 @@ void build(StringBuilder& builder, Vector const& endpoints) #include #include #include -#include #if defined(AK_COMPILER_CLANG) #pragma clang diagnostic push diff --git a/Services/RequestServer/ConnectionFromClient.cpp b/Services/RequestServer/ConnectionFromClient.cpp index 10392eb8d25..914aba69510 100644 --- a/Services/RequestServer/ConnectionFromClient.cpp +++ b/Services/RequestServer/ConnectionFromClient.cpp @@ -22,6 +22,10 @@ #include #include #include +#ifdef AK_OS_WINDOWS +// needed because curl.h includes winsock2.h +# include +#endif #include namespace RequestServer { @@ -318,7 +322,7 @@ Messages::RequestServer::InitTransportResponse ConnectionFromClient::init_transp Messages::RequestServer::ConnectNewClientResponse ConnectionFromClient::connect_new_client() { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC int socket_fds[2] {}; if (auto err = Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds); err.is_error()) { @@ -372,6 +376,12 @@ void ConnectionFromClient::set_dns_server(ByteString host_or_address, u16 port, default_resolver()->dns.reset_connection(); } +#ifdef AK_OS_WINDOWS +void ConnectionFromClient::start_request(i32, ByteString, URL::URL, HTTP::HeaderMap, ByteBuffer, Core::ProxyData) +{ + VERIFY(0 && "RequestServer::ConnectionFromClient::start_request is not implemented"); +} +#else void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL::URL url, HTTP::HeaderMap request_headers, ByteBuffer request_body, Core::ProxyData proxy_data) { auto host = url.serialized_host().to_byte_string(); @@ -496,6 +506,7 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString method, URL: m_active_requests.set(request_id, move(request)); }); } +#endif static Requests::NetworkError map_curl_code_to_network_error(CURLcode const& code) { diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index 57b57b7ce55..86c36aad0bb 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -834,8 +834,8 @@ static void gather_style_sheets(Vector& results, } if (valid) { - if (auto location = sheet.location(); location.has_value()) - identifier.url = location.release_value(); + if (auto sheet_url = sheet.href(); sheet_url.has_value()) + identifier.url = sheet_url.release_value(); identifier.rule_count = sheet.rules().length(); results.append(move(identifier)); @@ -848,7 +848,7 @@ static void gather_style_sheets(Vector& results, // We can gather this anyway, and hope it loads later results.append({ .type = Web::CSS::StyleSheetIdentifier::Type::ImportRule, - .url = import_rule->url().to_string(), + .url = import_rule->href(), }); } } diff --git a/Services/WebContent/WebDriverConnection.cpp b/Services/WebContent/WebDriverConnection.cpp index 16f81ccacec..d6a90062ed8 100644 --- a/Services/WebContent/WebDriverConnection.cpp +++ b/Services/WebContent/WebDriverConnection.cpp @@ -188,7 +188,7 @@ static bool fire_an_event(FlyString const& name, Optional ta ErrorOr> WebDriverConnection::connect(Web::PageClient& page_client, ByteString const& webdriver_ipc_path) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC and Windows IPC dbgln_if(WEBDRIVER_DEBUG, "Trying to connect to {}", webdriver_ipc_path); auto socket = TRY(Core::LocalSocket::connect(webdriver_ipc_path)); diff --git a/Services/WebContent/main.cpp b/Services/WebContent/main.cpp index 0ae19e324b9..cabe2e7d512 100644 --- a/Services/WebContent/main.cpp +++ b/Services/WebContent/main.cpp @@ -210,7 +210,7 @@ ErrorOr serenity_main(Main::Arguments arguments) if (maybe_content_filter_error.is_error()) dbgln("Failed to load content filters: {}", maybe_content_filter_error.error()); - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto webcontent_socket = TRY(Core::take_over_socket_from_system_server("WebContent"sv)); auto webcontent_client = TRY(WebContent::ConnectionFromClient::try_create(make(move(webcontent_socket)))); @@ -250,7 +250,7 @@ static ErrorOr load_content_filters(StringView config_path) ErrorOr initialize_resource_loader(GC::Heap& heap, int request_server_socket) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto socket = TRY(Core::LocalSocket::adopt_fd(request_server_socket)); TRY(socket->set_blocking(true)); @@ -267,7 +267,7 @@ ErrorOr initialize_resource_loader(GC::Heap& heap, int request_server_sock ErrorOr initialize_image_decoder(int image_decoder_socket) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto socket = TRY(Core::LocalSocket::adopt_fd(image_decoder_socket)); TRY(socket->set_blocking(true)); @@ -284,7 +284,7 @@ ErrorOr initialize_image_decoder(int image_decoder_socket) ErrorOr reinitialize_image_decoder(IPC::File const& image_decoder_socket) { - static_assert(IsSame, "Need to handle other IPC transports here"); + // TODO: Mach IPC auto socket = TRY(Core::LocalSocket::adopt_fd(image_decoder_socket.take_fd())); TRY(socket->set_blocking(true)); diff --git a/Tests/LibUnicode/TestSegmenter.cpp b/Tests/LibUnicode/TestSegmenter.cpp index d40ea82cebf..13368ab8451 100644 --- a/Tests/LibUnicode/TestSegmenter.cpp +++ b/Tests/LibUnicode/TestSegmenter.cpp @@ -136,11 +136,23 @@ TEST_CASE(out_of_bounds) auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); segmenter->set_segmented_text(text); - auto result = segmenter->previous_boundary(text.byte_count()); + auto result = segmenter->previous_boundary(text.byte_count() + 1); + EXPECT(result.has_value()); + + result = segmenter->next_boundary(text.byte_count() + 1); EXPECT(!result.has_value()); + result = segmenter->previous_boundary(text.byte_count()); + EXPECT(result.has_value()); + result = segmenter->next_boundary(text.byte_count()); EXPECT(!result.has_value()); + + result = segmenter->next_boundary(0); + EXPECT(result.has_value()); + + result = segmenter->previous_boundary(0); + EXPECT(!result.has_value()); } { auto text = MUST(AK::utf8_to_utf16("foo"sv)); @@ -148,10 +160,22 @@ TEST_CASE(out_of_bounds) auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Word); segmenter->set_segmented_text(Utf16View { text }); - auto result = segmenter->previous_boundary(text.size()); + auto result = segmenter->previous_boundary(text.size() + 1); + EXPECT(result.has_value()); + + result = segmenter->next_boundary(text.size() + 1); EXPECT(!result.has_value()); + result = segmenter->previous_boundary(text.size()); + EXPECT(result.has_value()); + result = segmenter->next_boundary(text.size()); EXPECT(!result.has_value()); + + result = segmenter->next_boundary(0); + EXPECT(result.has_value()); + + result = segmenter->previous_boundary(0); + EXPECT(!result.has_value()); } } diff --git a/Tests/LibWasm/test-wasm.cpp b/Tests/LibWasm/test-wasm.cpp index 037b56c3b63..39171d6d005 100644 --- a/Tests/LibWasm/test-wasm.cpp +++ b/Tests/LibWasm/test-wasm.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include diff --git a/Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt b/Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt new file mode 100644 index 00000000000..a7d359c32eb --- /dev/null +++ b/Tests/LibWeb/Layout/expected/block-and-inline/float-with-negative-margins.txt @@ -0,0 +1,13 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x48 [BFC] children: not-inline + BlockContainer at (8,8) content-size 784x0 children: inline + BlockContainer at (8,-2) content-size 50x50 floating [BFC] children: not-inline + TextNode <#text> + BlockContainer at (58,-2) content-size 50x50 floating [BFC] children: not-inline + TextNode <#text> + +ViewportPaintable (Viewport<#document>) [0,0 800x600] + PaintableWithLines (BlockContainer) [0,0 800x48] + PaintableWithLines (BlockContainer) [8,8 784x0] + PaintableWithLines (BlockContainer
.r) [8,-2 50x50] + PaintableWithLines (BlockContainer
.g) [58,-2 50x50] diff --git a/Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html b/Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html new file mode 100644 index 00000000000..53b5002a35a --- /dev/null +++ b/Tests/LibWeb/Layout/input/block-and-inline/float-with-negative-margins.html @@ -0,0 +1,13 @@ + + +
+
diff --git a/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt b/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt index 0ebf505f534..f3b99ef8537 100644 --- a/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt +++ b/Tests/LibWeb/Text/expected/Messaging/Messaging-post-channel-over-channel.txt @@ -1,6 +1,6 @@ Port1: "Hello" Port1: {"foo":{}} -Port2: "Hello" -Port3: "Hello from the transferred port" Port1: "DONE" +Port2: "Hello" Port2: "DONE" +Port3: "Hello from the transferred port" diff --git a/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt b/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt index 52162806d0d..d9ef6f2cb1e 100644 --- a/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt +++ b/Tests/LibWeb/Text/expected/css/CSSImportRule-supportsText.txt @@ -1,4 +1,4 @@ -cssText: @import url("https://something.invalid/") supports(foo: bar); +cssText: @import url("https://something.invalid") supports(foo: bar); supportsText: foo: bar -cssText: @import url("https://something.invalid/") supports((display: block) and (foo: bar)); +cssText: @import url("https://something.invalid") supports((display: block) and (foo: bar)); supportsText: (display: block) and (foo: bar) diff --git a/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt b/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt index 2c8da675484..8ce0f65441d 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/compression/compression-bad-chunks.tentative.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 21 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt new file mode 100644 index 00000000000..535648fbe77 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/all-prop-revert-layer.txt @@ -0,0 +1,203 @@ +Harness status: OK + +Found 197 tests + +187 Pass +10 Fail +Pass accent-color +Pass border-collapse +Pass border-spacing +Pass caption-side +Pass caret-color +Pass clip-rule +Pass color +Pass color-scheme +Pass cursor +Pass direction +Pass fill +Pass fill-opacity +Pass fill-rule +Pass font-family +Pass font-feature-settings +Pass font-language-override +Pass font-size +Pass font-style +Pass font-variant-alternates +Pass font-variant-caps +Pass font-variant-east-asian +Pass font-variant-emoji +Pass font-variant-ligatures +Pass font-variant-numeric +Pass font-variant-position +Pass font-variation-settings +Pass font-weight +Pass font-width +Pass image-rendering +Pass letter-spacing +Pass line-height +Pass list-style-position +Pass list-style-type +Pass math-depth +Pass math-shift +Pass math-style +Pass pointer-events +Pass quotes +Pass stroke +Pass stroke-dasharray +Pass stroke-dashoffset +Pass stroke-linecap +Pass stroke-linejoin +Pass stroke-miterlimit +Pass stroke-opacity +Pass stroke-width +Pass tab-size +Pass text-align +Pass text-anchor +Pass text-decoration-line +Pass text-indent +Pass text-justify +Pass text-shadow +Pass text-transform +Pass visibility +Pass white-space +Pass word-break +Pass word-spacing +Pass word-wrap +Pass writing-mode +Pass align-items +Pass align-self +Pass animation-delay +Pass animation-direction +Pass animation-duration +Pass animation-fill-mode +Pass animation-iteration-count +Fail animation-name +Pass animation-play-state +Pass animation-timing-function +Pass appearance +Pass aspect-ratio +Pass backdrop-filter +Pass background-attachment +Pass background-blend-mode +Pass background-clip +Pass background-color +Pass background-origin +Pass background-repeat +Pass background-size +Fail block-size +Pass bottom +Pass box-shadow +Pass box-sizing +Pass clear +Pass clip +Pass clip-path +Pass column-count +Pass column-gap +Pass column-span +Pass column-width +Pass contain +Pass content +Pass content-visibility +Pass counter-increment +Pass counter-reset +Pass counter-set +Pass cx +Pass cy +Pass display +Pass flex-basis +Pass flex-direction +Pass flex-grow +Pass flex-shrink +Pass flex-wrap +Pass float +Pass grid-auto-columns +Pass grid-auto-flow +Pass grid-auto-rows +Pass grid-column-end +Pass grid-column-start +Pass grid-row-end +Pass grid-row-start +Pass grid-template-areas +Pass grid-template-columns +Pass grid-template-rows +Fail height +Fail inline-size +Pass inset-block-end +Pass inset-block-start +Pass inset-inline-end +Pass inset-inline-start +Pass isolation +Pass justify-content +Pass justify-items +Pass justify-self +Pass left +Pass margin-block-end +Pass margin-block-start +Pass margin-bottom +Pass margin-inline-end +Pass margin-inline-start +Pass margin-left +Pass margin-right +Pass margin-top +Pass mask-image +Pass mask-type +Fail max-block-size +Pass max-height +Fail max-inline-size +Pass max-width +Fail min-block-size +Pass min-height +Fail min-inline-size +Pass min-width +Pass mix-blend-mode +Pass object-fit +Pass object-position +Pass opacity +Pass order +Pass outline-color +Pass outline-offset +Pass outline-style +Pass outline-width +Pass overflow-x +Pass overflow-y +Pass padding-block-end +Pass padding-block-start +Pass padding-bottom +Pass padding-inline-end +Pass padding-inline-start +Pass padding-left +Pass padding-right +Pass padding-top +Pass position +Pass r +Pass right +Pass rotate +Pass row-gap +Pass rx +Pass ry +Pass scale +Pass scrollbar-gutter +Pass scrollbar-width +Pass stop-color +Pass stop-opacity +Pass table-layout +Pass text-decoration-color +Pass text-decoration-style +Pass text-decoration-thickness +Pass text-overflow +Pass top +Fail transform +Pass transform-box +Pass transition-delay +Pass transition-duration +Pass transition-property +Pass transition-timing-function +Pass translate +Pass unicode-bidi +Pass user-select +Pass vertical-align +Pass view-transition-name +Fail width +Pass x +Pass y +Pass z-index \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt new file mode 100644 index 00000000000..b58337dfe22 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-cascade/parsing/supports-import-parsing.txt @@ -0,0 +1,28 @@ +Harness status: OK + +Found 22 tests + +9 Pass +13 Fail +Pass @import url("nonexist.css") supports(); should be an invalid import rule due to an invalid supports() declaration +Pass @import url("nonexist.css") supports(foo: bar); should be an invalid import rule due to an invalid supports() declaration +Fail @import url("nonexist.css") supports(display:block); should be a valid supports() import rule +Fail @import url("nonexist.css") supports((display:flex)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(not (display: flex)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports((display: flex) and (display: block)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports((display: flex) or (display: block)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports((display: flex) or (foo: bar)); should be a valid supports() import rule +Pass @import url("nonexist.css") supports(display: block !important); should be a valid supports() import rule +Pass @import url("nonexist.css") layer supports(); should be an invalid import rule due to an invalid supports() declaration +Pass @import url("nonexist.css") layer supports(foo: bar); should be an invalid import rule due to an invalid supports() declaration +Fail @import url("nonexist.css") layer(A) supports((display: flex) or (foo: bar)); should be a valid supports() import rule +Fail @import url("nonexist.css") layer(A.B) supports((display: flex) and (foo: bar)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(selector(a)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(selector(p a)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(selector(p > a)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(selector(p + a)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(font-tech(color-colrv1)); should be a valid supports() import rule +Fail @import url("nonexist.css") supports(font-format(opentype)); should be a valid supports() import rule +Fail @import url(nonexist.css) supports(display:block); should be a valid supports() import rule +Fail @import "nonexist.css" supports(display:block); should be a valid supports() import rule +Pass @import url("nonexist.css") supports; should still be a valid import rule with an invalid supports() declaration \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt new file mode 100644 index 00000000000..ea806d930f2 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.txt @@ -0,0 +1,40 @@ +Harness status: OK + +Found 34 tests + +26 Pass +8 Fail +Pass @media is CSSGroupingRule +Pass @media rule type +Pass Empty @media rule length +Fail insertRule of @import into @media +Fail insertRule into empty @media at bad index +Fail insertRule into @media updates length +Pass insertRule of valid @media into @media +Pass insertRule of valid style rule into @media +Fail insertRule of invalid @media into @media +Pass insertRule of empty string into @media +Pass insertRule of valid @media rule followed by garbage into @media +Pass insertRule of valid style rule followed by garbage into @media +Pass insertRule of mutiple valid @media into @media +Pass insertRule of valid style rulle followed by valid @media into @media +Pass insertRule of valid style rule followed by valid @media into @media +Pass insertRule of two valid style rules into @media +Pass Return value of insertRule into @media +Pass @supports is CSSGroupingRule +Pass @supports rule type +Pass Empty @supports rule length +Fail insertRule of @import into @supports +Fail insertRule into empty @supports at bad index +Fail insertRule into @supports updates length +Pass insertRule of valid @media into @supports +Pass insertRule of valid style rule into @supports +Fail insertRule of invalid @media into @supports +Pass insertRule of empty string into @supports +Pass insertRule of valid @media rule followed by garbage into @supports +Pass insertRule of valid style rule followed by garbage into @supports +Pass insertRule of mutiple valid @media into @supports +Pass insertRule of valid style rulle followed by valid @media into @supports +Pass insertRule of valid style rule followed by valid @media into @supports +Pass insertRule of two valid style rules into @supports +Pass Return value of insertRule into @supports \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt new file mode 100644 index 00000000000..e597bba2f51 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.txt @@ -0,0 +1,252 @@ +Harness status: OK + +Found 246 tests + +184 Pass +62 Fail +Fail CSS Transitions: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions: property from [0.5] to [2] at (0) should be [0.5 / 1] +Fail CSS Transitions: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [0.5] to [2] at (1) should be [2 / 1] +Fail CSS Transitions: property from [0.5] to [2] at (1.5) should be [4 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (0) should be [0.5 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [0.5] to [2] at (1) should be [2 / 1] +Fail CSS Transitions with transition: all: property from [0.5] to [2] at (1.5) should be [4 / 1] +Pass CSS Animations: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Pass CSS Animations: property from [0.5] to [2] at (0) should be [0.5 / 1] +Pass CSS Animations: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [0.5] to [2] at (1) should be [2 / 1] +Pass CSS Animations: property from [0.5] to [2] at (1.5) should be [4 / 1] +Pass Web Animations: property from [0.5] to [2] at (-0.5) should be [0.25 / 1] +Pass Web Animations: property from [0.5] to [2] at (0) should be [0.5 / 1] +Pass Web Animations: property from [0.5] to [2] at (0.5) should be [1 / 1] +Pass Web Animations: property from [0.5] to [2] at (1) should be [2 / 1] +Pass Web Animations: property from [0.5] to [2] at (1.5) should be [4 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions with transition: all: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Animations: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (-0.5) should be [0.25 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (0) should be [0.5 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (0.5) should be [1 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (1) should be [2 / 1] +Pass Web Animations: property from [1 / 2] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions: property from [] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Transitions with transition: all: property from [] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (-0.5) should be [0.25 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (0) should be [0.5 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [] to [2 / 1] at (1) should be [2 / 1] +Fail CSS Animations: property from [] to [2 / 1] at (1.5) should be [4 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Fail CSS Transitions: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Fail CSS Transitions with transition: all: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Pass CSS Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (-0.5) should be [auto 0.25 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0) should be [auto 0.5 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (0.5) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1) should be [auto 2 / 1] +Pass Web Animations: property from [auto 1 / 2] to [auto 2 / 1] at (1.5) should be [auto 4 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Pass CSS Animations: property from [auto] to [2 / 1] at (0) should be [auto] +Pass CSS Animations: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass CSS Animations: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Animations: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (-0.3) should be [auto] +Pass Web Animations: property from [auto] to [2 / 1] at (0) should be [auto] +Pass Web Animations: property from [auto] to [2 / 1] at (0.3) should be [auto] +Pass Web Animations: property from [auto] to [2 / 1] at (0.5) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (0.6) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (1) should be [2 / 1] +Pass Web Animations: property from [auto] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Transitions with transition: all: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass CSS Animations: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (-0.3) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0.3) should be [auto 1 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0.5) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (0.6) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (1) should be [2 / 1] +Pass Web Animations: property from [auto 1 / 1] to [2 / 1] at (1.5) should be [2 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Transitions with transition: all: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass CSS Animations: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (-0.3) should be [1 / 0] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0) should be [1 / 0] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0.3) should be [1 / 0] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0.5) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (0.6) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (1) should be [1 / 1] +Pass Web Animations: property from [1 / 0] to [1 / 1] at (1.5) should be [1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (-0.3) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0.3) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (-0.3) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0.3) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Transitions with transition: all: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass CSS Animations: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (-0.3) should be [1 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0) should be [1 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0.3) should be [1 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0.5) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (0.6) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (1) should be [0 / 1] +Pass Web Animations: property from [1 / 1] to [0 / 1] at (1.5) should be [0 / 1] +Pass Compositing: property underlying [2 / 1] from replace [0.5 / 1] to add [1 / 1] at (0) should be [0.5 / 1] +Fail Compositing: property underlying [2 / 1] from replace [0.5 / 1] to add [1 / 1] at (0.5) should be [1 / 1] +Fail Compositing: property underlying [2 / 1] from replace [0.5 / 1] to add [1 / 1] at (1) should be [2 / 1] \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt new file mode 100644 index 00000000000..c434b34d0ce --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transitions/animations/z-index-interpolation.txt @@ -0,0 +1,256 @@ +Harness status: OK + +Found 250 tests + +170 Pass +80 Fail +Fail CSS Transitions: property from neutral to [5] at (-0.3) should be [-4] +Fail CSS Transitions: property from neutral to [5] at (0) should be [-2] +Fail CSS Transitions: property from neutral to [5] at (0.3) should be [0] +Fail CSS Transitions: property from neutral to [5] at (0.6) should be [2] +Pass CSS Transitions: property from neutral to [5] at (1) should be [5] +Fail CSS Transitions: property from neutral to [5] at (1.5) should be [9] +Fail CSS Transitions with transition: all: property from neutral to [5] at (-0.3) should be [-4] +Fail CSS Transitions with transition: all: property from neutral to [5] at (0) should be [-2] +Fail CSS Transitions with transition: all: property from neutral to [5] at (0.3) should be [0] +Fail CSS Transitions with transition: all: property from neutral to [5] at (0.6) should be [2] +Pass CSS Transitions with transition: all: property from neutral to [5] at (1) should be [5] +Fail CSS Transitions with transition: all: property from neutral to [5] at (1.5) should be [9] +Fail CSS Animations: property from neutral to [5] at (-0.3) should be [-4] +Fail CSS Animations: property from neutral to [5] at (0) should be [-2] +Fail CSS Animations: property from neutral to [5] at (0.3) should be [0] +Fail CSS Animations: property from neutral to [5] at (0.6) should be [2] +Pass CSS Animations: property from neutral to [5] at (1) should be [5] +Fail CSS Animations: property from neutral to [5] at (1.5) should be [9] +Fail Web Animations: property from neutral to [5] at (-0.3) should be [-4] +Fail Web Animations: property from neutral to [5] at (0) should be [-2] +Fail Web Animations: property from neutral to [5] at (0.3) should be [0] +Fail Web Animations: property from neutral to [5] at (0.6) should be [2] +Pass Web Animations: property from neutral to [5] at (1) should be [5] +Fail Web Animations: property from neutral to [5] at (1.5) should be [9] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (-0.3) should be [initial] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0) should be [initial] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0.3) should be [initial] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [initial] to [5] at (1.5) should be [5] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (-0.3) should be [initial] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0) should be [initial] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0.3) should be [initial] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [initial] to [5] at (1.5) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (-0.3) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0.3) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions: property from [initial] to [5] at (1.5) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (-0.3) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0.3) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (1) should be [5] +Pass CSS Transitions with transition: all: property from [initial] to [5] at (1.5) should be [5] +Pass CSS Animations: property from [initial] to [5] at (-0.3) should be [initial] +Pass CSS Animations: property from [initial] to [5] at (0) should be [initial] +Pass CSS Animations: property from [initial] to [5] at (0.3) should be [initial] +Pass CSS Animations: property from [initial] to [5] at (0.5) should be [5] +Pass CSS Animations: property from [initial] to [5] at (0.6) should be [5] +Pass CSS Animations: property from [initial] to [5] at (1) should be [5] +Pass CSS Animations: property from [initial] to [5] at (1.5) should be [5] +Pass Web Animations: property from [initial] to [5] at (-0.3) should be [initial] +Pass Web Animations: property from [initial] to [5] at (0) should be [initial] +Pass Web Animations: property from [initial] to [5] at (0.3) should be [initial] +Pass Web Animations: property from [initial] to [5] at (0.5) should be [5] +Pass Web Animations: property from [initial] to [5] at (0.6) should be [5] +Pass Web Animations: property from [initial] to [5] at (1) should be [5] +Pass Web Animations: property from [initial] to [5] at (1.5) should be [5] +Fail CSS Transitions: property from [inherit] to [5] at (-0.3) should be [18] +Fail CSS Transitions: property from [inherit] to [5] at (0) should be [15] +Fail CSS Transitions: property from [inherit] to [5] at (0.3) should be [12] +Fail CSS Transitions: property from [inherit] to [5] at (0.6) should be [9] +Pass CSS Transitions: property from [inherit] to [5] at (1) should be [5] +Fail CSS Transitions: property from [inherit] to [5] at (1.5) should be [0] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (-0.3) should be [18] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (0) should be [15] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (0.3) should be [12] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (0.6) should be [9] +Pass CSS Transitions with transition: all: property from [inherit] to [5] at (1) should be [5] +Fail CSS Transitions with transition: all: property from [inherit] to [5] at (1.5) should be [0] +Pass CSS Animations: property from [inherit] to [5] at (-0.3) should be [18] +Pass CSS Animations: property from [inherit] to [5] at (0) should be [15] +Pass CSS Animations: property from [inherit] to [5] at (0.3) should be [12] +Pass CSS Animations: property from [inherit] to [5] at (0.6) should be [9] +Pass CSS Animations: property from [inherit] to [5] at (1) should be [5] +Pass CSS Animations: property from [inherit] to [5] at (1.5) should be [0] +Pass Web Animations: property from [inherit] to [5] at (-0.3) should be [18] +Pass Web Animations: property from [inherit] to [5] at (0) should be [15] +Pass Web Animations: property from [inherit] to [5] at (0.3) should be [12] +Pass Web Animations: property from [inherit] to [5] at (0.6) should be [9] +Pass Web Animations: property from [inherit] to [5] at (1) should be [5] +Pass Web Animations: property from [inherit] to [5] at (1.5) should be [0] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (-0.3) should be [unset] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0) should be [unset] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0.3) should be [unset] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [unset] to [5] at (1.5) should be [5] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (-0.3) should be [unset] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0) should be [unset] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0.3) should be [unset] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [unset] to [5] at (1.5) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (-0.3) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0.3) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions: property from [unset] to [5] at (1.5) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (-0.3) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0.3) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (1) should be [5] +Pass CSS Transitions with transition: all: property from [unset] to [5] at (1.5) should be [5] +Pass CSS Animations: property from [unset] to [5] at (-0.3) should be [unset] +Pass CSS Animations: property from [unset] to [5] at (0) should be [unset] +Pass CSS Animations: property from [unset] to [5] at (0.3) should be [unset] +Pass CSS Animations: property from [unset] to [5] at (0.5) should be [5] +Pass CSS Animations: property from [unset] to [5] at (0.6) should be [5] +Pass CSS Animations: property from [unset] to [5] at (1) should be [5] +Pass CSS Animations: property from [unset] to [5] at (1.5) should be [5] +Pass Web Animations: property from [unset] to [5] at (-0.3) should be [unset] +Pass Web Animations: property from [unset] to [5] at (0) should be [unset] +Pass Web Animations: property from [unset] to [5] at (0.3) should be [unset] +Pass Web Animations: property from [unset] to [5] at (0.5) should be [5] +Pass Web Animations: property from [unset] to [5] at (0.6) should be [5] +Pass Web Animations: property from [unset] to [5] at (1) should be [5] +Pass Web Animations: property from [unset] to [5] at (1.5) should be [5] +Fail CSS Transitions: property from [-5] to [5] at (-0.3) should be [-8] +Fail CSS Transitions: property from [-5] to [5] at (0) should be [-5] +Fail CSS Transitions: property from [-5] to [5] at (0.3) should be [-2] +Fail CSS Transitions: property from [-5] to [5] at (0.6) should be [1] +Pass CSS Transitions: property from [-5] to [5] at (1) should be [5] +Fail CSS Transitions: property from [-5] to [5] at (1.5) should be [10] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (-0.3) should be [-8] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (0) should be [-5] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (0.3) should be [-2] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (0.6) should be [1] +Pass CSS Transitions with transition: all: property from [-5] to [5] at (1) should be [5] +Fail CSS Transitions with transition: all: property from [-5] to [5] at (1.5) should be [10] +Pass CSS Animations: property from [-5] to [5] at (-0.3) should be [-8] +Pass CSS Animations: property from [-5] to [5] at (0) should be [-5] +Pass CSS Animations: property from [-5] to [5] at (0.3) should be [-2] +Pass CSS Animations: property from [-5] to [5] at (0.6) should be [1] +Pass CSS Animations: property from [-5] to [5] at (1) should be [5] +Pass CSS Animations: property from [-5] to [5] at (1.5) should be [10] +Pass Web Animations: property from [-5] to [5] at (-0.3) should be [-8] +Pass Web Animations: property from [-5] to [5] at (0) should be [-5] +Pass Web Animations: property from [-5] to [5] at (0.3) should be [-2] +Pass Web Animations: property from [-5] to [5] at (0.6) should be [1] +Pass Web Animations: property from [-5] to [5] at (1) should be [5] +Pass Web Animations: property from [-5] to [5] at (1.5) should be [10] +Fail CSS Transitions: property from [2] to [4] at (-0.3) should be [1] +Fail CSS Transitions: property from [2] to [4] at (0) should be [2] +Fail CSS Transitions: property from [2] to [4] at (0.3) should be [3] +Fail CSS Transitions: property from [2] to [4] at (0.6) should be [3] +Pass CSS Transitions: property from [2] to [4] at (1) should be [4] +Fail CSS Transitions: property from [2] to [4] at (1.5) should be [5] +Fail CSS Transitions with transition: all: property from [2] to [4] at (-0.3) should be [1] +Fail CSS Transitions with transition: all: property from [2] to [4] at (0) should be [2] +Fail CSS Transitions with transition: all: property from [2] to [4] at (0.3) should be [3] +Fail CSS Transitions with transition: all: property from [2] to [4] at (0.6) should be [3] +Pass CSS Transitions with transition: all: property from [2] to [4] at (1) should be [4] +Fail CSS Transitions with transition: all: property from [2] to [4] at (1.5) should be [5] +Pass CSS Animations: property from [2] to [4] at (-0.3) should be [1] +Pass CSS Animations: property from [2] to [4] at (0) should be [2] +Pass CSS Animations: property from [2] to [4] at (0.3) should be [3] +Pass CSS Animations: property from [2] to [4] at (0.6) should be [3] +Pass CSS Animations: property from [2] to [4] at (1) should be [4] +Pass CSS Animations: property from [2] to [4] at (1.5) should be [5] +Pass Web Animations: property from [2] to [4] at (-0.3) should be [1] +Pass Web Animations: property from [2] to [4] at (0) should be [2] +Pass Web Animations: property from [2] to [4] at (0.3) should be [3] +Pass Web Animations: property from [2] to [4] at (0.6) should be [3] +Pass Web Animations: property from [2] to [4] at (1) should be [4] +Pass Web Animations: property from [2] to [4] at (1.5) should be [5] +Fail CSS Transitions: property from [-2] to [-4] at (-0.3) should be [-1] +Fail CSS Transitions: property from [-2] to [-4] at (0) should be [-2] +Fail CSS Transitions: property from [-2] to [-4] at (0.1) should be [-2] +Fail CSS Transitions: property from [-2] to [-4] at (0.3) should be [-3] +Fail CSS Transitions: property from [-2] to [-4] at (0.6) should be [-3] +Pass CSS Transitions: property from [-2] to [-4] at (1) should be [-4] +Fail CSS Transitions: property from [-2] to [-4] at (1.5) should be [-5] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (-0.3) should be [-1] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0) should be [-2] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0.1) should be [-2] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0.3) should be [-3] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (0.6) should be [-3] +Pass CSS Transitions with transition: all: property from [-2] to [-4] at (1) should be [-4] +Fail CSS Transitions with transition: all: property from [-2] to [-4] at (1.5) should be [-5] +Pass CSS Animations: property from [-2] to [-4] at (-0.3) should be [-1] +Pass CSS Animations: property from [-2] to [-4] at (0) should be [-2] +Pass CSS Animations: property from [-2] to [-4] at (0.1) should be [-2] +Pass CSS Animations: property from [-2] to [-4] at (0.3) should be [-3] +Pass CSS Animations: property from [-2] to [-4] at (0.6) should be [-3] +Pass CSS Animations: property from [-2] to [-4] at (1) should be [-4] +Pass CSS Animations: property from [-2] to [-4] at (1.5) should be [-5] +Pass Web Animations: property from [-2] to [-4] at (-0.3) should be [-1] +Pass Web Animations: property from [-2] to [-4] at (0) should be [-2] +Pass Web Animations: property from [-2] to [-4] at (0.1) should be [-2] +Pass Web Animations: property from [-2] to [-4] at (0.3) should be [-3] +Pass Web Animations: property from [-2] to [-4] at (0.6) should be [-3] +Pass Web Animations: property from [-2] to [-4] at (1) should be [-4] +Pass Web Animations: property from [-2] to [-4] at (1.5) should be [-5] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (-0.3) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0) should be [auto] +Fail CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0.3) should be [auto] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions with transition-behavior:allow-discrete: property from [auto] to [10] at (1.5) should be [10] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (-0.3) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0) should be [auto] +Fail CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0.3) should be [auto] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions with transition-property:all and transition-behavor:allow-discrete: property from [auto] to [10] at (1.5) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (-0.3) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0.3) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions: property from [auto] to [10] at (1.5) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (-0.3) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0.3) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (1) should be [10] +Pass CSS Transitions with transition: all: property from [auto] to [10] at (1.5) should be [10] +Pass CSS Animations: property from [auto] to [10] at (-0.3) should be [auto] +Pass CSS Animations: property from [auto] to [10] at (0) should be [auto] +Pass CSS Animations: property from [auto] to [10] at (0.3) should be [auto] +Pass CSS Animations: property from [auto] to [10] at (0.5) should be [10] +Pass CSS Animations: property from [auto] to [10] at (0.6) should be [10] +Pass CSS Animations: property from [auto] to [10] at (1) should be [10] +Pass CSS Animations: property from [auto] to [10] at (1.5) should be [10] +Pass Web Animations: property from [auto] to [10] at (-0.3) should be [auto] +Pass Web Animations: property from [auto] to [10] at (0) should be [auto] +Pass Web Animations: property from [auto] to [10] at (0.3) should be [auto] +Pass Web Animations: property from [auto] to [10] at (0.5) should be [10] +Pass Web Animations: property from [auto] to [10] at (0.6) should be [10] +Pass Web Animations: property from [auto] to [10] at (1) should be [10] +Pass Web Animations: property from [auto] to [10] at (1.5) should be [10] \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt b/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt new file mode 100644 index 00000000000..7bc6e91ba48 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/cssom/cssimportrule.txt @@ -0,0 +1,17 @@ +Harness status: OK + +Found 11 tests + +8 Pass +3 Fail +Pass CSSRule and CSSImportRule types +Pass Type of CSSRule#type and constant values +Pass Existence and writability of CSSRule attributes +Pass Values of CSSRule attributes +Pass Existence and writability of CSSImportRule attributes +Fail Values of CSSImportRule attributes +Fail CSSImportRule : MediaList mediaText attribute should be updated due to [PutForwards] +Fail CSSStyleDeclaration cssText attribute should be updated due to [PutForwards] +Pass StyleSheet : MediaList mediaText attribute should be updated due to [PutForwards] +Pass Existence and writability of CSSImportRule supportsText attribute +Pass Value of CSSImportRule supportsText attribute \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt index db36b3a6af6..ea1b1e6c0d0 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/encoding/streams/encode-bad-chunks.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 6 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt new file mode 100644 index 00000000000..9ceba4b0d72 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/abort.any.txt @@ -0,0 +1,38 @@ +Harness status: OK + +Found 33 tests + +33 Pass +Pass a signal argument 'null' should cause pipeTo() to reject +Pass a signal argument 'AbortSignal' should cause pipeTo() to reject +Pass a signal argument 'true' should cause pipeTo() to reject +Pass a signal argument '-1' should cause pipeTo() to reject +Pass a signal argument '[object AbortSignal]' should cause pipeTo() to reject +Pass an aborted signal should cause the writable stream to reject with an AbortError +Pass (reason: 'null') all the error objects should be the same object +Pass (reason: 'undefined') all the error objects should be the same object +Pass (reason: 'error1: error1') all the error objects should be the same object +Pass preventCancel should prevent canceling the readable +Pass preventAbort should prevent aborting the readable +Pass preventCancel and preventAbort should prevent canceling the readable and aborting the readable +Pass (reason: 'null') abort should prevent further reads +Pass (reason: 'undefined') abort should prevent further reads +Pass (reason: 'error1: error1') abort should prevent further reads +Pass (reason: 'null') all pending writes should complete on abort +Pass (reason: 'undefined') all pending writes should complete on abort +Pass (reason: 'error1: error1') all pending writes should complete on abort +Pass (reason: 'null') underlyingSource.cancel() should called when abort, even with pending pull +Pass (reason: 'undefined') underlyingSource.cancel() should called when abort, even with pending pull +Pass (reason: 'error1: error1') underlyingSource.cancel() should called when abort, even with pending pull +Pass a rejection from underlyingSource.cancel() should be returned by pipeTo() +Pass a rejection from underlyingSink.abort() should be returned by pipeTo() +Pass a rejection from underlyingSink.abort() should be preferred to one from underlyingSource.cancel() +Pass abort signal takes priority over closed readable +Pass abort signal takes priority over errored readable +Pass abort signal takes priority over closed writable +Pass abort signal takes priority over errored writable +Pass abort should do nothing after the readable is closed +Pass abort should do nothing after the readable is errored +Pass abort should do nothing after the readable is errored, even with pending writes +Pass abort should do nothing after the writable is errored +Pass pipeTo on a teed readable byte stream should only be aborted when both branches are aborted \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt new file mode 100644 index 00000000000..94f417830b1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-backward.any.txt @@ -0,0 +1,21 @@ +Harness status: OK + +Found 16 tests + +16 Pass +Pass Closing must be propagated backward: starts closed; preventCancel omitted; fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel omitted; rejected cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = undefined (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = null (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = false (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = 0 (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = -0 (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = NaN (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = (falsy); fulfilled cancel promise +Pass Closing must be propagated backward: starts closed; preventCancel = true (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = a (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = 1 (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = Symbol() (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = [object Object] (truthy) +Pass Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true +Pass Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true, preventClose = true \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt new file mode 100644 index 00000000000..8911034e974 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/close-propagation-forward.any.txt @@ -0,0 +1,35 @@ +Harness status: OK + +Found 30 tests + +30 Pass +Pass Closing must be propagated forward: starts closed; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: starts closed; preventClose = undefined (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = null (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = false (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = 0 (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = -0 (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = NaN (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = (falsy); fulfilled close promise +Pass Closing must be propagated forward: starts closed; preventClose = true (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = a (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = 1 (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = Symbol() (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = [object Object] (truthy) +Pass Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true +Pass Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true, preventCancel = true +Pass Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: becomes closed asynchronously; preventClose = true +Pass Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; preventClose = true +Pass Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; fulfilled close promise +Pass Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; rejected close promise +Pass Closing must be propagated forward: becomes closed after one chunk; preventClose = true +Pass Closing must be propagated forward: shutdown must not occur until the final write completes +Pass Closing must be propagated forward: shutdown must not occur until the final write completes; preventClose = true +Pass Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write +Pass Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write; preventClose = true +Pass Closing must be propagated forward: erroring the writable while flushing pending writes should error pipeTo \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt new file mode 100644 index 00000000000..47969a0fed1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-backward.any.txt @@ -0,0 +1,40 @@ +Harness status: OK + +Found 35 tests + +35 Pass +Pass Errors must be propagated backward: starts errored; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = undefined (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = null (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = false (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = 0 (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = -0 (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = NaN (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = (falsy); fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = a (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = 1 (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = Symbol() (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = [object Object] (truthy) +Pass Errors must be propagated backward: becomes errored before piping due to write, preventCancel = true; preventAbort = true +Pass Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true, preventAbort = true, preventClose = true +Pass Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write; preventCancel = true +Pass Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = false; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = false; rejected cancel promise +Pass Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = true +Pass Errors must be propagated backward: becomes errored after piping; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored after piping; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored after piping; preventCancel = true +Pass Errors must be propagated backward: becomes errored after piping due to last write; source is closed; preventCancel omitted (but cancel is never called) +Pass Errors must be propagated backward: becomes errored after piping due to last write; source is closed; preventCancel = true +Pass Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = false; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = false; rejected cancel promise +Pass Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = true +Pass Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; fulfilled cancel promise +Pass Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; rejected cancel promise +Pass Errors must be propagated backward: becomes errored before piping via abort; preventCancel = true +Pass Errors must be propagated backward: erroring via the controller errors once pending write completes \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt new file mode 100644 index 00000000000..1406d4298aa --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/error-propagation-forward.any.txt @@ -0,0 +1,37 @@ +Harness status: OK + +Found 32 tests + +32 Pass +Pass Errors must be propagated forward: starts errored; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = undefined (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = null (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = false (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = 0 (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = -0 (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = NaN (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = (falsy); fulfilled abort promise +Pass Errors must be propagated forward: starts errored; preventAbort = true (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = a (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = 1 (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = Symbol() (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = [object Object] (truthy) +Pass Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true +Pass Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true, preventClose = true +Pass Errors must be propagated forward: becomes errored while empty; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored while empty; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored while empty; preventAbort = true +Pass Errors must be propagated forward: becomes errored while empty; dest never desires chunks; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored while empty; dest never desires chunks; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored while empty; dest never desires chunks; preventAbort = true +Pass Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; preventAbort = true +Pass Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; preventAbort = false; fulfilled abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; preventAbort = false; rejected abort promise +Pass Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; preventAbort = true +Pass Errors must be propagated forward: shutdown must not occur until the final write completes +Pass Errors must be propagated forward: shutdown must not occur until the final write completes; preventAbort = true +Pass Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write +Pass Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write; preventAbort = true \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt new file mode 100644 index 00000000000..eaa579cda26 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/flow-control.any.txt @@ -0,0 +1,10 @@ +Harness status: OK + +Found 5 tests + +5 Pass +Pass Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks +Pass Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks, but then does +Pass Piping from an empty ReadableStream into a WritableStream that does not desire chunks, but then the readable stream becomes non-empty and the writable stream starts desiring chunks +Pass Piping from a ReadableStream to a WritableStream that desires more chunks before finishing with previous ones +Pass Piping to a WritableStream that does not consume the writes fast enough exerts backpressure on the ReadableStream \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt new file mode 100644 index 00000000000..8baf203c193 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/general.any.txt @@ -0,0 +1,19 @@ +Harness status: OK + +Found 14 tests + +14 Pass +Pass Piping must lock both the ReadableStream and WritableStream +Pass Piping finishing must unlock both the ReadableStream and WritableStream +Pass pipeTo must check the brand of its ReadableStream this value +Pass pipeTo must check the brand of its WritableStream argument +Pass pipeTo must fail if the ReadableStream is locked, and not lock the WritableStream +Pass pipeTo must fail if the WritableStream is locked, and not lock the ReadableStream +Pass Piping from a ReadableStream from which lots of chunks are synchronously readable +Pass Piping from a ReadableStream for which a chunk becomes asynchronously readable after the pipeTo +Pass an undefined rejection from pull should cause pipeTo() to reject when preventAbort is true +Pass an undefined rejection from pull should cause pipeTo() to reject when preventAbort is false +Pass an undefined rejection from write should cause pipeTo() to reject when preventCancel is true +Pass an undefined rejection from write should cause pipeTo() to reject when preventCancel is false +Pass pipeTo() should reject if an option getter grabs a writer +Pass pipeTo() promise should resolve if null is passed \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt new file mode 100644 index 00000000000..212f4762aa8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/piping/multiple-propagation.any.txt @@ -0,0 +1,14 @@ +Harness status: OK + +Found 9 tests + +9 Pass +Pass Piping from an errored readable stream to an erroring writable stream +Pass Piping from an errored readable stream to an errored writable stream +Pass Piping from an errored readable stream to an erroring writable stream; preventAbort = true +Pass Piping from an errored readable stream to an errored writable stream; preventAbort = true +Pass Piping from an errored readable stream to a closing writable stream +Pass Piping from an errored readable stream to a closed writable stream +Pass Piping from a closed readable stream to an erroring writable stream +Pass Piping from a closed readable stream to an errored writable stream +Pass Piping from a closed readable stream to a closed writable stream \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt index ea4f794f5cf..d0d6f306af7 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/readable-byte-streams/tee.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 39 tests diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt index 3d42dd06767..db9690d7e18 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/writable-streams/aborting.any.txt @@ -1,4 +1,4 @@ -Harness status: Error +Harness status: OK Found 62 tests diff --git a/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html b/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html index 7ef3b100abd..d386d0e9aba 100644 --- a/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html +++ b/Tests/LibWeb/Text/input/Messaging/Messaging-post-channel-over-channel.html @@ -4,8 +4,12 @@ asyncTest(done => { let channel = new MessageChannel(); + const port1Logs = []; + const port2Logs = []; + const port3Logs = []; + channel.port1.onmessage = (event) => { - println("Port1: " + JSON.stringify(event.data)); + port1Logs.push("Port1: " + JSON.stringify(event.data)); if (event.ports.length > 0) { event.ports[0].postMessage("Hello from the transferred port"); return; @@ -14,8 +18,14 @@ }; channel.port2.onmessage = (event) => { - println("Port2: " + JSON.stringify(event.data)); + port2Logs.push("Port2: " + JSON.stringify(event.data)); if (event.data === "DONE") { + for (let log of port1Logs) + println(log); + for (let log of port2Logs) + println(log); + for (let log of port3Logs) + println(log); done(); } }; @@ -23,7 +33,7 @@ let channel2 = new MessageChannel(); channel2.port2.onmessage = (event) => { - println("Port3: " + JSON.stringify(event.data)); + port3Logs.push("Port3: " + JSON.stringify(event.data)); channel.port2.postMessage("DONE"); } diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html b/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html new file mode 100644 index 00000000000..5d1d08dfb8c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/all-prop-revert-layer.html @@ -0,0 +1,470 @@ + + +CSS Cascade: "all: revert-layer" + + + + + +
+ + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/parsing/supports-import-parsing.html b/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/parsing/supports-import-parsing.html new file mode 100644 index 00000000000..831c864024c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-cascade/parsing/supports-import-parsing.html @@ -0,0 +1,92 @@ + + +@import rule with supports parsing / serialization + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.html b/Tests/LibWeb/Text/input/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.html new file mode 100644 index 00000000000..d0bd5c3a616 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-conditional/js/conditional-CSSGroupingRule.html @@ -0,0 +1,244 @@ + + + CSSGroupingRule Conditional Rules Test + + + + + + + + + +
+
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html b/Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html new file mode 100644 index 00000000000..980767f7195 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-sizing/animation/aspect-ratio-interpolation.html @@ -0,0 +1,130 @@ + + +aspect-ratio interpolation + + + + + + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html new file mode 100644 index 00000000000..c7614536950 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transitions/animations/z-index-interpolation.html @@ -0,0 +1,130 @@ + + +z-index interpolation + + + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/cssom/cssimportrule.html b/Tests/LibWeb/Text/input/wpt-import/css/cssom/cssimportrule.html new file mode 100644 index 00000000000..1a87e7be7c3 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/cssom/cssimportrule.html @@ -0,0 +1,124 @@ + + + + CSSOM CSSRule CSSImportRule interface + + + + + + + + + + + + +
+ + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/cssom/support/a-green.css b/Tests/LibWeb/Text/input/wpt-import/css/cssom/support/a-green.css new file mode 100644 index 00000000000..b0dbb071d5b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/cssom/support/a-green.css @@ -0,0 +1 @@ +.a { color: green; } diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html new file mode 100644 index 00000000000..f98677456d1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js new file mode 100644 index 00000000000..e813b017769 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/abort.any.js @@ -0,0 +1,448 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/recording-streams.js +// META: script=../resources/test-utils.js +'use strict'; + +// Tests for the use of pipeTo with AbortSignal. +// There is some extra complexity to avoid timeouts in environments where abort is not implemented. + +const error1 = new Error('error1'); +error1.name = 'error1'; +const error2 = new Error('error2'); +error2.name = 'error2'; + +const errorOnPull = { + pull(controller) { + // This will cause the test to error if pipeTo abort is not implemented. + controller.error('failed to abort'); + } +}; + +// To stop pull() being called immediately when the stream is created, we need to set highWaterMark to 0. +const hwm0 = { highWaterMark: 0 }; + +for (const invalidSignal of [null, 'AbortSignal', true, -1, Object.create(AbortSignal.prototype)]) { + promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { signal: invalidSignal }), 'pipeTo should reject') + .then(() => { + assert_equals(rs.events.length, 0, 'no ReadableStream methods should have been called'); + assert_equals(ws.events.length, 0, 'no WritableStream methods should have been called'); + }); + }, `a signal argument '${invalidSignal}' should cause pipeTo() to reject`); +} + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject') + .then(() => Promise.all([ + rs.getReader().closed, + promise_rejects_dom(t, 'AbortError', ws.getWriter().closed, 'writer.closed should reject') + ])) + .then(() => { + assert_equals(rs.events.length, 2, 'cancel should have been called'); + assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); + assert_equals(rs.events[1].name, 'AbortError', 'the argument to cancel should be an AbortError'); + assert_equals(rs.events[1].constructor.name, 'DOMException', + 'the argument to cancel should be a DOMException'); + }); +}, 'an aborted signal should cause the writable stream to reject with an AbortError'); + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(reason); + const pipeToPromise = rs.pipeTo(ws, { signal }); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + await rs.getReader().closed; + await promise_rejects_exactly(t, error, ws.getWriter().closed, 'the writable should be errored with the same object'); + assert_equals(signal.reason, error, 'signal.reason should be error'), + assert_equals(rs.events.length, 2, 'cancel should have been called'); + assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); + assert_equals(rs.events[1], error, 'the readable should be canceled with the same object'); + }, `(reason: '${reason}') all the error objects should be the same object`); +} + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true }), 'pipeTo should reject') + .then(() => assert_equals(rs.events.length, 0, 'cancel should not be called')); +}, 'preventCancel should prevent canceling the readable'); + +promise_test(t => { + const rs = new ReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventAbort: true }), 'pipeTo should reject') + .then(() => { + assert_equals(ws.events.length, 0, 'writable should not have been aborted'); + return ws.getWriter().ready; + }); +}, 'preventAbort should prevent aborting the readable'); + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true, preventAbort: true }), + 'pipeTo should reject') + .then(() => { + assert_equals(rs.events.length, 0, 'cancel should not be called'); + assert_equals(ws.events.length, 0, 'writable should not have been aborted'); + return ws.getWriter().ready; + }); +}, 'preventCancel and preventAbort should prevent canceling the readable and aborting the readable'); + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + const rs = new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const ws = recordingWritableStream({ + write() { + abortController.abort(reason); + } + }); + const pipeToPromise = rs.pipeTo(ws, { signal }); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + assert_equals(signal.reason, error, 'signal.reason should be error'); + assert_equals(ws.events.length, 4, 'only chunk "a" should have been written'); + assert_array_equals(ws.events.slice(0, 3), ['write', 'a', 'abort'], 'events should match'); + assert_equals(ws.events[3], error, 'abort reason should be error'); + }, `(reason: '${reason}') abort should prevent further reads`); +} + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + c.enqueue('a'); + c.enqueue('b'); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + let resolveWrite; + const writePromise = new Promise(resolve => { + resolveWrite = resolve; + }); + const ws = recordingWritableStream({ + write() { + return writePromise; + } + }, new CountQueuingStrategy({ highWaterMark: Infinity })); + const pipeToPromise = rs.pipeTo(ws, { signal }); + await delay(0); + await abortController.abort(reason); + await readController.close(); // Make sure the test terminates when signal is not implemented. + await resolveWrite(); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + assert_equals(signal.reason, error, 'signal.reason should be error'); + assert_equals(ws.events.length, 6, 'chunks "a" and "b" should have been written'); + assert_array_equals(ws.events.slice(0, 5), ['write', 'a', 'write', 'b', 'abort'], 'events should match'); + assert_equals(ws.events[5], error, 'abort reason should be error'); + }, `(reason: '${reason}') all pending writes should complete on abort`); +} + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + let rejectPull; + const pullPromise = new Promise((_, reject) => { + rejectPull = reject; + }); + let rejectCancel; + const cancelPromise = new Promise((_, reject) => { + rejectCancel = reject; + }); + const rs = recordingReadableStream({ + async pull() { + await Promise.race([ + pullPromise, + cancelPromise, + ]); + }, + cancel(reason) { + rejectCancel(reason); + }, + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal }); + pipeToPromise.catch(() => {}); // Prevent unhandled rejection. + await delay(0); + abortController.abort(reason); + rejectPull('should not catch pull rejection'); + await delay(0); + assert_equals(rs.eventsWithoutPulls.length, 2, 'cancel should have been called'); + assert_equals(rs.eventsWithoutPulls[0], 'cancel', 'first event should be cancel'); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + }, `(reason: '${reason}') underlyingSource.cancel() should called when abort, even with pending pull`); +} + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + }, + cancel() { + return Promise.reject(error1); + } + }, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'a rejection from underlyingSource.cancel() should be returned by pipeTo()'); + +promise_test(t => { + const rs = new ReadableStream(errorOnPull, hwm0); + const ws = new WritableStream({ + abort() { + return Promise.reject(error1); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'a rejection from underlyingSink.abort() should be returned by pipeTo()'); + +promise_test(t => { + const events = []; + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + }, + cancel() { + events.push('cancel'); + return Promise.reject(error1); + } + }, hwm0); + const ws = new WritableStream({ + abort() { + events.push('abort'); + return Promise.reject(error2); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error2, rs.pipeTo(ws, { signal }), 'pipeTo should reject') + .then(() => assert_array_equals(events, ['abort', 'cancel'], 'abort() should be called before cancel()')); +}, 'a rejection from underlyingSink.abort() should be preferred to one from underlyingSource.cancel()'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over closed readable'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.error(error1); + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over errored readable'); + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + } + }, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + const writer = ws.getWriter(); + return writer.close().then(() => { + writer.releaseLock(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); + }); +}, 'abort signal takes priority over closed writable'); + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + } + }, hwm0); + const ws = new WritableStream({ + start(controller) { + controller.error(error1); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over errored writable'); + +promise_test(() => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventClose: true }); + readController.close(); + return Promise.resolve().then(() => { + abortController.abort(); + return pipeToPromise; + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is closed'); + +promise_test(t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); + readController.error(error1); + return Promise.resolve().then(() => { + abortController.abort(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is errored'); + +promise_test(t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + let resolveWrite; + const writePromise = new Promise(resolve => { + resolveWrite = resolve; + }); + const ws = new WritableStream({ + write() { + readController.error(error1); + return writePromise; + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); + readController.enqueue('a'); + return delay(0).then(() => { + abortController.abort(); + resolveWrite(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is errored, even with pending writes'); + +promise_test(t => { + const rs = recordingReadableStream({ + pull(controller) { + return delay(0).then(() => controller.close()); + } + }); + let writeController; + const ws = new WritableStream({ + start(c) { + writeController = c; + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventCancel: true }); + return Promise.resolve().then(() => { + writeController.error(error1); + return Promise.resolve(); + }).then(() => { + abortController.abort(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => { + assert_array_equals(rs.events, ['pull'], 'cancel should not have been called'); + }); +}, 'abort should do nothing after the writable is errored'); + +promise_test(async t => { + const rs = new ReadableStream({ + pull(c) { + c.enqueue(new Uint8Array([])); + }, + type: "bytes", + }); + const ws = new WritableStream(); + const [first, second] = rs.tee(); + + let aborted = false; + first.pipeTo(ws, { signal: AbortSignal.abort() }).catch(() => { + aborted = true; + }); + await delay(0); + assert_true(!aborted, "pipeTo should not resolve yet"); + await second.cancel(); + await delay(0); + assert_true(aborted, "pipeTo should be aborted now"); +}, "pipeTo on a teed readable byte stream should only be aborted when both branches are aborted"); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html new file mode 100644 index 00000000000..38e4080afff --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js new file mode 100644 index 00000000000..25bd475ed13 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-backward.any.js @@ -0,0 +1,153 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err.name, 'TypeError', 'the promise must reject with a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + } + ); + +}, 'Closing must be propagated backward: starts closed; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + // Our recording streams do not deal well with errors generated by the system, so give them some help + let recordedError; + const rs = recordingReadableStream({ + cancel(cancelErr) { + recordedError = cancelErr; + throw error1; + } + }); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_equals(recordedError.name, 'TypeError', 'the cancel reason must be a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', recordedError]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel omitted; rejected cancel promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws, { preventCancel: falsy }).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err.name, 'TypeError', 'the promise must reject with a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + } + ); + + }, `Closing must be propagated backward: starts closed; preventCancel = ${stringVersion} (falsy); fulfilled cancel ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { preventCancel: truthy })).then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + + }, `Closing must be propagated backward: starts closed; preventCancel = ${String(truthy)} (truthy)`); +} + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { preventCancel: true, preventAbort: true })) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, + rs.pipeTo(ws, { preventCancel: true, preventAbort: true, preventClose: true })) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true, preventClose ' + + '= true'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html new file mode 100644 index 00000000000..eea048b3cac --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js new file mode 100644 index 00000000000..0ec94f80abf --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/close-propagation-forward.any.js @@ -0,0 +1,589 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: starts closed; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: starts closed; preventClose omitted; rejected close promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: falsy }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + + }, `Closing must be propagated forward: starts closed; preventClose = ${stringVersion} (falsy); fulfilled close ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: truthy }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + + }, `Closing must be propagated forward: starts closed; preventClose = ${String(truthy)} (truthy)`); +} + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: true, preventAbort: true }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true'); + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: true, preventAbort: true, preventCancel: true }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true, preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose = true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.close(); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'close' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'close']); + }); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws, { preventClose: true }).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.close(); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the chunk must have been written, but close must not have happened'); + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the chunk must have been written, but close must not have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; preventClose = true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but close must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.close(); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but close must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close'], + 'all chunks must have been written and close must have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws, { preventClose: true }).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but close must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.close(); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but close must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'all chunks must have been written, but close must not have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write; preventClose = true'); + + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + let rejectWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise((resolve, reject) => { + rejectWritePromise = reject; + }); + } + }, { highWaterMark: 3 }); + const pipeToPromise = rs.pipeTo(ws); + return delay(0).then(() => { + rejectWritePromise(error1); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['write', 'a']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed, 'ws should be errored') + ]); + }); +}, 'Closing must be propagated forward: erroring the writable while flushing pending writes should error pipeTo'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html new file mode 100644 index 00000000000..aa92999ebf1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js new file mode 100644 index 00000000000..f786469d6c1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-backward.any.js @@ -0,0 +1,630 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + start() { + return Promise.reject(error1); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: starts errored; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; ' + + 'fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; rejected ' + + 'cancel promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: falsy }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + + }, `Errors must be propagated backward: becomes errored before piping due to write; preventCancel = ` + + `${stringVersion} (falsy); fulfilled cancel promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: truthy }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + + }, `Errors must be propagated backward: becomes errored before piping due to write; preventCancel = ` + + `${String(truthy)} (truthy)`); +} + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true, preventAbort: true }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write, preventCancel = true; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true, preventAbort: true, preventClose: true }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true, ' + + 'preventAbort = true, preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; fulfilled ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + }, + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; rejected ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = ' + + 'false; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + }, + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = ' + + 'false; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel omitted; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + controller.close(); + } + }); + + const ws = recordingWritableStream({ + write(chunk) { + if (chunk === 'c') { + return Promise.reject(error1); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c']); + }); + +}, 'Errors must be propagated backward: becomes errored after piping due to last write; source is closed; ' + + 'preventCancel omitted (but cancel is never called)'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + controller.close(); + } + }); + + const ws = recordingWritableStream({ + write(chunk) { + if (chunk === 'c') { + return Promise.reject(error1); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c']); + }); + +}, 'Errors must be propagated backward: becomes errored after piping due to last write; source is closed; ' + + 'preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'false; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'false; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return rs.pipeTo(ws).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err, error1, 'the promise must reject with error1'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['abort', error1]); + } + ); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; fulfilled ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error') + .then(() => { + return ws.getWriter().closed.then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err, error1, 'the promise must reject with error1'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['abort', error1]); + } + ); + }); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; rejected ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true })).then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + return flushAsyncEvents(); + } + }); + + const pipePromise = rs.pipeTo(ws); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + ws.controller.error(error1); + + return promise_rejects_exactly(t, error1, pipePromise); + }).then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a']); + }); + +}, 'Errors must be propagated backward: erroring via the controller errors once pending write completes'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html new file mode 100644 index 00000000000..dcf0443d4c9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js new file mode 100644 index 00000000000..e9260f9ea22 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/error-propagation-forward.any.js @@ -0,0 +1,569 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = false; rejected abort promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: falsy }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + + }, `Errors must be propagated forward: starts errored; preventAbort = ${stringVersion} (falsy); fulfilled abort ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: truthy }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + + }, `Errors must be propagated forward: starts errored; preventAbort = ${String(truthy)} (truthy)`); +} + + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true, preventCancel: true }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true, preventCancel: true, preventClose: true }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true, preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws)).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + rs.controller.error(error1); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'abort', error1]); + }); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true })).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + rs.controller.error(error1); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws)).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but abort must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.error(error1); + resolveWritePromise(); + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but abort must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'abort', error1], + 'all chunks must have been written and abort must have happened'); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true })).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but abort must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.error(error1); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but abort must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'all chunks must have been written, but abort must not have happened'); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write; preventAbort = true'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html new file mode 100644 index 00000000000..8afa4be6159 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.html @@ -0,0 +1,17 @@ + + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js new file mode 100644 index 00000000000..e2318da375a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/flow-control.any.js @@ -0,0 +1,297 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + } + }); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws, { preventCancel: true }); + + // Wait and make sure it doesn't do any reading. + return flushAsyncEvents().then(() => { + ws.controller.error(error1); + }) + .then(() => promise_rejects_exactly(t, error1, pipePromise, 'pipeTo must reject with the same error')) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }) + .then(() => readableStreamToArray(rs)) + .then(chunksNotPreviouslyRead => { + assert_array_equals(chunksNotPreviouslyRead, ['a', 'b']); + }); + +}, 'Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks'); + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('b'); + controller.close(); + } + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }); + + const writer = ws.getWriter(); + const firstWritePromise = writer.write('a'); + assert_equals(writer.desiredSize, 0, 'after writing the writer\'s desiredSize must be 0'); + writer.releaseLock(); + + // firstWritePromise won't settle until we call resolveWritePromise. + + const pipePromise = rs.pipeTo(ws); + + return flushAsyncEvents().then(() => resolveWritePromise()) + .then(() => Promise.all([firstWritePromise, pipePromise])) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close']); + }); + +}, 'Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks, but then does'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }); + + const writer = ws.getWriter(); + writer.write('a'); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); + assert_equals(writer.desiredSize, 0, 'after writing the writer\'s desiredSize must be 0'); + writer.releaseLock(); + + const pipePromise = rs.pipeTo(ws); + + rs.controller.enqueue('b'); + resolveWritePromise(); + rs.controller.close(); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close']); + }); + }); + +}, 'Piping from an empty ReadableStream into a WritableStream that does not desire chunks, but then the readable ' + + 'stream becomes non-empty and the writable stream starts desiring chunks'); + +promise_test(() => { + const unreadChunks = ['b', 'c', 'd']; + + const rs = recordingReadableStream({ + pull(controller) { + controller.enqueue(unreadChunks.shift()); + if (unreadChunks.length === 0) { + controller.close(); + } + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }, new CountQueuingStrategy({ highWaterMark: 3 })); + + const writer = ws.getWriter(); + const firstWritePromise = writer.write('a'); + assert_equals(writer.desiredSize, 2, 'after writing the writer\'s desiredSize must be 2'); + writer.releaseLock(); + + // firstWritePromise won't settle until we call resolveWritePromise. + + const pipePromise = rs.pipeTo(ws); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); + assert_equals(unreadChunks.length, 1, 'chunks should continue to be enqueued until the HWM is reached'); + }).then(() => resolveWritePromise()) + .then(() => Promise.all([firstWritePromise, pipePromise])) + .then(() => { + assert_array_equals(rs.events, ['pull', 'pull', 'pull']); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b','write', 'c','write', 'd', 'close']); + }); + +}, 'Piping from a ReadableStream to a WritableStream that desires more chunks before finishing with previous ones'); + +class StepTracker { + constructor() { + this.waiters = []; + this.wakers = []; + } + + // Returns promise which resolves when step `n` is reached. Also schedules step n + 1 to happen shortly after the + // promise is resolved. + waitThenAdvance(n) { + if (this.waiters[n] === undefined) { + this.waiters[n] = new Promise(resolve => { + this.wakers[n] = resolve; + }); + this.waiters[n] + .then(() => flushAsyncEvents()) + .then(() => { + if (this.wakers[n + 1] !== undefined) { + this.wakers[n + 1](); + } + }); + } + if (n == 0) { + this.wakers[0](); + } + return this.waiters[n]; + } +} + +promise_test(() => { + const steps = new StepTracker(); + const desiredSizes = []; + const rs = recordingReadableStream({ + start(controller) { + steps.waitThenAdvance(1).then(() => enqueue('a')); + steps.waitThenAdvance(3).then(() => enqueue('b')); + steps.waitThenAdvance(5).then(() => enqueue('c')); + steps.waitThenAdvance(7).then(() => enqueue('d')); + steps.waitThenAdvance(11).then(() => controller.close()); + + function enqueue(chunk) { + controller.enqueue(chunk); + desiredSizes.push(controller.desiredSize); + } + } + }); + + const chunksFinishedWriting = []; + const writableStartPromise = Promise.resolve(); + let writeCalled = false; + const ws = recordingWritableStream({ + start() { + return writableStartPromise; + }, + write(chunk) { + const waitForStep = writeCalled ? 12 : 9; + writeCalled = true; + return steps.waitThenAdvance(waitForStep).then(() => { + chunksFinishedWriting.push(chunk); + }); + } + }); + + return writableStartPromise.then(() => { + const pipePromise = rs.pipeTo(ws); + steps.waitThenAdvance(0); + + return Promise.all([ + steps.waitThenAdvance(2).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 2, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 2, one chunk must have been written'); + + // When 'a' (the very first chunk) was enqueued, it was immediately used to fulfill the outstanding read request + // promise, leaving the queue empty. + assert_array_equals(desiredSizes, [1], + 'at step 2, the desiredSize at the last enqueue (step 1) must have been 1'); + assert_equals(rs.controller.desiredSize, 1, 'at step 2, the current desiredSize must be 1'); + }), + + steps.waitThenAdvance(4).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 4, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 4, one chunk must have been written'); + + // When 'b' was enqueued at step 3, the queue was also empty, since immediately after enqueuing 'a' at + // step 1, it was dequeued in order to fulfill the read() call that was made at step 0. Thus the queue + // had size 1 (thus desiredSize of 0). + assert_array_equals(desiredSizes, [1, 0], + 'at step 4, the desiredSize at the last enqueue (step 3) must have been 0'); + assert_equals(rs.controller.desiredSize, 0, 'at step 4, the current desiredSize must be 0'); + }), + + steps.waitThenAdvance(6).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 6, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 6, one chunk must have been written'); + + // When 'c' was enqueued at step 5, the queue was not empty; it had 'b' in it, since 'b' will not be read until + // the first write completes at step 9. Thus, the queue size is 2 after enqueuing 'c', giving a desiredSize of + // -1. + assert_array_equals(desiredSizes, [1, 0, -1], + 'at step 6, the desiredSize at the last enqueue (step 5) must have been -1'); + assert_equals(rs.controller.desiredSize, -1, 'at step 6, the current desiredSize must be -1'); + }), + + steps.waitThenAdvance(8).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 8, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 8, one chunk must have been written'); + + // When 'd' was enqueued at step 7, the situation is the same as before, leading to a queue containing 'b', 'c', + // and 'd'. + assert_array_equals(desiredSizes, [1, 0, -1, -2], + 'at step 8, the desiredSize at the last enqueue (step 7) must have been -2'); + assert_equals(rs.controller.desiredSize, -2, 'at step 8, the current desiredSize must be -2'); + }), + + steps.waitThenAdvance(10).then(() => { + assert_array_equals(chunksFinishedWriting, ['a'], 'at step 10, one chunk must have finished writing'); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'at step 10, two chunks must have been written'); + + assert_equals(rs.controller.desiredSize, -1, 'at step 10, the current desiredSize must be -1'); + }), + + pipePromise.then(() => { + assert_array_equals(desiredSizes, [1, 0, -1, -2], 'backpressure must have been exerted at the source'); + assert_array_equals(chunksFinishedWriting, ['a', 'b', 'c', 'd'], 'all chunks finished writing'); + + assert_array_equals(rs.eventsWithoutPulls, [], 'nothing unexpected should happen to the ReadableStream'); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c', 'write', 'd', 'close'], + 'all chunks were written (and the WritableStream closed)'); + }) + ]); + }); +}, 'Piping to a WritableStream that does not consume the writes fast enough exerts backpressure on the ReadableStream'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html new file mode 100644 index 00000000000..7bdc8bf6ad8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js new file mode 100644 index 00000000000..f051d8102c2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/general.any.js @@ -0,0 +1,212 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/recording-streams.js +'use strict'; + +test(() => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + assert_false(rs.locked, 'sanity check: the ReadableStream must not start locked'); + assert_false(ws.locked, 'sanity check: the WritableStream must not start locked'); + + rs.pipeTo(ws); + + assert_true(rs.locked, 'the ReadableStream must become locked'); + assert_true(ws.locked, 'the WritableStream must become locked'); + +}, 'Piping must lock both the ReadableStream and WritableStream'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + + return rs.pipeTo(ws).then(() => { + assert_false(rs.locked, 'the ReadableStream must become unlocked'); + assert_false(ws.locked, 'the WritableStream must become unlocked'); + }); + +}, 'Piping finishing must unlock both the ReadableStream and WritableStream'); + +promise_test(t => { + + const fakeRS = Object.create(ReadableStream.prototype); + const ws = new WritableStream(); + + return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(fakeRS, [ws]), + 'pipeTo should reject with a TypeError'); + +}, 'pipeTo must check the brand of its ReadableStream this value'); + +promise_test(t => { + + const rs = new ReadableStream(); + const fakeWS = Object.create(WritableStream.prototype); + + return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(rs, [fakeWS]), + 'pipeTo should reject with a TypeError'); + +}, 'pipeTo must check the brand of its WritableStream argument'); + +promise_test(t => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + rs.getReader(); + + assert_true(rs.locked, 'sanity check: the ReadableStream starts locked'); + assert_false(ws.locked, 'sanity check: the WritableStream does not start locked'); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws)).then(() => { + assert_false(ws.locked, 'the WritableStream must still be unlocked'); + }); + +}, 'pipeTo must fail if the ReadableStream is locked, and not lock the WritableStream'); + +promise_test(t => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + ws.getWriter(); + + assert_false(rs.locked, 'sanity check: the ReadableStream does not start locked'); + assert_true(ws.locked, 'sanity check: the WritableStream starts locked'); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws)).then(() => { + assert_false(rs.locked, 'the ReadableStream must still be unlocked'); + }); + +}, 'pipeTo must fail if the WritableStream is locked, and not lock the ReadableStream'); + +promise_test(() => { + + const CHUNKS = 10; + + const rs = new ReadableStream({ + start(c) { + for (let i = 0; i < CHUNKS; ++i) { + c.enqueue(i); + } + c.close(); + } + }); + + const written = []; + const ws = new WritableStream({ + write(chunk) { + written.push(chunk); + }, + close() { + written.push('closed'); + } + }, new CountQueuingStrategy({ highWaterMark: CHUNKS })); + + return rs.pipeTo(ws).then(() => { + const targetValues = []; + for (let i = 0; i < CHUNKS; ++i) { + targetValues.push(i); + } + targetValues.push('closed'); + + assert_array_equals(written, targetValues, 'the correct values must be written'); + + // Ensure both readable and writable are closed by the time the pipe finishes. + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + + // NOTE: no requirement on *when* the pipe finishes; that is left to implementations. + +}, 'Piping from a ReadableStream from which lots of chunks are synchronously readable'); + +promise_test(t => { + + let controller; + const rs = recordingReadableStream({ + start(c) { + controller = c; + } + }); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws).then(() => { + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + }); + + t.step_timeout(() => { + controller.enqueue('Hello'); + t.step_timeout(() => controller.close(), 10); + }, 10); + + return pipePromise; + +}, 'Piping from a ReadableStream for which a chunk becomes asynchronously readable after the pipeTo'); + +for (const preventAbort of [true, false]) { + promise_test(() => { + + const rs = new ReadableStream({ + pull() { + return Promise.reject(undefined); + } + }); + + return rs.pipeTo(new WritableStream(), { preventAbort }).then( + () => assert_unreached('pipeTo promise should be rejected'), + value => assert_equals(value, undefined, 'rejection value should be undefined')); + + }, `an undefined rejection from pull should cause pipeTo() to reject when preventAbort is ${preventAbort}`); +} + +for (const preventCancel of [true, false]) { + promise_test(() => { + + const rs = new ReadableStream({ + pull(controller) { + controller.enqueue(0); + } + }); + + const ws = new WritableStream({ + write() { + return Promise.reject(undefined); + } + }); + + return rs.pipeTo(ws, { preventCancel }).then( + () => assert_unreached('pipeTo promise should be rejected'), + value => assert_equals(value, undefined, 'rejection value should be undefined')); + + }, `an undefined rejection from write should cause pipeTo() to reject when preventCancel is ${preventCancel}`); +} + +promise_test(t => { + const rs = new ReadableStream(); + const ws = new WritableStream(); + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { + get preventAbort() { + ws.getWriter(); + } + }), 'pipeTo should reject'); +}, 'pipeTo() should reject if an option getter grabs a writer'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + + return rs.pipeTo(ws, null); +}, 'pipeTo() promise should resolve if null is passed'); diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html new file mode 100644 index 00000000000..b71f29922b3 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.html @@ -0,0 +1,16 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js new file mode 100644 index 00000000000..9be828a2326 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/piping/multiple-propagation.any.js @@ -0,0 +1,227 @@ +// META: global=window,worker,shadowrealm +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +function createErroredWritableStream(t) { + return Promise.resolve().then(() => { + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + const writer = ws.getWriter(); + return promise_rejects_exactly(t, error2, writer.closed, 'the writable stream must be errored with error2') + .then(() => { + writer.releaseLock(); + assert_array_equals(ws.events, []); + return ws; + }); + }); +} + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + // Trying to abort a stream that is erroring will give the writable's error + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error2, ws.getWriter().closed, 'the writable stream must be errored with error2') + ]); + }); + +}, 'Piping from an errored readable stream to an erroring writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'); + }); +}, 'Piping from an errored readable stream to an errored writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the readable stream\'s error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error2, ws.getWriter().closed, 'the writable stream must be errored with error2') + ]); + }); + +}, 'Piping from an errored readable stream to an erroring writable stream; preventAbort = true'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the readable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'); + }); + +}, 'Piping from an errored readable stream to an errored writable stream; preventAbort = true'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error1, ws.getWriter().closed, + 'closed must reject with error1'), + promise_rejects_exactly(t, error1, closePromise, + 'close() must reject with error1') + ]); + }); + +}, 'Piping from an errored readable stream to a closing writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + + return flushAsyncEvents().then(() => { + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + ws.getWriter().closed, + closePromise + ]); + }); + }); + +}, 'Piping from an errored readable stream to a closed writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error1); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed, 'the writable stream must be errored with error1') + ]); + }); + +}, 'Piping from a closed readable stream to an erroring writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return rs.getReader().closed; + }); + +}, 'Piping from a closed readable stream to an errored writable stream'); + +promise_test(() => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws).then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Piping from a closed readable stream to a closed writable stream');