From c75dda8f8bc58ab617cdcd7e4f4a1357962d3850 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Fri, 22 Nov 2024 13:05:10 +0100 Subject: [PATCH 001/397] LibWeb: Stop radio buttons firing change events when losing focus --- Libraries/LibWeb/HTML/HTMLInputElement.cpp | 1 + ...adio-button-focus-lost-no-change-event.txt | 1 + ...dio-button-focus-lost-no-change-event.html | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/radio-button-focus-lost-no-change-event.txt create mode 100644 Tests/LibWeb/Text/input/radio-button-focus-lost-no-change-event.html diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index 7ea12b8d7c4d..0b6ef85e02a4 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -638,6 +638,7 @@ void HTMLInputElement::commit_pending_changes() case TypeAttributeState::Text: case TypeAttributeState::URL: case TypeAttributeState::Checkbox: + case TypeAttributeState::RadioButton: if (!m_has_uncommitted_changes) return; break; diff --git a/Tests/LibWeb/Text/expected/radio-button-focus-lost-no-change-event.txt b/Tests/LibWeb/Text/expected/radio-button-focus-lost-no-change-event.txt new file mode 100644 index 000000000000..5a854a88dc7e --- /dev/null +++ b/Tests/LibWeb/Text/expected/radio-button-focus-lost-no-change-event.txt @@ -0,0 +1 @@ +PASS: Change event was not fired diff --git a/Tests/LibWeb/Text/input/radio-button-focus-lost-no-change-event.html b/Tests/LibWeb/Text/input/radio-button-focus-lost-no-change-event.html new file mode 100644 index 000000000000..5a38b7a1cf7c --- /dev/null +++ b/Tests/LibWeb/Text/input/radio-button-focus-lost-no-change-event.html @@ -0,0 +1,23 @@ + + + + From 3f5fff79ae008e2a15cc37671a411eae8e2682e4 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Fri, 22 Nov 2024 13:31:18 +0100 Subject: [PATCH 002/397] LibWeb: Update checkbox focus lost test --- .../expected/checkbox-focus-lost-no-change-event.txt | 1 + .../input/checkbox-focus-lost-no-change-event.html | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/LibWeb/Text/expected/checkbox-focus-lost-no-change-event.txt b/Tests/LibWeb/Text/expected/checkbox-focus-lost-no-change-event.txt index e69de29bb2d1..8d0cbeebd764 100644 --- a/Tests/LibWeb/Text/expected/checkbox-focus-lost-no-change-event.txt +++ b/Tests/LibWeb/Text/expected/checkbox-focus-lost-no-change-event.txt @@ -0,0 +1 @@ +PASS: Change event was not fired \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/checkbox-focus-lost-no-change-event.html b/Tests/LibWeb/Text/input/checkbox-focus-lost-no-change-event.html index 02b524062c69..ee308eed6370 100644 --- a/Tests/LibWeb/Text/input/checkbox-focus-lost-no-change-event.html +++ b/Tests/LibWeb/Text/input/checkbox-focus-lost-no-change-event.html @@ -2,8 +2,9 @@ \ No newline at end of file + From 1c733993e09148a0ae33d41e258417bee646abd2 Mon Sep 17 00:00:00 2001 From: Pavel Shliak Date: Tue, 19 Nov 2024 15:48:47 +0400 Subject: [PATCH 003/397] LibGfx: Do not provide BPP --- Libraries/LibGfx/Bitmap.h | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Libraries/LibGfx/Bitmap.h b/Libraries/LibGfx/Bitmap.h index 5bd905928744..63b55049f7d3 100644 --- a/Libraries/LibGfx/Bitmap.h +++ b/Libraries/LibGfx/Bitmap.h @@ -109,26 +109,8 @@ class Bitmap : public RefCounted { [[nodiscard]] size_t pitch() const { return m_pitch; } - [[nodiscard]] static unsigned bpp_for_format(BitmapFormat format) - { - switch (format) { - case BitmapFormat::BGRx8888: - case BitmapFormat::BGRA8888: - return 32; - default: - VERIFY_NOT_REACHED(); - case BitmapFormat::Invalid: - return 0; - } - } - [[nodiscard]] static size_t minimum_pitch(size_t width, BitmapFormat); - [[nodiscard]] unsigned bpp() const - { - return bpp_for_format(m_format); - } - [[nodiscard]] bool has_alpha_channel() const { return m_format == BitmapFormat::BGRA8888 || m_format == BitmapFormat::RGBA8888; } [[nodiscard]] BitmapFormat format() const { return m_format; } From b68d67693e29b70b53597d60d57a7a05a4eadc7a Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 11:22:32 -0500 Subject: [PATCH 004/397] LibJS: Implement the Temporal.PlainYearMonth constructor And the simple Temporal.PlainYearMonth.prototype getters, so that the constructed Temporal.PlainYearMonth may actually be validated. --- Libraries/LibJS/CMakeLists.txt | 3 + Libraries/LibJS/Forward.h | 7 +- Libraries/LibJS/Print.cpp | 12 ++ Libraries/LibJS/Runtime/Intrinsics.cpp | 2 + .../Runtime/Temporal/AbstractOperations.cpp | 3 + Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 48 ++++++ Libraries/LibJS/Runtime/Temporal/Calendar.h | 2 + .../LibJS/Runtime/Temporal/PlainYearMonth.cpp | 152 ++++++++++++++++++ .../LibJS/Runtime/Temporal/PlainYearMonth.h | 39 +++++ .../Temporal/PlainYearMonthConstructor.cpp | 116 +++++++++++++ .../Temporal/PlainYearMonthConstructor.h | 34 ++++ .../Temporal/PlainYearMonthPrototype.cpp | 126 +++++++++++++++ .../Temporal/PlainYearMonthPrototype.h | 38 +++++ Libraries/LibJS/Runtime/Temporal/Temporal.cpp | 2 + .../PlainYearMonth/PlainYearMonth.compare.js | 14 ++ .../PlainYearMonth/PlainYearMonth.from.js | 130 +++++++++++++++ .../Temporal/PlainYearMonth/PlainYearMonth.js | 60 +++++++ .../PlainYearMonth.prototype.calendarId.js | 15 ++ .../PlainYearMonth.prototype.daysInMonth.js | 14 ++ .../PlainYearMonth.prototype.daysInYear.js | 14 ++ .../PlainYearMonth.prototype.era.js | 14 ++ .../PlainYearMonth.prototype.eraYear.js | 14 ++ .../PlainYearMonth.prototype.inLeapYear.js | 14 ++ .../PlainYearMonth.prototype.month.js | 14 ++ .../PlainYearMonth.prototype.monthCode.js | 14 ++ .../PlainYearMonth.prototype.monthsInYear.js | 14 ++ .../PlainYearMonth.prototype.year.js | 14 ++ 27 files changed, 926 insertions(+), 3 deletions(-) create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.h create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp create mode 100644 Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.compare.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.from.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.calendarId.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInMonth.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInYear.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.era.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.eraYear.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.inLeapYear.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.month.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthCode.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthsInYear.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.year.js diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index c010b88da9ba..b5e21ade214a 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -217,6 +217,9 @@ set(SOURCES Runtime/Temporal/PlainMonthDay.cpp Runtime/Temporal/PlainMonthDayConstructor.cpp Runtime/Temporal/PlainMonthDayPrototype.cpp + Runtime/Temporal/PlainYearMonth.cpp + Runtime/Temporal/PlainYearMonthConstructor.cpp + Runtime/Temporal/PlainYearMonthPrototype.cpp Runtime/Temporal/PlainTime.cpp Runtime/Temporal/Temporal.cpp Runtime/Temporal/TimeZone.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index e73e9051da42..766ae9fd5219 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -87,9 +87,10 @@ __JS_ENUMERATE(RelativeTimeFormat, relative_time_format, RelativeTimeFormatPrototype, RelativeTimeFormatConstructor) \ __JS_ENUMERATE(Segmenter, segmenter, SegmenterPrototype, SegmenterConstructor) -#define JS_ENUMERATE_TEMPORAL_OBJECTS \ - __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) \ - __JS_ENUMERATE(PlainMonthDay, plain_month_day, PlainMonthDayPrototype, PlainMonthDayConstructor) +#define JS_ENUMERATE_TEMPORAL_OBJECTS \ + __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) \ + __JS_ENUMERATE(PlainMonthDay, plain_month_day, PlainMonthDayPrototype, PlainMonthDayConstructor) \ + __JS_ENUMERATE(PlainYearMonth, plain_year_month, PlainYearMonthPrototype, PlainYearMonthConstructor) #define JS_ENUMERATE_BUILTIN_NAMESPACE_OBJECTS \ __JS_ENUMERATE(AtomicsObject, atomics) \ diff --git a/Libraries/LibJS/Print.cpp b/Libraries/LibJS/Print.cpp index 6a8945260498..6ac16a8804bb 100644 --- a/Libraries/LibJS/Print.cpp +++ b/Libraries/LibJS/Print.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -845,6 +846,15 @@ ErrorOr print_temporal_plain_month_day(JS::PrintContext& print_context, JS return {}; } +ErrorOr print_temporal_plain_year_month(JS::PrintContext& print_context, JS::Temporal::PlainYearMonth const& plain_year_month, HashTable& seen_objects) +{ + TRY(print_type(print_context, "Temporal.PlainYearMonth"sv)); + TRY(js_out(print_context, " \033[34;1m{:04}-{:02}\033[0m", plain_year_month.iso_date().year, plain_year_month.iso_date().month)); + TRY(js_out(print_context, "\n calendar: ")); + TRY(print_value(print_context, JS::PrimitiveString::create(plain_year_month.vm(), plain_year_month.calendar()), seen_objects)); + return {}; +} + ErrorOr print_boolean_object(JS::PrintContext& print_context, JS::BooleanObject const& boolean_object, HashTable& seen_objects) { TRY(print_type(print_context, "Boolean"sv)); @@ -964,6 +974,8 @@ ErrorOr print_value(JS::PrintContext& print_context, JS::Value value, Hash return print_temporal_duration(print_context, static_cast(object), seen_objects); if (is(object)) return print_temporal_plain_month_day(print_context, static_cast(object), seen_objects); + if (is(object)) + return print_temporal_plain_year_month(print_context, static_cast(object), seen_objects); return print_object(print_context, object, seen_objects); } diff --git a/Libraries/LibJS/Runtime/Intrinsics.cpp b/Libraries/LibJS/Runtime/Intrinsics.cpp index f511939245cf..45f352355e3a 100644 --- a/Libraries/LibJS/Runtime/Intrinsics.cpp +++ b/Libraries/LibJS/Runtime/Intrinsics.cpp @@ -103,6 +103,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index f1ac2c6e7a6c..63434bf6454f 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include namespace JS::Temporal { @@ -467,6 +468,8 @@ ThrowCompletionOr is_partial_temporal_object(VM& vm, Value value) // FIXME: Add the other types as we define them. if (is(object)) return false; + if (is(object)) + return false; // 3. Let calendarProperty be ? Get(value, "calendar"). auto calendar_property = TRY(object.get(vm.names.calendar)); diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 529e588bc93d..7ff0dc068979 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -324,6 +325,8 @@ ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal // FIXME: Add the other calendar-holding types as we define them. if (is(temporal_calendar_object)) return static_cast(temporal_calendar_object).calendar(); + if (is(temporal_calendar_object)) + return static_cast(temporal_calendar_object).calendar(); } // 2. If temporalCalendarLike is not a String, throw a TypeError exception. @@ -346,6 +349,8 @@ ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM& // FIXME: Add the other calendar-holding types as we define them. if (is(item)) return static_cast(item).calendar(); + if (is(item)) + return static_cast(item).calendar(); // 2. Let calendarLike be ? Get(item, "calendar"). auto calendar_like = TRY(item.get(vm.names.calendar)); @@ -360,6 +365,30 @@ ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM& return TRY(to_temporal_calendar_identifier(vm, calendar_like)); } +// 12.2.11 CalendarYearMonthFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields +ThrowCompletionOr calendar_year_month_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow) +{ + // 1. Perform ? CalendarResolveFields(calendar, fields, YEAR-MONTH). + TRY(calendar_resolve_fields(vm, calendar, fields, DateType::YearMonth)); + + // FIXME: 2. Let firstDayIndex be the 1-based index of the first day of the month described by fields (i.e., 1 unless the + // month's first day is skipped by this calendar.) + static auto constexpr first_day_index = 1; + + // 3. Set fields.[[Day]] to firstDayIndex. + fields.day = first_day_index; + + // 4. Let result be ? CalendarDateToISO(calendar, fields, overflow). + auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow)); + + // 5. If ISOYearMonthWithinLimits(result) is false, throw a RangeError exception. + if (!iso_year_month_within_limits(result)) + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + + // 6. Return result. + return result; +} + // 12.2.12 CalendarMonthDayFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields ThrowCompletionOr calendar_month_day_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow) { @@ -530,6 +559,25 @@ u8 iso_day_of_week(ISODate const& iso_date) return day_of_week; } +// 12.2.19 CalendarDateToISO ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatetoiso +ThrowCompletionOr calendar_date_to_iso(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Assert: fields.[[Year]], fields.[[Month]], and fields.[[Day]] are not UNSET. + VERIFY(fields.year.has_value()); + VERIFY(fields.month.has_value()); + VERIFY(fields.day.has_value()); + + // b. Return ? RegulateISODate(fields.[[Year]], fields.[[Month]], fields.[[Day]], overflow). + return TRY(regulate_iso_date(vm, *fields.year, *fields.month, *fields.day, overflow)); + } + + // 2. Return an implementation-defined ISO Date Record, or throw a RangeError exception, as described below. + // FIXME: Create an ISODateRecord based on an ISO8601 calendar for now. See also: CalendarResolveFields. + return calendar_month_day_to_iso_reference_date(vm, "iso8601"sv, fields, overflow); +} + // 12.2.20 CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index b7a620ce3543..df2dce55feee 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -99,6 +99,7 @@ using CalendarFieldListOrPartial = Variant; ThrowCompletionOr canonicalize_calendar(VM&, StringView id); Vector const& available_calendars(); ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); +ThrowCompletionOr calendar_year_month_from_fields(VM&, StringView calendar, CalendarFields, Overflow); ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); String format_calendar_annotation(StringView id, ShowCalendar); bool calendar_equals(StringView one, StringView two); @@ -110,6 +111,7 @@ Vector calendar_field_keys_present(CalendarFields const&); CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields); ThrowCompletionOr to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); +ThrowCompletionOr calendar_date_to_iso(VM&, StringView calendar, CalendarFields const&, Overflow); ThrowCompletionOr calendar_month_day_to_iso_reference_date(VM&, StringView calendar, CalendarFields const&, Overflow); CalendarDate calendar_iso_to_date(StringView calendar, ISODate const&); Vector calendar_extra_fields(StringView calendar, CalendarFieldList); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp new file mode 100644 index 000000000000..2280d3a72128 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainYearMonth); + +// 9 Temporal.PlainYearMonth Objects, https://tc39.es/proposal-temporal/#sec-temporal-plainyearmonth-objects +PlainYearMonth::PlainYearMonth(ISODate iso_date, String calendar, Object& prototype) + : Object(ConstructWithPrototypeTag::Tag, prototype) + , m_iso_date(iso_date) + , m_calendar(move(calendar)) +{ +} + +// 9.5.2 ToTemporalYearMonth ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalyearmonth +ThrowCompletionOr> to_temporal_year_month(VM& vm, Value item, Value options) +{ + // 1. If options is not present, set options to undefined. + + // 2. If item is an Object, then + if (item.is_object()) { + auto const& object = item.as_object(); + + // a. If item has an [[InitializedTemporalYearMonth]] internal slot, then + if (is(object)) { + auto const& plain_year_month = static_cast(object); + + // i. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // ii. Perform ? GetTemporalOverflowOption(resolvedOptions). + TRY(get_temporal_overflow_option(vm, resolved_options)); + + // iii. Return ! CreateTemporalYearMonth(item.[[ISODate]], item.[[Calendar]]). + return MUST(create_temporal_year_month(vm, plain_year_month.iso_date(), plain_year_month.calendar())); + } + + // b. Let calendar be ? GetTemporalCalendarIdentifierWithISODefault(item). + auto calendar = TRY(get_temporal_calendar_identifier_with_iso_default(vm, object)); + + // c. Let fields be ? PrepareCalendarFields(calendar, item, « YEAR, MONTH, MONTH-CODE », «», «»). + auto fields = TRY(prepare_calendar_fields(vm, calendar, object, { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode } }, {}, CalendarFieldList {})); + + // d. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // e. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // f. Let isoDate be ? CalendarYearMonthFromFields(calendar, fields, overflow). + auto iso_date = TRY(calendar_year_month_from_fields(vm, calendar, move(fields), overflow)); + + // g. Return ! CreateTemporalYearMonth(isoDate, calendar). + return MUST(create_temporal_year_month(vm, iso_date, move(calendar))); + } + + // 3. If item is not a String, throw a TypeError exception. + if (!item.is_string()) + return vm.throw_completion(ErrorType::TemporalInvalidPlainYearMonth); + + // 4. Let result be ? ParseISODateTime(item, « TemporalYearMonthString »). + auto parse_result = TRY(parse_iso_date_time(vm, item.as_string().utf8_string_view(), { { Production::TemporalYearMonthString } })); + + // 5. Let calendar be result.[[Calendar]]. + // 6. If calendar is empty, set calendar to "iso8601". + auto calendar = parse_result.calendar.value_or("iso8601"_string); + + // 7. Set calendar to ? CanonicalizeCalendar(calendar). + calendar = TRY(canonicalize_calendar(vm, calendar)); + + // 8. Let isoDate be CreateISODateRecord(result.[[Year]], result.[[Month]], result.[[Day]]). + auto iso_date = create_iso_date_record(*parse_result.year, parse_result.month, parse_result.day); + + // 9. Set result to ISODateToFields(calendar, isoDate, YEAR-MONTH). + auto result = iso_date_to_fields(calendar, iso_date, DateType::YearMonth); + + // 10. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 11. Perform ? GetTemporalOverflowOption(resolvedOptions). + TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 12. NOTE: The following operation is called with CONSTRAIN regardless of the value of overflow, in order for the + // calendar to store a canonical value in the [[Day]] field of the [[ISODate]] internal slot of the result. + // 13. Set isoDate to ? CalendarYearMonthFromFields(calendar, result, CONSTRAIN). + iso_date = TRY(calendar_year_month_from_fields(vm, calendar, result, Overflow::Constrain)); + + // 14. Return ! CreateTemporalYearMonth(isoDate, calendar). + return MUST(create_temporal_year_month(vm, iso_date, move(calendar))); +} + +// 9.5.3 ISOYearMonthWithinLimits ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isoyearmonthwithinlimits +bool iso_year_month_within_limits(ISODate iso_date) +{ + // 1. If isoDate.[[Year]] < -271821 or isoDate.[[Year]] > 275760, then + if (iso_date.year < -271821 || iso_date.year > 275760) { + // a. Return false. + return false; + } + + // 2. If isoDate.[[Year]] = -271821 and isoDate.[[Month]] < 4, then + if (iso_date.year == -271821 && iso_date.month < 4) { + // a. Return false. + return false; + } + + // 3. If isoDate.[[Year]] = 275760 and isoDate.[[Month]] > 9, then + if (iso_date.year == 275760 && iso_date.month > 9) { + // a. Return false. + return false; + } + + // 4. Return true. + return true; +} + +// 9.5.5 CreateTemporalYearMonth ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalyearmonth +ThrowCompletionOr> create_temporal_year_month(VM& vm, ISODate iso_date, String calendar, GC::Ptr new_target) +{ + auto& realm = *vm.current_realm(); + + // 1. If ISOYearMonthWithinLimits(isoDate) is false, throw a RangeError exception. + if (!iso_year_month_within_limits(iso_date)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainYearMonth); + + // 2. If newTarget is not present, set newTarget to %Temporal.PlainYearMonth%. + if (!new_target) + new_target = realm.intrinsics().temporal_plain_year_month_constructor(); + + // 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainYearMonth.prototype%", « [[InitializedTemporalYearMonth]], [[ISODate]], [[Calendar]] »). + // 4. Set object.[[ISODate]] to isoDate. + // 5. Set object.[[Calendar]] to calendar. + auto object = TRY(ordinary_create_from_constructor(vm, *new_target, &Intrinsics::temporal_plain_year_month_prototype, iso_date, move(calendar))); + + // 6. Return object. + return object; +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h new file mode 100644 index 000000000000..36884ed74c92 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace JS::Temporal { + +class PlainYearMonth final : public Object { + JS_OBJECT(PlainYearMonth, Object); + GC_DECLARE_ALLOCATOR(PlainYearMonth); + +public: + virtual ~PlainYearMonth() override = default; + + [[nodiscard]] ISODate iso_date() const { return m_iso_date; } + [[nodiscard]] String const& calendar() const { return m_calendar; } + +private: + PlainYearMonth(ISODate, String calendar, Object& prototype); + + ISODate m_iso_date; // [[ISODate]] + String m_calendar; // [[Calendar]] +}; + +ThrowCompletionOr> to_temporal_year_month(VM&, Value item, Value options = js_undefined()); +bool iso_year_month_within_limits(ISODate); +ThrowCompletionOr> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr new_target = {}); + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.cpp new file mode 100644 index 000000000000..11f76dc42e62 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2021, Luke Wilde + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainYearMonthConstructor); + +// 9.1 The Temporal.PlainYearMonth Constructor, https://tc39.es/proposal-temporal/#sec-temporal-plainyearmonth-constructor +PlainYearMonthConstructor::PlainYearMonthConstructor(Realm& realm) + : NativeFunction(realm.vm().names.PlainYearMonth.as_string(), realm.intrinsics().function_prototype()) +{ +} + +void PlainYearMonthConstructor::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 9.2.1 Temporal.PlainYearMonth.prototype, https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype + define_direct_property(vm.names.prototype, realm.intrinsics().temporal_plain_year_month_prototype(), 0); + + define_direct_property(vm.names.length, Value(2), Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.from, from, 1, attr); + define_native_function(realm, vm.names.compare, compare, 2, attr); +} + +// 9.1.1 Temporal.PlainYearMonth ( isoYear, isoMonth [ , calendar [ , referenceISODay ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth +ThrowCompletionOr PlainYearMonthConstructor::call() +{ + auto& vm = this->vm(); + + // 1. If NewTarget is undefined, then + // a. Throw a TypeError exception. + return vm.throw_completion(ErrorType::ConstructorWithoutNew, "Temporal.PlainYearMonth"); +} + +// 9.1.1 Temporal.PlainYearMonth ( isoYear, isoMonth [ , calendar [ , referenceISODay ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth +ThrowCompletionOr> PlainYearMonthConstructor::construct(FunctionObject& new_target) +{ + auto& vm = this->vm(); + + auto iso_year = vm.argument(0); + auto iso_month = vm.argument(1); + auto calendar_value = vm.argument(2); + auto reference_iso_day = vm.argument(3); + + // 2. If referenceISODay is undefined, then + if (reference_iso_day.is_undefined()) { + // a. Set referenceISODay to 1𝔽. + reference_iso_day = Value { 1 }; + } + + // 3. Let y be ? ToIntegerWithTruncation(isoYear). + auto year = TRY(to_integer_with_truncation(vm, iso_year, ErrorType::TemporalInvalidPlainYearMonth)); + + // 4. Let m be ? ToIntegerWithTruncation(isoMonth). + auto month = TRY(to_integer_with_truncation(vm, iso_month, ErrorType::TemporalInvalidPlainYearMonth)); + + // 5. If calendar is undefined, set calendar to "iso8601". + if (calendar_value.is_undefined()) + calendar_value = PrimitiveString::create(vm, "iso8601"_string); + + // 6. If calendar is not a String, throw a TypeError exception. + if (!calendar_value.is_string()) + return vm.throw_completion(ErrorType::NotAString, "calendar"sv); + + // 7. Set calendar to ? CanonicalizeCalendar(calendar). + auto calendar = TRY(canonicalize_calendar(vm, calendar_value.as_string().utf8_string_view())); + + // 8. Let ref be ? ToIntegerWithTruncation(referenceISODay). + auto reference = TRY(to_integer_with_truncation(vm, reference_iso_day, ErrorType::TemporalInvalidPlainYearMonth)); + + // 9. If IsValidISODate(y, m, ref) is false, throw a RangeError exception. + if (!is_valid_iso_date(year, month, reference)) + return vm.throw_completion(ErrorType::TemporalInvalidPlainYearMonth); + + // 10. Let isoDate be CreateISODateRecord(y, m, ref). + auto iso_date = create_iso_date_record(year, month, reference); + + // 11. Return ? CreateTemporalYearMonth(isoDate, calendar, NewTarget). + return TRY(create_temporal_year_month(vm, iso_date, move(calendar), &new_target)); +} + +// 9.2.2 Temporal.PlainYearMonth.from ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.from +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthConstructor::from) +{ + // 1. Return ? ToTemporalYearMonth(item, options). + return TRY(to_temporal_year_month(vm, vm.argument(0), vm.argument(1))); +} + +// 9.2.3 Temporal.PlainYearMonth.compare ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.compare +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthConstructor::compare) +{ + // 1. Set one to ? ToTemporalYearMonth(one). + auto one = TRY(to_temporal_year_month(vm, vm.argument(0))); + + // 2. Set two to ? ToTemporalYearMonth(two). + auto two = TRY(to_temporal_year_month(vm, vm.argument(1))); + + // 3. Return 𝔽(CompareISODate(one.[[ISODate]], two.[[ISODate]])). + return compare_iso_date(one->iso_date(), two->iso_date()); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.h new file mode 100644 index 000000000000..41b966b3557a --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthConstructor.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021-2022, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace JS::Temporal { + +class PlainYearMonthConstructor final : public NativeFunction { + JS_OBJECT(PlainYearMonthConstructor, NativeFunction); + GC_DECLARE_ALLOCATOR(PlainYearMonthConstructor); + +public: + virtual void initialize(Realm&) override; + virtual ~PlainYearMonthConstructor() override = default; + + virtual ThrowCompletionOr call() override; + virtual ThrowCompletionOr> construct(FunctionObject& new_target) override; + +private: + explicit PlainYearMonthConstructor(Realm&); + + virtual bool has_constructor() const override { return true; } + + JS_DECLARE_NATIVE_FUNCTION(from); + JS_DECLARE_NATIVE_FUNCTION(compare); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp new file mode 100644 index 000000000000..f9aba3f9c810 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021-2023, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace JS::Temporal { + +GC_DEFINE_ALLOCATOR(PlainYearMonthPrototype); + +// 9.3 Properties of the Temporal.PlainYearMonth Prototype Object, https://tc39.es/proposal-temporal/#sec-properties-of-the-temporal-plainyearmonth-prototype-object +PlainYearMonthPrototype::PlainYearMonthPrototype(Realm& realm) + : PrototypeObject(realm.intrinsics().object_prototype()) +{ +} + +void PlainYearMonthPrototype::initialize(Realm& realm) +{ + Base::initialize(realm); + + auto& vm = this->vm(); + + // 9.3.2 Temporal.PlainYearMonth.prototype[ %Symbol.toStringTag% ], https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype-%symbol.tostringtag% + define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Temporal.PlainYearMonth"_string), Attribute::Configurable); + + define_native_accessor(realm, vm.names.calendarId, calendar_id_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.era, era_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.eraYear, era_year_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.year, year_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.month, month_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.monthCode, month_code_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.daysInYear, days_in_year_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.daysInMonth, days_in_month_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.monthsInYear, months_in_year_getter, {}, Attribute::Configurable); + define_native_accessor(realm, vm.names.inLeapYear, in_leap_year_getter, {}, Attribute::Configurable); +} + +// 9.3.3 get Temporal.PlainYearMonth.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.calendarid +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::calendar_id_getter) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return yearMonth.[[Calendar]]. + return PrimitiveString::create(vm, year_month->calendar()); +} + +// 9.3.4 get Temporal.PlainYearMonth.prototype.era, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.era +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::era_getter) +{ + // 1. Let plainYearMonth be the this value. + // 2. Perform ? RequireInternalSlot(plainYearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return CalendarISOToDate(plainYearMonth.[[Calendar]], plainYearMonth.[[ISODate]]).[[Era]]. + auto result = calendar_iso_to_date(year_month->calendar(), year_month->iso_date()).era; + + if (!result.has_value()) + return js_undefined(); + + return PrimitiveString::create(vm, result.release_value()); +} + +// 9.3.5 get Temporal.PlainYearMonth.prototype.eraYear, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.erayear +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::era_year_getter) +{ + // 1. Let plainYearMonth be the this value. + // 2. Perform ? RequireInternalSlot(plainYearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Let result be CalendarISOToDate(plainYearMonth.[[Calendar]], plainYearMonth.[[ISODate]]).[[EraYear]]. + auto result = calendar_iso_to_date(year_month->calendar(), year_month->iso_date()).era_year; + + // 4. If result is undefined, return undefined. + if (!result.has_value()) + return js_undefined(); + + // 5. Return 𝔽(result). + return *result; +} + +#define JS_ENUMERATE_PLAIN_MONTH_YEAR_SIMPLE_FIELDS \ + __JS_ENUMERATE(year) \ + __JS_ENUMERATE(month) \ + __JS_ENUMERATE(days_in_year) \ + __JS_ENUMERATE(days_in_month) \ + __JS_ENUMERATE(months_in_year) \ + __JS_ENUMERATE(in_leap_year) + +// 9.3.6 get Temporal.PlainYearMonth.prototype.year, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.year +// 9.3.7 get Temporal.PlainYearMonth.prototype.month, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.month +// 9.3.9 get Temporal.PlainYearMonth.prototype.daysInYear, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.daysinyear +// 9.3.10 get Temporal.PlainYearMonth.prototype.daysInMonth, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.daysinmonth +// 9.3.11 get Temporal.PlainYearMonth.prototype.monthsInYear, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.monthsinyear +// 9.3.12 get Temporal.PlainYearMonth.prototype.inLeapYear, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.inleapyear +#define __JS_ENUMERATE(field) \ + JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::field##_getter) \ + { \ + /* 1. Let yearMonth be the this value. */ \ + /* 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). */ \ + auto year_month = TRY(typed_this_object(vm)); \ + \ + /* 3. Return CalendarISOToDate(yearMonth.[[Calendar]], yearMonth.[[ISODate]]).[[]]. */ \ + return calendar_iso_to_date(year_month->calendar(), year_month->iso_date()).field; \ + } +JS_ENUMERATE_PLAIN_MONTH_YEAR_SIMPLE_FIELDS +#undef __JS_ENUMERATE + +// 9.3.8 get Temporal.PlainYearMonth.prototype.monthCode, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.monthcode +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::month_code_getter) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return CalendarISOToDate(yearMonth.[[Calendar]], yearMonth.[[ISODate]]).[[MonthCode]]. + auto month_code = calendar_iso_to_date(year_month->calendar(), year_month->iso_date()).month_code; + return PrimitiveString::create(vm, move(month_code)); +} + +} diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h new file mode 100644 index 000000000000..53a73ff791e4 --- /dev/null +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021, Linus Groh + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace JS::Temporal { + +class PlainYearMonthPrototype final : public PrototypeObject { + JS_PROTOTYPE_OBJECT(PlainYearMonthPrototype, PlainYearMonth, Temporal.PlainYearMonth); + GC_DECLARE_ALLOCATOR(PlainYearMonthPrototype); + +public: + virtual void initialize(Realm&) override; + virtual ~PlainYearMonthPrototype() override = default; + +private: + explicit PlainYearMonthPrototype(Realm&); + + JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter); + JS_DECLARE_NATIVE_FUNCTION(era_getter); + JS_DECLARE_NATIVE_FUNCTION(era_year_getter); + JS_DECLARE_NATIVE_FUNCTION(year_getter); + JS_DECLARE_NATIVE_FUNCTION(month_getter); + JS_DECLARE_NATIVE_FUNCTION(month_code_getter); + JS_DECLARE_NATIVE_FUNCTION(days_in_year_getter); + JS_DECLARE_NATIVE_FUNCTION(days_in_month_getter); + JS_DECLARE_NATIVE_FUNCTION(months_in_year_getter); + JS_DECLARE_NATIVE_FUNCTION(in_leap_year_getter); +}; + +} diff --git a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp index e8d067e3de57..190620a905ca 100644 --- a/Libraries/LibJS/Runtime/Temporal/Temporal.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Temporal.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace JS::Temporal { @@ -32,6 +33,7 @@ void Temporal::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_intrinsic_accessor(vm.names.Duration, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_duration_constructor(); }); define_intrinsic_accessor(vm.names.PlainMonthDay, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_plain_month_day_constructor(); }); + define_intrinsic_accessor(vm.names.PlainYearMonth, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_plain_year_month_constructor(); }); } } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.compare.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.compare.js new file mode 100644 index 000000000000..789845e8d2e9 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.compare.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("length is 2", () => { + expect(Temporal.PlainYearMonth.compare).toHaveLength(2); + }); + + test("basic functionality", () => { + const plainYearMonth1 = new Temporal.PlainYearMonth(2021, 8); + expect(Temporal.PlainYearMonth.compare(plainYearMonth1, plainYearMonth1)).toBe(0); + const plainYearMonth2 = new Temporal.PlainYearMonth(2021, 9); + expect(Temporal.PlainYearMonth.compare(plainYearMonth2, plainYearMonth2)).toBe(0); + expect(Temporal.PlainYearMonth.compare(plainYearMonth1, plainYearMonth2)).toBe(-1); + expect(Temporal.PlainYearMonth.compare(plainYearMonth2, plainYearMonth1)).toBe(1); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.from.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.from.js new file mode 100644 index 000000000000..55fa5e358e1d --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.from.js @@ -0,0 +1,130 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.from).toHaveLength(1); + }); + + test("PlainYearMonth instance argument", () => { + const plainYearMonth_ = new Temporal.PlainYearMonth(2021, 7); + const plainYearMonth = Temporal.PlainYearMonth.from(plainYearMonth_); + expect(plainYearMonth.year).toBe(2021); + expect(plainYearMonth.month).toBe(7); + expect(plainYearMonth.monthCode).toBe("M07"); + expect(plainYearMonth.daysInYear).toBe(365); + expect(plainYearMonth.daysInMonth).toBe(31); + expect(plainYearMonth.monthsInYear).toBe(12); + expect(plainYearMonth.inLeapYear).toBeFalse(); + }); + + test("fields object argument", () => { + const object = { + year: 2021, + month: 7, + }; + const plainYearMonth = Temporal.PlainYearMonth.from(object); + expect(plainYearMonth.year).toBe(2021); + expect(plainYearMonth.month).toBe(7); + expect(plainYearMonth.monthCode).toBe("M07"); + expect(plainYearMonth.daysInYear).toBe(365); + expect(plainYearMonth.daysInMonth).toBe(31); + expect(plainYearMonth.monthsInYear).toBe(12); + expect(plainYearMonth.inLeapYear).toBeFalse(); + }); + + test("from year month string", () => { + const plainYearMonth = Temporal.PlainYearMonth.from("2021-07"); + expect(plainYearMonth.year).toBe(2021); + expect(plainYearMonth.month).toBe(7); + expect(plainYearMonth.monthCode).toBe("M07"); + }); + + test("from date time string", () => { + const plainYearMonth = Temporal.PlainYearMonth.from("2021-07-06T23:42:01"); + expect(plainYearMonth.year).toBe(2021); + expect(plainYearMonth.month).toBe(7); + expect(plainYearMonth.monthCode).toBe("M07"); + expect(plainYearMonth.daysInYear).toBe(365); + expect(plainYearMonth.daysInMonth).toBe(31); + expect(plainYearMonth.monthsInYear).toBe(12); + expect(plainYearMonth.inLeapYear).toBeFalse(); + }); + + test("compares calendar name in year month string in lowercase", () => { + const values = [ + "2023-02[u-ca=iso8601]", + "2023-02[u-ca=isO8601]", + "2023-02[u-ca=iSo8601]", + "2023-02[u-ca=iSO8601]", + "2023-02[u-ca=Iso8601]", + "2023-02[u-ca=IsO8601]", + "2023-02[u-ca=ISo8601]", + "2023-02[u-ca=ISO8601]", + ]; + + for (const value of values) { + expect(() => { + Temporal.PlainYearMonth.from(value); + }).not.toThrowWithMessage( + RangeError, + "YYYY-MM string format can only be used with the iso8601 calendar" + ); + } + }); +}); + +describe("errors", () => { + test("missing fields", () => { + expect(() => { + Temporal.PlainYearMonth.from({}); + }).toThrowWithMessage(TypeError, "Required property year is missing or undefined"); + expect(() => { + Temporal.PlainYearMonth.from({ year: 0 }); + }).toThrowWithMessage(TypeError, "Required property month is missing or undefined"); + expect(() => { + Temporal.PlainYearMonth.from({ month: 1 }); + }).toThrowWithMessage(TypeError, "Required property year is missing or undefined"); + }); + + test("invalid year month string", () => { + expect(() => { + Temporal.PlainYearMonth.from("foo"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("string must not contain a UTC designator", () => { + expect(() => { + Temporal.PlainYearMonth.from("2021-07-06T23:42:01Z"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("extended year must not be negative zero", () => { + expect(() => { + Temporal.PlainYearMonth.from("-000000-01"); + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + expect(() => { + Temporal.PlainYearMonth.from("−000000-01"); // U+2212 + }).toThrowWithMessage(RangeError, "Invalid ISO date time"); + }); + + test("can only use iso8601 calendar with year month strings", () => { + expect(() => { + Temporal.PlainYearMonth.from("2023-02[u-ca=iso8602]"); + }).toThrowWithMessage(RangeError, "Invalid calendar identifier 'iso8602'"); + + expect(() => { + Temporal.PlainYearMonth.from("2023-02[u-ca=ladybird]"); + }).toThrowWithMessage(RangeError, "Invalid calendar identifier 'ladybird'"); + }); + + test("doesn't throw non-iso8601 calendar error when using a superset format string such as DateTime", () => { + // NOTE: This will still throw, but only because "ladybird" is not a recognised calendar, not because of the string format restriction. + try { + Temporal.PlainYearMonth.from("2023-02-10T22:57[u-ca=ladybird]"); + } catch (e) { + expect(e).toBeInstanceOf(RangeError); + expect(e.message).not.toBe( + "MM-DD string format can only be used with the iso8601 calendar" + ); + expect(e.message).toBe("Invalid calendar identifier 'ladybird'"); + } + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js new file mode 100644 index 000000000000..e6887e86c07c --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.js @@ -0,0 +1,60 @@ +describe("errors", () => { + test("called without new", () => { + expect(() => { + Temporal.PlainYearMonth(); + }).toThrowWithMessage( + TypeError, + "Temporal.PlainYearMonth constructor must be called with 'new'" + ); + }); + + test("cannot pass Infinity", () => { + expect(() => { + new Temporal.PlainYearMonth(Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + expect(() => { + new Temporal.PlainYearMonth(0, Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + expect(() => { + new Temporal.PlainYearMonth(0, 1, undefined, Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + expect(() => { + new Temporal.PlainYearMonth(-Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + expect(() => { + new Temporal.PlainYearMonth(0, -Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + expect(() => { + new Temporal.PlainYearMonth(0, 1, undefined, -Infinity); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + }); + + test("cannot pass invalid ISO month/day", () => { + expect(() => { + new Temporal.PlainYearMonth(0, 0); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + expect(() => { + new Temporal.PlainYearMonth(0, 1, undefined, 0); + }).toThrowWithMessage(RangeError, "Invalid plain year month"); + }); +}); + +describe("normal behavior", () => { + test("length is 2", () => { + expect(Temporal.PlainYearMonth).toHaveLength(2); + }); + + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(typeof plainYearMonth).toBe("object"); + expect(plainYearMonth).toBeInstanceOf(Temporal.PlainYearMonth); + expect(Object.getPrototypeOf(plainYearMonth)).toBe(Temporal.PlainYearMonth.prototype); + }); + + // FIXME: Re-implement this test with Temporal.PlainYearMonth.prototype.toString({ calendarName: "always" }). + // test("default reference day is 1", () => { + // const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + // const fields = plainYearMonth.getISOFields(); + // expect(fields.isoDay).toBe(1); + // }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.calendarId.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.calendarId.js new file mode 100644 index 000000000000..800f2413e519 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.calendarId.js @@ -0,0 +1,15 @@ +describe("correct behavior", () => { + test("calendarId basic functionality", () => { + const calendar = "iso8601"; + const plainYearMonth = new Temporal.PlainYearMonth(2000, 5, calendar); + expect(plainYearMonth.calendarId).toBe("iso8601"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "calendarId", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInMonth.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInMonth.js new file mode 100644 index 000000000000..59c07055d415 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInMonth.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.daysInMonth).toBe(31); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "daysInMonth", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInYear.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInYear.js new file mode 100644 index 000000000000..a0a114902f73 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.daysInYear.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.daysInYear).toBe(365); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "daysInYear", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.era.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.era.js new file mode 100644 index 000000000000..747704310703 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.era.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.era).toBeUndefined(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "era", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.eraYear.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.eraYear.js new file mode 100644 index 000000000000..7ebaef8b627f --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.eraYear.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.eraYear).toBeUndefined(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "eraYear", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.inLeapYear.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.inLeapYear.js new file mode 100644 index 000000000000..f79e77e7bda9 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.inLeapYear.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.inLeapYear).toBe(false); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "inLeapYear", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.month.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.month.js new file mode 100644 index 000000000000..8c09f11cbe8a --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.month.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.month).toBe(7); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "month", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthCode.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthCode.js new file mode 100644 index 000000000000..8fdd1852daed --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthCode.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.monthCode).toBe("M07"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "monthCode", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthsInYear.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthsInYear.js new file mode 100644 index 000000000000..6ca5f84084a7 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.monthsInYear.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.monthsInYear).toBe(12); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "monthsInYear", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.year.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.year.js new file mode 100644 index 000000000000..13e0021ab34e --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.year.js @@ -0,0 +1,14 @@ +describe("correct behavior", () => { + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.year).toBe(2021); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Reflect.get(Temporal.PlainYearMonth.prototype, "year", "foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); From 2da526423fec330ff642c5443ba9d0c3347b4120 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 11:39:26 -0500 Subject: [PATCH 005/397] LibJS: Implement stringification Temporal.PlainYearMonth prototypes --- .../LibJS/Runtime/Temporal/PlainYearMonth.cpp | 27 ++++++++ .../LibJS/Runtime/Temporal/PlainYearMonth.h | 1 + .../Temporal/PlainYearMonthPrototype.cpp | 46 +++++++++++++ .../Temporal/PlainYearMonthPrototype.h | 3 + .../Temporal/PlainYearMonth/PlainYearMonth.js | 12 ++-- .../PlainYearMonth.prototype.toJSON.js | 23 +++++++ ...PlainYearMonth.prototype.toLocaleString.js | 23 +++++++ .../PlainYearMonth.prototype.toString.js | 64 +++++++++++++++++++ 8 files changed, 193 insertions(+), 6 deletions(-) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp index 2280d3a72128..f737d92a9492 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -149,4 +149,31 @@ ThrowCompletionOr> create_temporal_year_month(VM& vm, IS return object; } +// 9.5.6 TemporalYearMonthToString ( yearMonth, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-temporalyearmonthtostring +String temporal_year_month_to_string(PlainYearMonth const& year_month, ShowCalendar show_calendar) +{ + // 1. Let year be PadISOYear(yearMonth.[[ISODate]].[[Year]]). + auto year = pad_iso_year(year_month.iso_date().year); + + // 2. Let month be ToZeroPaddedDecimalString(yearMonth.[[ISODate]].[[Month]], 2). + // 3. Let result be the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and month. + auto result = MUST(String::formatted("{}-{:02}", year, year_month.iso_date().month)); + + // 4. If showCalendar is one of always or critical, or if yearMonth.[[Calendar]] is not "iso8601", then + if (show_calendar == ShowCalendar::Always || show_calendar == ShowCalendar::Critical || year_month.calendar() != "iso8601"sv) { + // a. Let day be ToZeroPaddedDecimalString(yearMonth.[[ISODate]].[[Day]], 2). + // b. Set result to the string-concatenation of result, the code unit 0x002D (HYPHEN-MINUS), and day. + result = MUST(String::formatted("{}-{:02}", result, year_month.iso_date().day)); + } + + // 5. Let calendarString be FormatCalendarAnnotation(yearMonth.[[Calendar]], showCalendar). + auto calendar_string = format_calendar_annotation(year_month.calendar(), show_calendar); + + // 6. Set result to the string-concatenation of result and calendarString. + result = MUST(String::formatted("{}{}", result, calendar_string)); + + // 7. Return result. + return result; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h index 36884ed74c92..20b876d244af 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h @@ -35,5 +35,6 @@ class PlainYearMonth final : public Object { ThrowCompletionOr> to_temporal_year_month(VM&, Value item, Value options = js_undefined()); bool iso_year_month_within_limits(ISODate); ThrowCompletionOr> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr new_target = {}); +String temporal_year_month_to_string(PlainYearMonth const&, ShowCalendar); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index f9aba3f9c810..b363af9c1581 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include @@ -37,6 +38,11 @@ void PlainYearMonthPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.daysInMonth, days_in_month_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.monthsInYear, months_in_year_getter, {}, Attribute::Configurable); define_native_accessor(realm, vm.names.inLeapYear, in_leap_year_getter, {}, Attribute::Configurable); + + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.toString, to_string, 0, attr); + define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); + define_native_function(realm, vm.names.toJSON, to_json, 0, attr); } // 9.3.3 get Temporal.PlainYearMonth.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.calendarid @@ -123,4 +129,44 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::month_code_getter) return PrimitiveString::create(vm, move(month_code)); } +// 9.3.19 Temporal.PlainYearMonth.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tostring +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_string) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, vm.argument(0))); + + // 4. Let showCalendar be ? GetTemporalShowCalendarNameOption(resolvedOptions). + auto show_calendar = TRY(get_temporal_show_calendar_name_option(vm, resolved_options)); + + // 5. Return TemporalYearMonthToString(yearMonth, showCalendar). + return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, show_calendar)); +} + +// 9.3.20 Temporal.PlainYearMonth.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tolocalestring +// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402. +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_locale_string) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return TemporalYearMonthToString(yearMonth, AUTO). + return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, ShowCalendar::Auto)); +} + +// 9.3.21 Temporal.PlainYearMonth.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tojson +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_json) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return TemporalYearMonthToString(yearMonth, AUTO). + return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, ShowCalendar::Auto)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 53a73ff791e4..57e288bdee1f 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -33,6 +33,9 @@ class PlainYearMonthPrototype final : public PrototypeObject { expect(Object.getPrototypeOf(plainYearMonth)).toBe(Temporal.PlainYearMonth.prototype); }); - // FIXME: Re-implement this test with Temporal.PlainYearMonth.prototype.toString({ calendarName: "always" }). - // test("default reference day is 1", () => { - // const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); - // const fields = plainYearMonth.getISOFields(); - // expect(fields.isoDay).toBe(1); - // }); + test("default reference day is 1", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + const fields = plainYearMonth.toString({ calendarName: "always" }); + const day = fields.split("-")[2].split("[")[0]; + expect(day).toBe("01"); + }); }); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js new file mode 100644 index 000000000000..8e98e16218f5 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toJSON.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainYearMonth.prototype.toJSON).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainYearMonth; + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.toJSON()).toBe("2021-07"); + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7, "gregory", 6); + expect(plainYearMonth.toJSON()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.toJSON.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js new file mode 100644 index 000000000000..4fb4aa16bf43 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toLocaleString.js @@ -0,0 +1,23 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainYearMonth.prototype.toLocaleString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainYearMonth; + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.toLocaleString()).toBe("2021-07"); + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7, "gregory", 6); + expect(plainYearMonth.toLocaleString()).toBe("2021-07-06[u-ca=gregory]"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.toLocaleString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js new file mode 100644 index 000000000000..99a667c69983 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.toString.js @@ -0,0 +1,64 @@ +describe("correct behavior", () => { + test("length is 0", () => { + expect(Temporal.PlainYearMonth.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + let plainYearMonth; + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(plainYearMonth.toString()).toBe("2021-07"); + expect(plainYearMonth.toString({ calendarName: "auto" })).toBe("2021-07"); + expect(plainYearMonth.toString({ calendarName: "always" })).toBe( + "2021-07-01[u-ca=iso8601]" + ); + expect(plainYearMonth.toString({ calendarName: "never" })).toBe("2021-07"); + expect(plainYearMonth.toString({ calendarName: "critical" })).toBe( + "2021-07-01[!u-ca=iso8601]" + ); + + plainYearMonth = new Temporal.PlainYearMonth(2021, 7, "gregory", 6); + expect(plainYearMonth.toString()).toBe("2021-07-06[u-ca=gregory]"); + expect(plainYearMonth.toString({ calendarName: "auto" })).toBe("2021-07-06[u-ca=gregory]"); + expect(plainYearMonth.toString({ calendarName: "always" })).toBe( + "2021-07-06[u-ca=gregory]" + ); + expect(plainYearMonth.toString({ calendarName: "never" })).toBe("2021-07-06"); + expect(plainYearMonth.toString({ calendarName: "critical" })).toBe( + "2021-07-06[!u-ca=gregory]" + ); + + plainYearMonth = new Temporal.PlainYearMonth(0, 1); + expect(plainYearMonth.toString()).toBe("0000-01"); + + plainYearMonth = new Temporal.PlainYearMonth(999, 1); + expect(plainYearMonth.toString()).toBe("0999-01"); + + plainYearMonth = new Temporal.PlainYearMonth(9999, 1); + expect(plainYearMonth.toString()).toBe("9999-01"); + + plainYearMonth = new Temporal.PlainYearMonth(12345, 1); + expect(plainYearMonth.toString()).toBe("+012345-01"); + + plainYearMonth = new Temporal.PlainYearMonth(123456, 1); + expect(plainYearMonth.toString()).toBe("+123456-01"); + + plainYearMonth = new Temporal.PlainYearMonth(-12345, 1); + expect(plainYearMonth.toString()).toBe("-012345-01"); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.toString.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); + + test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(() => { + plainYearMonth.toString({ calendarName: "foo" }); + }).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName"); + }); +}); From 203269fae223931eae79d45b52a7704b590afd62 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 11:56:09 -0500 Subject: [PATCH 006/397] LibJS: Implement Temporal.PlainYearMonth.prototype.with/equals --- .../Temporal/PlainYearMonthPrototype.cpp | 59 +++++++++++++++++++ .../Temporal/PlainYearMonthPrototype.h | 2 + .../PlainYearMonth.prototype.equals.js | 14 +++++ .../PlainYearMonth.prototype.with.js | 54 +++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.equals.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.with.js diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index b363af9c1581..b3d5c7421c85 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -40,6 +40,8 @@ void PlainYearMonthPrototype::initialize(Realm& realm) define_native_accessor(realm, vm.names.inLeapYear, in_leap_year_getter, {}, Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(realm, vm.names.with, with, 1, attr); + define_native_function(realm, vm.names.equals, equals, 1, attr); define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(realm, vm.names.toJSON, to_json, 0, attr); @@ -129,6 +131,63 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::month_code_getter) return PrimitiveString::create(vm, move(month_code)); } +// 9.3.13 Temporal.PlainYearMonth.prototype.with ( temporalYearMonthLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.with +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::with) +{ + auto temporal_year_month_like = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. If ? IsPartialTemporalObject(temporalYearMonthLike) is false, throw a TypeError exception. + if (!TRY(is_partial_temporal_object(vm, temporal_year_month_like))) + return vm.throw_completion(ErrorType::TemporalObjectMustBePartialTemporalObject); + + // 4. Let calendar be yearMonth.[[Calendar]]. + auto const& calendar = year_month->calendar(); + + // 5. Let fields be ISODateToFields(calendar, yearMonth.[[ISODate]], YEAR-MONTH). + auto fields = iso_date_to_fields(calendar, year_month->iso_date(), DateType::YearMonth); + + // 6. Let partialYearMonth be ? PrepareCalendarFields(calendar, temporalYearMonthLike, « YEAR, MONTH, MONTH-CODE », « », PARTIAL). + auto partial_year_month = TRY(prepare_calendar_fields(vm, calendar, temporal_year_month_like.as_object(), { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode } }, {}, Partial {})); + + // 7. Set fields to CalendarMergeFields(calendar, fields, partialYearMonth). + fields = calendar_merge_fields(calendar, fields, partial_year_month); + + // 8. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 9. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 10. Let isoDate be ? CalendarYearMonthFromFields(calendar, fields, overflow). + auto iso_date = TRY(calendar_year_month_from_fields(vm, calendar, fields, overflow)); + + // 11. Return ! CreateTemporalYearMonth(isoDate, calendar). + return MUST(create_temporal_year_month(vm, iso_date, calendar)); +} + +// 9.3.18 Temporal.PlainYearMonth.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.equals +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::equals) +{ + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Set other to ? ToTemporalYearMonth(other). + auto other = TRY(to_temporal_year_month(vm, vm.argument(0))); + + // 4. If CompareISODate(yearMonth.[[ISODate]], other.[[ISODate]]) ≠ 0, return false. + if (compare_iso_date(year_month->iso_date(), other->iso_date()) != 0) + return false; + + // 5. Return CalendarEquals(yearMonth.[[Calendar]], other.[[Calendar]]). + return calendar_equals(year_month->calendar(), other->calendar()); +} + // 9.3.19 Temporal.PlainYearMonth.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_string) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 57e288bdee1f..737412ea623e 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -33,6 +33,8 @@ class PlainYearMonthPrototype final : public PrototypeObject { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.prototype.equals).toHaveLength(1); + }); + + test("basic functionality", () => { + const firstPlainDateTime = new Temporal.PlainYearMonth(1, 1, "iso8601"); + const secondPlainDateTime = new Temporal.PlainYearMonth(0, 1, "iso8601"); + expect(firstPlainDateTime.equals(firstPlainDateTime)).toBeTrue(); + expect(secondPlainDateTime.equals(secondPlainDateTime)).toBeTrue(); + expect(firstPlainDateTime.equals(secondPlainDateTime)).toBeFalse(); + expect(secondPlainDateTime.equals(firstPlainDateTime)).toBeFalse(); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.with.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.with.js new file mode 100644 index 000000000000..f6260cba8903 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.with.js @@ -0,0 +1,54 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.prototype.with).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(1970, 1); + const values = [ + [{ year: 2021 }, new Temporal.PlainYearMonth(2021, 1)], + [{ year: 2021, month: 7 }, new Temporal.PlainYearMonth(2021, 7)], + [{ year: 2021, monthCode: "M07" }, new Temporal.PlainYearMonth(2021, 7)], + ]; + for (const [arg, expected] of values) { + expect(plainYearMonth.with(arg).equals(expected)).toBeTrue(); + } + + // Supplying the same values doesn't change the year/month, but still creates a new object + const plainYearMonthLike = { year: plainYearMonth.year, month: plainYearMonth.month }; + expect(plainYearMonth.with(plainYearMonthLike)).not.toBe(plainYearMonth); + expect(plainYearMonth.with(plainYearMonthLike).equals(plainYearMonth)).toBeTrue(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.with.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); + + test("argument must be an object", () => { + expect(() => { + new Temporal.PlainYearMonth(1970, 1).with("foo"); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainYearMonth(1970, 1).with(42); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must have one of 'month', 'monthCode', 'year'", () => { + expect(() => { + new Temporal.PlainYearMonth(1970, 1).with({}); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); + + test("argument must not have 'calendar' or 'timeZone'", () => { + expect(() => { + new Temporal.PlainYearMonth(1970, 1).with({ calendar: {} }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + expect(() => { + new Temporal.PlainYearMonth(1970, 1).with({ timeZone: {} }); + }).toThrowWithMessage(TypeError, "Object must be a partial Temporal object"); + }); +}); From cb5d1b5086db673488a0aa0bc13a6c3af4a4309f Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 13:21:29 -0500 Subject: [PATCH 007/397] LibJS: Implement Temporal.PlainYearMonth.prototype.until/since --- Libraries/LibJS/Forward.h | 1 + Libraries/LibJS/Runtime/ErrorTypes.h | 2 + .../Runtime/Temporal/AbstractOperations.cpp | 91 +++ .../Runtime/Temporal/AbstractOperations.h | 15 + Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 166 +++++ Libraries/LibJS/Runtime/Temporal/Calendar.h | 3 + .../LibJS/Runtime/Temporal/DateEquations.cpp | 93 +++ .../LibJS/Runtime/Temporal/DateEquations.h | 2 + Libraries/LibJS/Runtime/Temporal/Duration.cpp | 580 +++++++++++++++++- Libraries/LibJS/Runtime/Temporal/Duration.h | 22 + Libraries/LibJS/Runtime/Temporal/Instant.cpp | 13 + Libraries/LibJS/Runtime/Temporal/Instant.h | 2 + .../LibJS/Runtime/Temporal/PlainDate.cpp | 40 ++ Libraries/LibJS/Runtime/Temporal/PlainDate.h | 2 + .../LibJS/Runtime/Temporal/PlainDateTime.cpp | 15 + .../LibJS/Runtime/Temporal/PlainDateTime.h | 1 + .../LibJS/Runtime/Temporal/PlainTime.cpp | 52 ++ Libraries/LibJS/Runtime/Temporal/PlainTime.h | 2 + .../LibJS/Runtime/Temporal/PlainYearMonth.cpp | 94 +++ .../LibJS/Runtime/Temporal/PlainYearMonth.h | 8 + .../Temporal/PlainYearMonthPrototype.cpp | 31 + .../Temporal/PlainYearMonthPrototype.h | 2 + Libraries/LibJS/Runtime/Temporal/TimeZone.cpp | 122 ++++ Libraries/LibJS/Runtime/Temporal/TimeZone.h | 12 + .../PlainYearMonth.prototype.since.js | 108 ++++ .../PlainYearMonth.prototype.until.js | 108 ++++ 26 files changed, 1586 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 766ae9fd5219..af73e54a3fe5 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -286,6 +286,7 @@ struct DateDuration; struct InternalDuration; struct ISODate; struct ISODateTime; +struct ISOYearMonth; struct ParseResult; struct PartialDuration; struct Time; diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index e5301f763f1c..59850358e2c0 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -238,6 +238,8 @@ M(TemporalAmbiguousMonthOfPlainMonthDay, "Accessing month of PlainMonthDay is ambiguous, use monthCode instead") \ M(TemporalDifferentCalendars, "Cannot compare dates from two different calendars") \ M(TemporalDifferentTimeZones, "Cannot compare dates from two different time zones") \ + M(TemporalDisambiguatePossibleEpochNSRejectMoreThanOne, "Cannot disambiguate two or more possible epoch nanoseconds") \ + M(TemporalDisambiguatePossibleEpochNSRejectZero, "Cannot disambiguate zero possible epoch nanoseconds") \ M(TemporalDisambiguatePossibleInstantsEarlierZero, "Cannot disambiguate zero possible instants with mode \"earlier\"") \ M(TemporalDisambiguatePossibleInstantsRejectMoreThanOne, "Cannot disambiguate two or more possible instants with mode \"reject\"") \ M(TemporalDisambiguatePossibleInstantsRejectZero, "Cannot disambiguate zero possible instants with mode \"reject\"") \ diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 63434bf6454f..aee307c1d8cd 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -72,6 +72,19 @@ double epoch_days_to_epoch_ms(double day, double time) return day * JS::ms_per_day + time; } +// 13.4 CheckISODaysRange ( isoDate ), https://tc39.es/proposal-temporal/#sec-checkisodaysrange +ThrowCompletionOr check_iso_days_range(VM& vm, ISODate const& iso_date) +{ + // 1. If abs(ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]])) > 10**8, then + if (fabs(iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day)) > 100'000'000) { + // a. Throw a RangeError exception. + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + } + + // 2. Return unused. + return {}; +} + // 13.6 GetTemporalOverflowOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& options) { @@ -86,6 +99,29 @@ ThrowCompletionOr get_temporal_overflow_option(VM& vm, Object const& o return Overflow::Reject; } +// 13.8 NegateRoundingMode ( roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-negateroundingmode +RoundingMode negate_rounding_mode(RoundingMode rounding_mode) +{ + // 1. If roundingMode is CEIL, return FLOOR. + if (rounding_mode == RoundingMode::Ceil) + return RoundingMode::Floor; + + // 2. If roundingMode is FLOOR, return CEIL. + if (rounding_mode == RoundingMode::Floor) + return RoundingMode::Ceil; + + // 3. If roundingMode is HALF-CEIL, return HALF-FLOOR. + if (rounding_mode == RoundingMode::HalfCeil) + return RoundingMode::HalfFloor; + + // 4. If roundingMode is HALF-FLOOR, return HALF-CEIL. + if (rounding_mode == RoundingMode::HalfFloor) + return RoundingMode::HalfCeil; + + // 5. Return roundingMode. + return rounding_mode; +} + // 13.10 GetTemporalShowCalendarNameOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowcalendarnameoption ThrowCompletionOr get_temporal_show_calendar_name_option(VM& vm, Object const& options) { @@ -1403,6 +1439,61 @@ CalendarFields iso_date_to_fields(StringView calendar, ISODate const& iso_date, return fields; } +// 13.43 GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, fallbackSmallestUnit, smallestLargestDefaultUnit ), https://tc39.es/proposal-temporal/#sec-temporal-getdifferencesettings +ThrowCompletionOr get_difference_settings(VM& vm, DurationOperation operation, Object const& options, UnitGroup unit_group, ReadonlySpan disallowed_units, Unit fallback_smallest_unit, Unit smallest_largest_default_unit) +{ + // 1. NOTE: The following steps read options and perform independent validation in alphabetical order. + + // 2. Let largestUnit be ? GetTemporalUnitValuedOption(options, "largestUnit", unitGroup, AUTO). + auto largest_unit = TRY(get_temporal_unit_valued_option(vm, options, vm.names.largestUnit, unit_group, Auto {})); + + // 3. If disallowedUnits contains largestUnit, throw a RangeError exception. + if (auto* unit = largest_unit.get_pointer(); unit && disallowed_units.contains_slow(*unit)) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(*unit), vm.names.largestUnit); + + // 4. Let roundingIncrement be ? GetRoundingIncrementOption(options). + auto rounding_increment = TRY(get_rounding_increment_option(vm, options)); + + // 5. Let roundingMode be ? GetRoundingModeOption(options, TRUNC). + auto rounding_mode = TRY(get_rounding_mode_option(vm, options, RoundingMode::Trunc)); + + // 6. If operation is SINCE, then + if (operation == DurationOperation::Since) { + // a. Set roundingMode to NegateRoundingMode(roundingMode). + rounding_mode = negate_rounding_mode(rounding_mode); + } + + // 7. Let smallestUnit be ? GetTemporalUnitValuedOption(options, "smallestUnit", unitGroup, fallbackSmallestUnit). + auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, options, vm.names.smallestUnit, unit_group, fallback_smallest_unit)); + auto smallest_unit_value = smallest_unit.get(); + + // 8. If disallowedUnits contains smallestUnit, throw a RangeError exception. + if (disallowed_units.contains_slow(smallest_unit_value)) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(smallest_unit_value), vm.names.smallestUnit); + + // 9. Let defaultLargestUnit be LargerOfTwoTemporalUnits(smallestLargestDefaultUnit, smallestUnit). + auto default_largest_unit = larger_of_two_temporal_units(smallest_largest_default_unit, smallest_unit.get()); + + // 10. If largestUnit is AUTO, set largestUnit to defaultLargestUnit. + if (largest_unit.has()) + largest_unit = default_largest_unit; + auto largest_unit_value = largest_unit.get(); + + // 11. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception. + if (larger_of_two_temporal_units(largest_unit_value, smallest_unit_value) != largest_unit_value) + return vm.throw_completion(ErrorType::TemporalInvalidUnitRange, temporal_unit_to_string(smallest_unit_value), temporal_unit_to_string(largest_unit_value)); + + // 12. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit). + auto maximum = maximum_temporal_duration_rounding_increment(smallest_unit_value); + + // 13. If maximum is not UNSET, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false). + if (!maximum.has()) + TRY(validate_temporal_rounding_increment(vm, rounding_increment, maximum.get(), false)); + + // 14. Return the Record { [[SmallestUnit]]: smallestUnit, [[LargestUnit]]: largestUnit, [[RoundingMode]]: roundingMode, [[RoundingIncrement]]: roundingIncrement, }. + return DifferenceSettings { .smallest_unit = smallest_unit_value, .largest_unit = largest_unit_value, .rounding_mode = rounding_mode, .rounding_increment = rounding_increment }; +} + // 14.4.1.1 GetOptionsObject ( options ), https://tc39.es/proposal-temporal/#sec-getoptionsobject ThrowCompletionOr> get_options_object(VM& vm, Value options) { diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index f31a269e55fd..a91f95f80f34 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -33,6 +33,11 @@ enum class DateType { YearMonth, }; +enum class DurationOperation { + Since, + Until, +}; + enum class Overflow { Constrain, Reject, @@ -148,9 +153,18 @@ struct ParsedISODateTime { Optional calendar; }; +struct DifferenceSettings { + Unit smallest_unit; + Unit largest_unit; + RoundingMode rounding_mode; + u64 rounding_increment { 0 }; +}; + double iso_date_to_epoch_days(double year, double month, double date); double epoch_days_to_epoch_ms(double day, double time); +ThrowCompletionOr check_iso_days_range(VM&, ISODate const&); ThrowCompletionOr get_temporal_overflow_option(VM&, Object const& options); +RoundingMode negate_rounding_mode(RoundingMode); ThrowCompletionOr get_temporal_show_calendar_name_option(VM&, Object const& options); ThrowCompletionOr validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive); ThrowCompletionOr get_temporal_fractional_second_digits_option(VM&, Object const& options); @@ -177,6 +191,7 @@ ThrowCompletionOr parse_temporal_time_zone_string(VM& vm, StringView t ThrowCompletionOr to_month_code(VM&, Value argument); ThrowCompletionOr to_offset_string(VM&, Value argument); CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType); +ThrowCompletionOr get_difference_settings(VM&, DurationOperation, Object const& options, UnitGroup, ReadonlySpan disallowed_units, Unit fallback_smallest_unit, Unit smallest_largest_default_unit); // 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation template diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index 7ff0dc068979..1113546c76eb 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -311,6 +312,154 @@ CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& return merged; } +// 12.2.6 CalendarDateAdd ( calendar, isoDate, duration, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateadd +ThrowCompletionOr calendar_date_add(VM& vm, StringView calendar, ISODate const& iso_date, DateDuration const& duration, Overflow overflow) +{ + ISODate result; + + // 1. If calendar is "iso8601", then + // FIXME: Return an ISODate for an ISO8601 calendar for now. + if (true || calendar == "iso8601"sv) { + // a. Let intermediate be BalanceISOYearMonth(isoDate.[[Year]] + duration.[[Years]], isoDate.[[Month]] + duration.[[Months]]). + auto intermediate = balance_iso_year_month(static_cast(iso_date.year) + duration.years, static_cast(iso_date.month) + duration.months); + + // b. Set intermediate to ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], isoDate.[[Day]], overflow). + auto intermediate_date = TRY(regulate_iso_date(vm, intermediate.year, intermediate.month, iso_date.day, overflow)); + + // c. Let d be intermediate.[[Day]] + duration.[[Days]] + 7 × duration.[[Weeks]]. + auto day = intermediate_date.day + duration.days + (7 * duration.weeks); + + // d. Let result be BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d). + result = balance_iso_date(intermediate_date.year, intermediate_date.month, day); + } + // 2. Else, + else { + // a. Let result be an implementation-defined ISO Date Record, or throw a RangeError exception, as described below. + } + + // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception. + if (!iso_date_within_limits(result)) + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + + // 4. Return result. + return result; +} + +// 12.2.7 CalendarDateUntil ( calendar, one, two, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-calendardateuntil +DateDuration calendar_date_until(VM& vm, StringView calendar, ISODate const& one, ISODate const& two, Unit largest_unit) +{ + // 1. If calendar is "iso8601", then + if (calendar == "iso8601"sv) { + // a. Let sign be -CompareISODate(one, two). + auto sign = compare_iso_date(one, two); + sign *= -1; + + // b. If sign = 0, return ZeroDateDuration(). + if (sign == 0) + return zero_date_duration(vm); + + // c. Let years be 0. + double years = 0; + + // d. If largestUnit is YEAR, then + if (largest_unit == Unit::Year) { + // i. Let candidateYears be sign. + double candidate_years = sign; + + // ii. Repeat, while ISODateSurpasses(sign, one.[[Year]] + candidateYears, one.[[Month]], one.[[Day]], two) is false, + while (!iso_date_surpasses(sign, static_cast(one.year) + candidate_years, one.month, one.day, two)) { + // 1. Set years to candidateYears. + years = candidate_years; + + // 2. Set candidateYears to candidateYears + sign. + candidate_years += sign; + } + } + + // e. Let months be 0. + double months = 0; + + // f. If largestUnit is YEAR or largestUnit is MONTH, then + if (largest_unit == Unit::Year || largest_unit == Unit::Month) { + // i. Let candidateMonths be sign. + double candidate_months = sign; + + // ii. Let intermediate be BalanceISOYearMonth(one.[[Year]] + years, one.[[Month]] + candidateMonths). + auto intermediate = balance_iso_year_month(static_cast(one.year) + years, static_cast(one.month) + candidate_months); + + // iii. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], one.[[Day]], two) is false, + while (!iso_date_surpasses(sign, intermediate.year, intermediate.month, one.day, two)) { + // 1. Set months to candidateMonths. + months = candidate_months; + + // 2. Set candidateMonths to candidateMonths + sign. + candidate_months += sign; + + // 3. Set intermediate to BalanceISOYearMonth(intermediate.[[Year]], intermediate.[[Month]] + sign). + intermediate = balance_iso_year_month(intermediate.year, static_cast(intermediate.month) + sign); + } + } + + // g. Set intermediate to BalanceISOYearMonth(one.[[Year]] + years, one.[[Month]] + months). + auto intermediate = balance_iso_year_month(static_cast(one.year) + years, static_cast(one.month) + months); + + // h. Let constrained be ! RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], one.[[Day]], CONSTRAIN). + auto constrained = MUST(regulate_iso_date(vm, intermediate.year, intermediate.month, one.day, Overflow::Constrain)); + + // i. Let weeks be 0. + double weeks = 0; + + // j. If largestUnit is WEEK, then + if (largest_unit == Unit::Week) { + // i. Let candidateWeeks be sign. + double candidate_weeks = sign; + + // ii. Set intermediate to BalanceISODate(constrained.[[Year]], constrained.[[Month]], constrained.[[Day]] + 7 × candidateWeeks). + auto intermediate = balance_iso_date(constrained.year, constrained.month, static_cast(constrained.day) + (7.0 * candidate_weeks)); + + // iii. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]], two) is false, + while (!iso_date_surpasses(sign, intermediate.year, intermediate.month, intermediate.day, two)) { + // 1. Set weeks to candidateWeeks. + weeks = candidate_weeks; + + // 2. Set candidateWeeks to candidateWeeks + sign. + candidate_weeks += sign; + + // 3. Set intermediate to BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]] + 7 × sign). + intermediate = balance_iso_date(intermediate.year, intermediate.month, static_cast(intermediate.day) + (7.0 * sign)); + } + } + + // k. Let days be 0. + double days = 0; + + // l. Let candidateDays be sign. + double candidate_days = sign; + + // m. Set intermediate to BalanceISODate(constrained.[[Year]], constrained.[[Month]], constrained.[[Day]] + 7 × weeks + candidateDays). + auto intermediate_date = balance_iso_date(constrained.year, constrained.month, static_cast(constrained.day) + (7.0 * weeks) + candidate_days); + + // n. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]], two) is false, + while (!iso_date_surpasses(sign, intermediate_date.year, intermediate_date.month, intermediate_date.day, two)) { + // i. Set days to candidateDays. + days = candidate_days; + + // ii. Set candidateDays to candidateDays + sign. + candidate_days += sign; + + // iii. Set intermediate to BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], intermediate.[[Day]] + sign). + intermediate_date = balance_iso_date(intermediate_date.year, intermediate_date.month, static_cast(intermediate_date.day) + sign); + } + + // o. Return ! CreateDateDurationRecord(years, months, weeks, days). + return MUST(create_date_duration_record(vm, years, months, weeks, days)); + } + + // 2. Return an implementation-defined Date Duration Record as described above. + // FIXME: Return a DateDuration for an ISO8601 calendar for now. + return calendar_date_until(vm, "iso8601"sv, one, two, largest_unit); +} + // 12.2.8 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like) { @@ -365,6 +514,23 @@ ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM& return TRY(to_temporal_calendar_identifier(vm, calendar_like)); } +// 12.2.10 CalendarDateFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendardatefromfields +ThrowCompletionOr calendar_date_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow) +{ + // 1. Perform ? CalendarResolveFields(calendar, fields, DATE). + TRY(calendar_resolve_fields(vm, calendar, fields, DateType::Date)); + + // 2. Let result be ? CalendarDateToISO(calendar, fields, overflow). + auto result = TRY(calendar_date_to_iso(vm, calendar, fields, overflow)); + + // 3. If ISODateWithinLimits(result) is false, throw a RangeError exception. + if (!iso_date_within_limits(result)) + return vm.throw_completion(ErrorType::TemporalInvalidISODate); + + // 4. Return result. + return result; +} + // 12.2.11 CalendarYearMonthFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendaryearmonthfromfields ThrowCompletionOr calendar_year_month_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow) { diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.h b/Libraries/LibJS/Runtime/Temporal/Calendar.h index df2dce55feee..0c820b0c6432 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.h +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.h @@ -99,6 +99,7 @@ using CalendarFieldListOrPartial = Variant; ThrowCompletionOr canonicalize_calendar(VM&, StringView id); Vector const& available_calendars(); ThrowCompletionOr prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names); +ThrowCompletionOr calendar_date_from_fields(VM&, StringView calendar, CalendarFields, Overflow); ThrowCompletionOr calendar_year_month_from_fields(VM&, StringView calendar, CalendarFields, Overflow); ThrowCompletionOr calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow); String format_calendar_annotation(StringView id, ShowCalendar); @@ -109,6 +110,8 @@ u16 iso_day_of_year(ISODate const&); u8 iso_day_of_week(ISODate const&); Vector calendar_field_keys_present(CalendarFields const&); CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields); +ThrowCompletionOr calendar_date_add(VM&, StringView calendar, ISODate const&, DateDuration const&, Overflow); +DateDuration calendar_date_until(VM&, StringView calendar, ISODate const&, ISODate const&, Unit largest_unit); ThrowCompletionOr to_temporal_calendar_identifier(VM&, Value temporal_calendar_like); ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item); ThrowCompletionOr calendar_date_to_iso(VM&, StringView calendar, CalendarFields const&, Overflow); diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp index fc499e183f04..473add52b51b 100644 --- a/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.cpp @@ -79,6 +79,52 @@ u16 epoch_time_to_day_in_year(double time) return static_cast(epoch_time_to_day_number(time) - epoch_day_number_for_year(epoch_time_to_epoch_year(time))); } +// https://tc39.es/proposal-temporal/#eqn-epochtimetomonthinyear +u8 epoch_time_to_month_in_year(double time) +{ + auto day_in_year = epoch_time_to_day_in_year(time); + auto in_leap_year = mathematical_in_leap_year(time); + + // EpochTimeToMonthInYear(t) + // = 0 if 0 ≤ EpochTimeToDayInYear(t) < 31 + // = 1 if 31 ≤ EpochTimeToDayInYear(t) < 59 + MathematicalInLeapYear(t) + // = 2 if 59 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 90 + MathematicalInLeapYear(t) + // = 3 if 90 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 120 + MathematicalInLeapYear(t) + // = 4 if 120 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 151 + MathematicalInLeapYear(t) + // = 5 if 151 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 181 + MathematicalInLeapYear(t) + // = 6 if 181 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 212 + MathematicalInLeapYear(t) + // = 7 if 212 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 243 + MathematicalInLeapYear(t) + // = 8 if 243 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 273 + MathematicalInLeapYear(t) + // = 9 if 273 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 304 + MathematicalInLeapYear(t) + // = 10 if 304 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 334 + MathematicalInLeapYear(t) + // = 11 if 334 + MathematicalInLeapYear(t) ≤ EpochTimeToDayInYear(t) < 365 + MathematicalInLeapYear(t) + if (day_in_year < 31) + return 0; + if (day_in_year >= 31 && day_in_year < 59 + in_leap_year) + return 1; + if (day_in_year >= 59 + in_leap_year && day_in_year < 90 + in_leap_year) + return 2; + if (day_in_year >= 90 + in_leap_year && day_in_year < 120 + in_leap_year) + return 3; + if (day_in_year >= 120 + in_leap_year && day_in_year < 151 + in_leap_year) + return 4; + if (day_in_year >= 151 + in_leap_year && day_in_year < 181 + in_leap_year) + return 5; + if (day_in_year >= 181 + in_leap_year && day_in_year < 212 + in_leap_year) + return 6; + if (day_in_year >= 212 + in_leap_year && day_in_year < 243 + in_leap_year) + return 7; + if (day_in_year >= 243 + in_leap_year && day_in_year < 273 + in_leap_year) + return 8; + if (day_in_year >= 273 + in_leap_year && day_in_year < 304 + in_leap_year) + return 9; + if (day_in_year >= 304 + in_leap_year && day_in_year < 334 + in_leap_year) + return 10; + if (day_in_year >= 334 + in_leap_year && day_in_year < 365 + in_leap_year) + return 11; + VERIFY_NOT_REACHED(); +} + // https://tc39.es/proposal-temporal/#eqn-epochtimetoweekday u8 epoch_time_to_week_day(double time) { @@ -86,4 +132,51 @@ u8 epoch_time_to_week_day(double time) return static_cast(modulo(epoch_time_to_day_number(time) + 4, 7.0)); } +// https://tc39.es/proposal-temporal/#eqn-epochtimetodate +u8 epoch_time_to_date(double time) +{ + auto day_in_year = epoch_time_to_day_in_year(time); + auto month_in_year = epoch_time_to_month_in_year(time); + auto in_leap_year = mathematical_in_leap_year(time); + + // EpochTimeToDate(t) + // = EpochTimeToDayInYear(t) + 1 if EpochTimeToMonthInYear(t) = 0 + // = EpochTimeToDayInYear(t) - 30 if EpochTimeToMonthInYear(t) = 1 + // = EpochTimeToDayInYear(t) - 58 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 2 + // = EpochTimeToDayInYear(t) - 89 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 3 + // = EpochTimeToDayInYear(t) - 119 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 4 + // = EpochTimeToDayInYear(t) - 150 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 5 + // = EpochTimeToDayInYear(t) - 180 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 6 + // = EpochTimeToDayInYear(t) - 211 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 7 + // = EpochTimeToDayInYear(t) - 242 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 8 + // = EpochTimeToDayInYear(t) - 272 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 9 + // = EpochTimeToDayInYear(t) - 303 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 10 + // = EpochTimeToDayInYear(t) - 333 - MathematicalInLeapYear(t) if EpochTimeToMonthInYear(t) = 11 + if (month_in_year == 0) + return day_in_year + 1; + if (month_in_year == 1) + return day_in_year - 30; + if (month_in_year == 2) + return day_in_year - 58 - in_leap_year; + if (month_in_year == 3) + return day_in_year - 89 - in_leap_year; + if (month_in_year == 4) + return day_in_year - 119 - in_leap_year; + if (month_in_year == 5) + return day_in_year - 150 - in_leap_year; + if (month_in_year == 6) + return day_in_year - 180 - in_leap_year; + if (month_in_year == 7) + return day_in_year - 211 - in_leap_year; + if (month_in_year == 8) + return day_in_year - 242 - in_leap_year; + if (month_in_year == 9) + return day_in_year - 272 - in_leap_year; + if (month_in_year == 10) + return day_in_year - 303 - in_leap_year; + if (month_in_year == 11) + return day_in_year - 333 - in_leap_year; + VERIFY_NOT_REACHED(); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/DateEquations.h b/Libraries/LibJS/Runtime/Temporal/DateEquations.h index 0dcd60330149..659691830fd4 100644 --- a/Libraries/LibJS/Runtime/Temporal/DateEquations.h +++ b/Libraries/LibJS/Runtime/Temporal/DateEquations.h @@ -19,6 +19,8 @@ double epoch_day_number_for_year(double year); double epoch_time_for_year(double year); i32 epoch_time_to_epoch_year(double time); u16 epoch_time_to_day_in_year(double time); +u8 epoch_time_to_month_in_year(double time); u8 epoch_time_to_week_day(double time); +u8 epoch_time_to_date(double time); } diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 31f6d28a54ed..03b35d95fff7 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -9,13 +9,16 @@ #include #include -#include #include +#include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -294,6 +297,21 @@ ThrowCompletionOr create_date_duration_record(VM& vm, double years return DateDuration { years, months, weeks, days }; } +// 7.5.10 AdjustDateDurationRecord ( dateDuration, days [ , weeks [ , months ] ] ), https://tc39.es/proposal-temporal/#sec-temporal-adjustdatedurationrecord +ThrowCompletionOr adjust_date_duration_record(VM& vm, DateDuration const& date_duration, double days, Optional weeks, Optional months) +{ + // 1. If weeks is not present, set weeks to dateDuration.[[Weeks]]. + if (!weeks.has_value()) + weeks = date_duration.weeks; + + // 2. If months is not present, set months to dateDuration.[[Months]]. + if (!months.has_value()) + months = date_duration.months; + + // 3. Return ? CreateDateDurationRecord(dateDuration.[[Years]], months, weeks, days). + return TRY(create_date_duration_record(vm, date_duration.years, *months, *weeks, days)); +} + // 7.5.11 CombineDateAndTimeDuration ( dateDuration, timeDuration ), https://tc39.es/proposal-temporal/#sec-temporal-combinedateandtimeduration ThrowCompletionOr combine_date_and_time_duration(VM& vm, DateDuration date_duration, TimeDuration time_duration) { @@ -418,6 +436,20 @@ i8 date_duration_sign(DateDuration const& date_duration) return 0; } +// 7.5.15 InternalDurationSign ( internalDuration ), https://tc39.es/proposal-temporal/#sec-temporal-internaldurationsign +i8 internal_duration_sign(InternalDuration const& internal_duration) +{ + // 1. Let dateSign be DateDurationSign(internalDuration.[[Date]]). + auto date_sign = date_duration_sign(internal_duration.date); + + // 2. If dateSign ≠ 0, return dateSign. + if (date_sign != 0) + return date_sign; + + // 3. Return TimeDurationSign(internalDuration.[[Time]]). + return time_duration_sign(internal_duration.time); +} + // 7.5.16 IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidduration bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds) { @@ -687,6 +719,13 @@ ThrowCompletionOr add_24_hour_days_to_time_duration(VM& vm, TimeDu return result; } +// 7.5.24 AddTimeDurationToEpochNanoseconds ( d, epochNs ), https://tc39.es/proposal-temporal/#sec-temporal-addtimedurationtoepochnanoseconds +TimeDuration add_time_duration_to_epoch_nanoseconds(TimeDuration const& duration, TimeDuration const& epoch_nanoseconds) +{ + // 1. Return epochNs + ℤ(d). + return epoch_nanoseconds.plus(duration); +} + // 7.5.25 CompareTimeDuration ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-comparetimeduration i8 compare_time_duration(TimeDuration const& one, TimeDuration const& two) { @@ -702,6 +741,19 @@ i8 compare_time_duration(TimeDuration const& one, TimeDuration const& two) return 0; } +// 7.5.26 TimeDurationFromEpochNanosecondsDifference ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-timedurationfromepochnanosecondsdifference +TimeDuration time_duration_from_epoch_nanoseconds_difference(TimeDuration const& one, TimeDuration const& two) +{ + // 1. Let result be ℝ(one) - ℝ(two). + auto result = one.minus(two); + + // 2. Assert: abs(result) ≤ maxTimeDuration. + VERIFY(result.unsigned_value() <= MAX_TIME_DURATION.unsigned_value()); + + // 3. Return result. + return result; +} + // 7.5.27 RoundTimeDurationToIncrement ( d, increment, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtimedurationtoincrement ThrowCompletionOr round_time_duration_to_increment(VM& vm, TimeDuration const& duration, Crypto::UnsignedBigInteger const& increment, RoundingMode rounding_mode) { @@ -756,6 +808,532 @@ double total_time_duration(TimeDuration const& time_duration, Unit unit) return result.to_double(); } +// 7.5.33 NudgeToCalendarUnit ( sign, duration, destEpochNs, isoDateTime, timeZone, calendar, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-nudgetocalendarunit +ThrowCompletionOr nudge_to_calendar_unit(VM& vm, i8 sign, InternalDuration const& duration, TimeDuration const& dest_epoch_ns, ISODateTime const& iso_date_time, Optional time_zone, StringView calendar, u64 increment, Unit unit, RoundingMode rounding_mode) +{ + DateDuration start_duration; + DateDuration end_duration; + + double r1 = 0; + double r2 = 0; + + // 1. If unit is YEAR, then + if (unit == Unit::Year) { + // a. Let years be RoundNumberToIncrement(duration.[[Date]].[[Years]], increment, TRUNC). + auto years = round_number_to_increment(duration.date.years, increment, RoundingMode::Trunc); + + // b. Let r1 be years. + r1 = years; + + // c. Let r2 be years + increment × sign. + r2 = years + static_cast(increment) * sign; + + // d. Let startDuration be ? CreateDateDurationRecord(r1, 0, 0, 0). + start_duration = TRY(create_date_duration_record(vm, r1, 0, 0, 0)); + + // e. Let endDuration be ? CreateDateDurationRecord(r2, 0, 0, 0). + end_duration = TRY(create_date_duration_record(vm, r2, 0, 0, 0)); + } + // 2. Else if unit is MONTH, then + else if (unit == Unit::Month) { + // a. Let months be RoundNumberToIncrement(duration.[[Date]].[[Months]], increment, TRUNC). + auto months = round_number_to_increment(duration.date.months, increment, RoundingMode::Trunc); + + // b. Let r1 be months. + r1 = months; + + // c. Let r2 be months + increment × sign. + r2 = months + static_cast(increment) * sign; + + // d. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, r1). + start_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, 0, r1)); + + // e. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, r2). + end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, 0, r2)); + } + // 3. Else if unit is WEEK, then + else if (unit == Unit::Week) { + // a. Let yearsMonths be ! AdjustDateDurationRecord(duration.[[Date]], 0, 0). + auto years_months = MUST(adjust_date_duration_record(vm, duration.date, 0, 0)); + + // b. Let weeksStart be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], yearsMonths, CONSTRAIN). + auto weeks_start = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, years_months, Overflow::Constrain)); + + // c. Let weeksEnd be BalanceISODate(weeksStart.[[Year]], weeksStart.[[Month]], weeksStart.[[Day]] + duration.[[Date]].[[Days]]). + auto weeks_end = balance_iso_date(weeks_start.year, weeks_start.month, static_cast(weeks_start.day) + duration.date.days); + + // d. Let untilResult be CalendarDateUntil(calendar, weeksStart, weeksEnd, WEEK). + auto until_result = calendar_date_until(vm, calendar, weeks_start, weeks_end, Unit::Week); + + // e. Let weeks be RoundNumberToIncrement(duration.[[Date]].[[Weeks]] + untilResult.[[Weeks]], increment, TRUNC). + auto weeks = round_number_to_increment(duration.date.weeks + until_result.weeks, increment, RoundingMode::Trunc); + + // f. Let r1 be weeks. + r1 = weeks; + + // g. Let r2 be weeks + increment × sign. + r2 = weeks + static_cast(increment) * sign; + + // h. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, r1). + start_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, r1)); + + // i. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, r2). + end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, r2)); + } + // 4. Else, + else { + // a. Assert: unit is DAY. + VERIFY(unit == Unit::Day); + + // b. Let days be RoundNumberToIncrement(duration.[[Date]].[[Days]], increment, TRUNC). + auto days = round_number_to_increment(duration.date.days, increment, RoundingMode::Trunc); + + // c. Let r1 be days. + r1 = days; + + // d. Let r2 be days + increment × sign. + r2 = days + static_cast(increment) * sign; + + // e. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], r1). + start_duration = TRY(adjust_date_duration_record(vm, duration.date, r1)); + + // f. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], r2). + end_duration = TRY(adjust_date_duration_record(vm, duration.date, r2)); + } + + // 5. Assert: If sign is 1, r1 ≥ 0 and r1 < r2. + if (sign == 1) + VERIFY(r1 >= 0 && r1 < r2); + // 6. Assert: If sign is -1, r1 ≤ 0 and r1 > r2. + else if (sign == -1) + VERIFY(r1 <= 0 && r1 > r2); + + // 7. Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], startDuration, CONSTRAIN). + auto start = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, start_duration, Overflow::Constrain)); + + // 8. Let end be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, CONSTRAIN). + auto end = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, end_duration, Overflow::Constrain)); + + // 9. Let startDateTime be CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]). + auto start_date_time = combine_iso_date_and_time_record(start, iso_date_time.time); + + // 10. Let endDateTime be CombineISODateAndTimeRecord(end, isoDateTime.[[Time]]). + auto end_date_time = combine_iso_date_and_time_record(end, iso_date_time.time); + + TimeDuration start_epoch_ns; + TimeDuration end_epoch_ns; + + // 11. If timeZone is UNSET, then + if (!time_zone.has_value()) { + // a. Let startEpochNs be GetUTCEpochNanoseconds(startDateTime). + start_epoch_ns = get_utc_epoch_nanoseconds(start_date_time); + + // b. Let endEpochNs be GetUTCEpochNanoseconds(endDateTime). + end_epoch_ns = get_utc_epoch_nanoseconds(end_date_time); + } + // 12. Else, + else { + // a. Let startEpochNs be ? GetEpochNanosecondsFor(timeZone, startDateTime, COMPATIBLE). + start_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, *time_zone, start_date_time, Disambiguation::Compatible)); + + // b. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, COMPATIBLE). + end_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, *time_zone, end_date_time, Disambiguation::Compatible)); + } + + // 13. If sign is 1, then + if (sign == 1) { + // a. Assert: startEpochNs ≤ destEpochNs ≤ endEpochNs. + VERIFY(start_epoch_ns <= dest_epoch_ns); + VERIFY(dest_epoch_ns <= end_epoch_ns); + } + // 14. Else, + else { + // a. Assert: endEpochNs ≤ destEpochNs ≤ startEpochNs. + VERIFY(end_epoch_ns <= dest_epoch_ns); + VERIFY(dest_epoch_ns <= start_epoch_ns); + } + + // 15. Assert: startEpochNs ≠ endEpochNs. + VERIFY(start_epoch_ns != end_epoch_ns); + + // 16. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs). + auto progress_numerator = dest_epoch_ns.minus(start_epoch_ns); + auto progress_denominator = end_epoch_ns.minus(start_epoch_ns); + auto progress_equals_one = progress_numerator == progress_denominator; + + // 17. Let total be r1 + progress × increment × sign. + auto total_numerator = progress_numerator.multiplied_by(Crypto::UnsignedBigInteger { increment }); + + if (sign == -1) + total_numerator.negate(); + if (progress_denominator.is_negative()) + total_numerator.negate(); + + auto total_mv = Crypto::BigFraction { Crypto::SignedBigInteger { r1 } } + Crypto::BigFraction { move(total_numerator), progress_denominator.unsigned_value() }; + auto total = total_mv.to_double(); + + // 18. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic. This division can be + // implemented as if expressing the denominator and numerator of total as two time durations, and performing one + // division operation with a floating-point result. + + // 19. Assert: 0 ≤ progress ≤ 1. + + // 20. If sign < 0, let isNegative be NEGATIVE; else let isNegative be POSITIVE. + auto is_negative = sign < 0 ? Sign::Negative : Sign::Positive; + + // 21. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). + auto unsigned_rounding_mode = get_unsigned_rounding_mode(rounding_mode, is_negative); + + double rounded_unit = 0; + + // 22. If progress = 1, then + if (progress_equals_one) { + // a. Let roundedUnit be abs(r2). + rounded_unit = fabs(r2); + } + // 23. Else, + else { + // a. Assert: abs(r1) ≤ abs(total) < abs(r2). + VERIFY(fabs(r1) <= fabs(total)); + VERIFY(fabs(total) <= fabs(r2)); + + // b. Let roundedUnit be ApplyUnsignedRoundingMode(abs(total), abs(r1), abs(r2), unsignedRoundingMode). + rounded_unit = apply_unsigned_rounding_mode(fabs(total), fabs(r1), fabs(r2), unsigned_rounding_mode); + } + + auto did_expand_calendar_unit = false; + DateDuration result_duration; + TimeDuration nudged_epoch_ns; + + // 24. If roundedUnit is abs(r2), then + if (rounded_unit == fabs(r2)) { + // a. Let didExpandCalendarUnit be true. + did_expand_calendar_unit = true; + + // b. Let resultDuration be endDuration. + result_duration = end_duration; + + // c. Let nudgedEpochNs be endEpochNs. + nudged_epoch_ns = move(end_epoch_ns); + } + // 25. Else, + else { + // a. Let didExpandCalendarUnit be false. + did_expand_calendar_unit = false; + + // b. Let resultDuration be startDuration. + result_duration = start_duration; + + // c. Let nudgedEpochNs be startEpochNs. + nudged_epoch_ns = move(start_epoch_ns); + } + + // 26. Set resultDuration to ! CombineDateAndTimeDuration(resultDuration, 0). + auto result_date_and_time_duration = MUST(combine_date_and_time_duration(vm, result_duration, TimeDuration { 0 })); + + // 27. Let nudgeResult be Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandCalendarUnit }. + DurationNudgeResult nudge_result { .duration = move(result_date_and_time_duration), .nudged_epoch_ns = move(nudged_epoch_ns), .did_expand_calendar_unit = did_expand_calendar_unit }; + + // 28. Return the Record { [[NudgeResult]]: nudgeResult, [[Total]]: total }. + return CalendarNudgeResult { .nudge_result = move(nudge_result), .total = move(total_mv) }; +} + +// 7.5.34 NudgeToZonedTime ( sign, duration, isoDateTime, timeZone, calendar, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-nudgetozonedtime +ThrowCompletionOr nudge_to_zoned_time(VM& vm, i8 sign, InternalDuration const& duration, ISODateTime const& iso_date_time, StringView time_zone, StringView calendar, u64 increment, Unit unit, RoundingMode rounding_mode) +{ + // 1. Let start be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], duration.[[Date]], CONSTRAIN). + auto start = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, duration.date, Overflow::Constrain)); + + // 2. Let startDateTime be CombineISODateAndTimeRecord(start, isoDateTime.[[Time]]). + auto start_date_time = combine_iso_date_and_time_record(start, iso_date_time.time); + + // 3. Let endDate be BalanceISODate(start.[[Year]], start.[[Month]], start.[[Day]] + sign). + auto end_date = balance_iso_date(start.year, start.month, static_cast(start.day) + sign); + + // 4. Let endDateTime be CombineISODateAndTimeRecord(endDate, isoDateTime.[[Time]]). + auto end_date_time = combine_iso_date_and_time_record(end_date, iso_date_time.time); + + // 5. Let startEpochNs be ? GetEpochNanosecondsFor(timeZone, startDateTime, COMPATIBLE). + auto start_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, time_zone, start_date_time, Disambiguation::Compatible)); + + // 6. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, COMPATIBLE). + auto end_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, time_zone, end_date_time, Disambiguation::Compatible)); + + // 7. Let daySpan be TimeDurationFromEpochNanosecondsDifference(endEpochNs, startEpochNs). + auto day_span = time_duration_from_epoch_nanoseconds_difference(end_epoch_ns, start_epoch_ns); + + // 8. Assert: TimeDurationSign(daySpan) = sign. + VERIFY(time_duration_sign(day_span) == sign); + + // 9. Let unitLength be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains unit. + auto const& unit_length = temporal_unit_length_in_nanoseconds(unit); + + // 10. Let roundedTimeDuration be ? RoundTimeDurationToIncrement(duration.[[Time]], increment × unitLength, roundingMode). + auto unit_length_multiplied_by_increment = unit_length.multiplied_by(Crypto::UnsignedBigInteger { increment }); + auto rounded_time_duration = TRY(round_time_duration_to_increment(vm, duration.time, unit_length_multiplied_by_increment, rounding_mode)); + + // 11. Let beyondDaySpan be ? AddTimeDuration(roundedTimeDuration, -daySpan). + day_span.negate(); + auto beyond_day_span = TRY(add_time_duration(vm, rounded_time_duration, day_span)); + + auto did_round_beyond_day = false; + TimeDuration nudged_epoch_ns; + i8 day_delta = 0; + + // 12. If TimeDurationSign(beyondDaySpan) ≠ -sign, then + if (time_duration_sign(beyond_day_span) != -sign) { + // a. Let didRoundBeyondDay be true. + did_round_beyond_day = true; + + // b. Let dayDelta be sign. + day_delta = sign; + + // c. Set roundedTimeDuration to ? RoundTimeDurationToIncrement(beyondDaySpan, increment × unitLength, roundingMode). + rounded_time_duration = TRY(round_time_duration_to_increment(vm, beyond_day_span, unit_length_multiplied_by_increment, rounding_mode)); + + // d. Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(roundedTimeDuration, endEpochNs). + nudged_epoch_ns = add_time_duration_to_epoch_nanoseconds(rounded_time_duration, end_epoch_ns); + } + // 13. Else, + else { + // a. Let didRoundBeyondDay be false. + did_round_beyond_day = false; + + // b. Let dayDelta be 0. + day_delta = 0; + + // c. Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(roundedTimeDuration, startEpochNs). + nudged_epoch_ns = add_time_duration_to_epoch_nanoseconds(rounded_time_duration, start_epoch_ns); + } + + // 14. Let dateDuration be ? AdjustDateDurationRecord(duration.[[Date]], duration.[[Date]].[[Days]] + dayDelta). + auto date_duration = TRY(adjust_date_duration_record(vm, duration.date, duration.date.days + day_delta)); + + // 15. Let resultDuration be ? CombineDateAndTimeDuration(dateDuration, roundedTimeDuration). + auto result_duration = TRY(combine_date_and_time_duration(vm, date_duration, move(rounded_time_duration))); + + // 16. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didRoundBeyondDay }. + return DurationNudgeResult { .duration = move(result_duration), .nudged_epoch_ns = move(nudged_epoch_ns), .did_expand_calendar_unit = did_round_beyond_day }; +} + +// 7.5.35 NudgeToDayOrTime ( duration, destEpochNs, largestUnit, increment, smallestUnit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-nudgetodayortime +ThrowCompletionOr nudge_to_day_or_time(VM& vm, InternalDuration const& duration, TimeDuration const& dest_epoch_ns, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode rounding_mode) +{ + // 1. Let timeDuration be ! Add24HourDaysToTimeDuration(duration.[[Time]], duration.[[Date]].[[Days]]). + auto time_duration = MUST(add_24_hour_days_to_time_duration(vm, duration.time, duration.date.days)); + + // 2. Let unitLength be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains smallestUnit. + auto const& unit_length = temporal_unit_length_in_nanoseconds(smallest_unit); + + // 3. Let roundedTime be ? RoundTimeDurationToIncrement(timeDuration, unitLength × increment, roundingMode). + auto unit_length_multiplied_by_increment = unit_length.multiplied_by(Crypto::UnsignedBigInteger { increment }); + auto rounded_time = TRY(round_time_duration_to_increment(vm, time_duration, unit_length_multiplied_by_increment, rounding_mode)); + + // 4. Let diffTime be ! AddTimeDuration(roundedTime, -timeDuration). + time_duration.negate(); + auto diff_time = MUST(add_time_duration(vm, rounded_time, time_duration)); + time_duration.negate(); + + // 5. Let wholeDays be truncate(TotalTimeDuration(timeDuration, DAY)). + auto whole_days = trunc(total_time_duration(time_duration, Unit::Day)); + + // 6. Let roundedWholeDays be truncate(TotalTimeDuration(roundedTime, DAY)). + auto rounded_whole_days = trunc(total_time_duration(rounded_time, Unit::Day)); + + // 7. Let dayDelta be roundedWholeDays - wholeDays. + auto day_delta = rounded_whole_days - whole_days; + + // 8. If dayDelta < 0, let dayDeltaSign be -1; else if dayDelta > 0, let dayDeltaSign be 1; else let dayDeltaSign be 0. + auto day_delta_sign = day_delta < 0 ? -1 : (day_delta > 0 ? 1 : 0); + + // 9. If dayDeltaSign = TimeDurationSign(timeDuration), let didExpandDays be true; else let didExpandDays be false. + auto did_expand_days = day_delta_sign == time_duration_sign(time_duration); + + // 10. Let nudgedEpochNs be AddTimeDurationToEpochNanoseconds(diffTime, destEpochNs). + auto nudged_epoch_ns = add_time_duration_to_epoch_nanoseconds(diff_time, dest_epoch_ns); + + // 11. Let days be 0. + double days = 0; + + // 12. Let remainder be roundedTime. + TimeDuration remainder; + + // 13. If TemporalUnitCategory(largestUnit) is DATE, then + if (temporal_unit_category(largest_unit) == UnitCategory::Date) { + // a. Set days to roundedWholeDays. + days = rounded_whole_days; + + // b. Set remainder to ! AddTimeDuration(roundedTime, TimeDurationFromComponents(-roundedWholeDays * HoursPerDay, 0, 0, 0, 0, 0)). + remainder = MUST(add_time_duration(vm, rounded_time, time_duration_from_components(-rounded_whole_days * JS::hours_per_day, 0, 0, 0, 0, 0))); + } else { + remainder = move(rounded_time); + } + + // 14. Let dateDuration be ? AdjustDateDurationRecord(duration.[[Date]], days). + auto date_duration = TRY(adjust_date_duration_record(vm, duration.date, days)); + + // 15. Let resultDuration be ? CombineDateAndTimeDuration(dateDuration, remainder). + auto result_duration = TRY(combine_date_and_time_duration(vm, date_duration, move(remainder))); + + // 16. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandDays }. + return DurationNudgeResult { .duration = move(result_duration), .nudged_epoch_ns = move(nudged_epoch_ns), .did_expand_calendar_unit = did_expand_days }; +} + +// 7.5.36 BubbleRelativeDuration ( sign, duration, nudgedEpochNs, isoDateTime, timeZone, calendar, largestUnit, smallestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-bubblerelativeduration +ThrowCompletionOr bubble_relative_duration(VM& vm, i8 sign, InternalDuration duration, TimeDuration const& nudged_epoch_ns, ISODateTime const& iso_date_time, Optional time_zone, StringView calendar, Unit largest_unit, Unit smallest_unit) +{ + // 1. If smallestUnit is largestUnit, return duration. + if (smallest_unit == largest_unit) + return duration; + + // 2. Let largestUnitIndex be the ordinal index of the row of Table 21 whose "Value" column contains largestUnit. + auto largest_unit_index = to_underlying(largest_unit); + + // 3. Let smallestUnitIndex be the ordinal index of the row of Table 21 whose "Value" column contains smallestUnit. + auto smallest_unit_index = to_underlying(smallest_unit); + + // 4. Let unitIndex be smallestUnitIndex - 1. + auto unit_index = smallest_unit_index - 1; + + // 5. Let done be false. + auto done = false; + + // 6. Repeat, while unitIndex ≥ largestUnitIndex and done is false, + while (unit_index >= largest_unit_index && !done) { + // a. Let unit be the value in the "Value" column of Table 21 in the row whose ordinal index is unitIndex. + auto unit = static_cast(unit_index); + + // b. If unit is not WEEK, or largestUnit is WEEK, then + if (unit != Unit::Week || largest_unit == Unit::Week) { + DateDuration end_duration; + + // i. If unit is YEAR, then + if (unit == Unit::Year) { + // 1. Let years be duration.[[Date]].[[Years]] + sign. + auto years = duration.date.years + sign; + + // 2. Let endDuration be ? CreateDateDurationRecord(years, 0, 0, 0). + end_duration = TRY(create_date_duration_record(vm, years, 0, 0, 0)); + } + // ii. Else if unit is MONTH, then + else if (unit == Unit::Month) { + // 1. Let months be duration.[[Date]].[[Months]] + sign. + auto months = duration.date.months + sign; + + // 2. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, months). + end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, 0, months)); + } + // iii. Else, + else { + // 1. Assert: unit is WEEK. + VERIFY(unit == Unit::Week); + + // 2. Let weeks be duration.[[Date]].[[Weeks]] + sign. + auto weeks = duration.date.weeks + sign; + + // 3. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, weeks). + end_duration = TRY(adjust_date_duration_record(vm, duration.date, 0, weeks)); + } + + // iv. Let end be ? CalendarDateAdd(calendar, isoDateTime.[[ISODate]], endDuration, CONSTRAIN). + auto end = TRY(calendar_date_add(vm, calendar, iso_date_time.iso_date, end_duration, Overflow::Constrain)); + + // v. Let endDateTime be CombineISODateAndTimeRecord(end, isoDateTime.[[Time]]). + auto end_date_time = combine_iso_date_and_time_record(end, iso_date_time.time); + + TimeDuration end_epoch_ns; + + // vi. If timeZone is UNSET, then + if (!time_zone.has_value()) { + // 1. Let endEpochNs be GetUTCEpochNanoseconds(endDateTime). + end_epoch_ns = get_utc_epoch_nanoseconds(end_date_time); + } + // vii. Else, + else { + // 1. Let endEpochNs be ? GetEpochNanosecondsFor(timeZone, endDateTime, COMPATIBLE). + end_epoch_ns = TRY(get_epoch_nanoseconds_for(vm, *time_zone, end_date_time, Disambiguation::Compatible)); + } + + // viii. Let beyondEnd be nudgedEpochNs - endEpochNs. + auto beyond_end = nudged_epoch_ns.minus(end_epoch_ns); + + // ix. If beyondEnd < 0, let beyondEndSign be -1; else if beyondEnd > 0, let beyondEndSign be 1; else let beyondEndSign be 0. + auto beyond_end_sign = beyond_end.is_negative() ? -1 : (beyond_end.is_positive() ? 1 : 0); + + // x. If beyondEndSign ≠ -sign, then + if (beyond_end_sign != -sign) { + // 1. Set duration to ! CombineDateAndTimeDuration(endDuration, 0). + duration = MUST(combine_date_and_time_duration(vm, end_duration, TimeDuration { 0 })); + } + // xi. Else, + else { + // 1. Set done to true. + done = true; + } + } + + // c. Set unitIndex to unitIndex - 1. + --unit_index; + } + + // 7. Return duration. + return duration; +} + +// 7.5.37 RoundRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone, calendar, largestUnit, increment, smallestUnit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundrelativeduration +ThrowCompletionOr round_relative_duration(VM& vm, InternalDuration duration, TimeDuration const& dest_epoch_ns, ISODateTime const& iso_date_time, Optional time_zone, StringView calendar, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode rounding_mode) +{ + // 1. Let irregularLengthUnit be false. + auto irregular_length_unit = false; + + // 2. If IsCalendarUnit(smallestUnit) is true, set irregularLengthUnit to true. + if (is_calendar_unit(smallest_unit)) + irregular_length_unit = true; + + // 3. If timeZone is not UNSET and smallestUnit is DAY, set irregularLengthUnit to true. + if (time_zone.has_value() && smallest_unit == Unit::Day) + irregular_length_unit = true; + + // 4. If InternalDurationSign(duration) < 0, let sign be -1; else let sign be 1. + i8 sign = internal_duration_sign(duration) < 0 ? -1 : 1; + + DurationNudgeResult nudge_result; + + // 5. If irregularLengthUnit is true, then + if (irregular_length_unit) { + // a. Let record be ? NudgeToCalendarUnit(sign, duration, destEpochNs, isoDateTime, timeZone, calendar, increment, smallestUnit, roundingMode). + auto record = TRY(nudge_to_calendar_unit(vm, sign, duration, dest_epoch_ns, iso_date_time, time_zone, calendar, increment, smallest_unit, rounding_mode)); + + // b. Let nudgeResult be record.[[NudgeResult]]. + nudge_result = move(record.nudge_result); + } + // 6. Else if timeZone is not UNSET, then + else if (time_zone.has_value()) { + // a. Let nudgeResult be ? NudgeToZonedTime(sign, duration, isoDateTime, timeZone, calendar, increment, smallestUnit, roundingMode). + nudge_result = TRY(nudge_to_zoned_time(vm, sign, duration, iso_date_time, *time_zone, calendar, increment, smallest_unit, rounding_mode)); + } + // 7. Else, + else { + // a. Let nudgeResult be ? NudgeToDayOrTime(duration, destEpochNs, largestUnit, increment, smallestUnit, roundingMode). + nudge_result = TRY(nudge_to_day_or_time(vm, duration, dest_epoch_ns, largest_unit, increment, smallest_unit, rounding_mode)); + } + + // 8. Set duration to nudgeResult.[[Duration]]. + duration = move(nudge_result.duration); + + // 9. If nudgeResult.[[DidExpandCalendarUnit]] is true and smallestUnit is not WEEK, then + if (nudge_result.did_expand_calendar_unit && smallest_unit != Unit::Week) { + // a. Let startUnit be LargerOfTwoTemporalUnits(smallestUnit, DAY). + auto start_unit = larger_of_two_temporal_units(smallest_unit, Unit::Day); + + // b. Set duration to ? BubbleRelativeDuration(sign, duration, nudgeResult.[[NudgedEpochNs]], isoDateTime, timeZone, calendar, largestUnit, startUnit). + duration = TRY(bubble_relative_duration(vm, sign, move(duration), nudge_result.nudged_epoch_ns, iso_date_time, time_zone, calendar, largest_unit, start_unit)); + } + + // 10. Return duration. + return duration; +} + // 7.5.39 TemporalDurationToString ( duration, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring String temporal_duration_to_string(Duration const& duration, Precision precision) { diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.h b/Libraries/LibJS/Runtime/Temporal/Duration.h index 22d967d51555..ab3696e08aa7 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -9,6 +9,7 @@ #pragma once #include +#include #include #include #include @@ -100,15 +101,29 @@ struct InternalDuration { TimeDuration time; }; +// 7.5.32 Duration Nudge Result Records, https://tc39.es/proposal-temporal/#sec-temporal-duration-nudge-result-records +struct DurationNudgeResult { + InternalDuration duration; + TimeDuration nudged_epoch_ns; + bool did_expand_calendar_unit { false }; +}; + +struct CalendarNudgeResult { + DurationNudgeResult nudge_result; + Crypto::BigFraction total; +}; + DateDuration zero_date_duration(VM&); InternalDuration to_internal_duration_record(VM&, Duration const&); InternalDuration to_internal_duration_record_with_24_hour_days(VM&, Duration const&); ThrowCompletionOr> temporal_duration_from_internal(VM&, InternalDuration const&, Unit largest_unit); ThrowCompletionOr create_date_duration_record(VM&, double years, double months, double weeks, double days); +ThrowCompletionOr adjust_date_duration_record(VM&, DateDuration const&, double days, Optional weeks = {}, Optional months = {}); ThrowCompletionOr combine_date_and_time_duration(VM&, DateDuration, TimeDuration); ThrowCompletionOr> to_temporal_duration(VM&, Value); i8 duration_sign(Duration const&); i8 date_duration_sign(DateDuration const&); +i8 internal_duration_sign(InternalDuration const&); bool is_valid_duration(double years, double months, double weeks, double days, double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); Unit default_temporal_largest_unit(Duration const&); ThrowCompletionOr to_temporal_partial_duration_record(VM&, Value temporal_duration_like); @@ -117,10 +132,17 @@ GC::Ref create_negated_temporal_duration(VM&, Duration const&); TimeDuration time_duration_from_components(double hours, double minutes, double seconds, double milliseconds, double microseconds, double nanoseconds); ThrowCompletionOr add_time_duration(VM&, TimeDuration const&, TimeDuration const&); ThrowCompletionOr add_24_hour_days_to_time_duration(VM&, TimeDuration const&, double days); +TimeDuration add_time_duration_to_epoch_nanoseconds(TimeDuration const& duration, TimeDuration const& epoch_nanoseconds); i8 compare_time_duration(TimeDuration const&, TimeDuration const&); +TimeDuration time_duration_from_epoch_nanoseconds_difference(TimeDuration const&, TimeDuration const&); ThrowCompletionOr round_time_duration_to_increment(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, RoundingMode); i8 time_duration_sign(TimeDuration const&); ThrowCompletionOr round_time_duration(VM&, TimeDuration const&, Crypto::UnsignedBigInteger const& increment, Unit, RoundingMode); +ThrowCompletionOr nudge_to_calendar_unit(VM&, i8 sign, InternalDuration const&, TimeDuration const& dest_epoch_ns, ISODateTime const&, Optional time_zone, StringView calendar, u64 increment, Unit, RoundingMode); +ThrowCompletionOr nudge_to_zoned_time(VM&, i8 sign, InternalDuration const&, ISODateTime const&, StringView time_zone, StringView calendar, u64 increment, Unit, RoundingMode); +ThrowCompletionOr nudge_to_day_or_time(VM&, InternalDuration const&, TimeDuration const& dest_epoch_ns, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode); +ThrowCompletionOr bubble_relative_duration(VM&, i8 sign, InternalDuration, TimeDuration const& nudged_epoch_ns, ISODateTime const&, Optional time_zone, StringView calendar, Unit largest_unit, Unit smallest_unit); +ThrowCompletionOr round_relative_duration(VM&, InternalDuration, TimeDuration const& dest_epoch_ns, ISODateTime const&, Optional time_zone, StringView calendar, Unit largest_unit, u64 increment, Unit smallest_unit, RoundingMode); double total_time_duration(TimeDuration const&, Unit); String temporal_duration_to_string(Duration const&, Precision); ThrowCompletionOr> add_durations(VM&, ArithmeticOperation, Duration const&, Value); diff --git a/Libraries/LibJS/Runtime/Temporal/Instant.cpp b/Libraries/LibJS/Runtime/Temporal/Instant.cpp index 5a23ec90287c..80c31945668d 100644 --- a/Libraries/LibJS/Runtime/Temporal/Instant.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Instant.cpp @@ -34,4 +34,17 @@ Crypto::UnsignedBigInteger const SECONDS_PER_MINUTE = 60_bigint; Crypto::UnsignedBigInteger const MINUTES_PER_HOUR = 60_bigint; Crypto::UnsignedBigInteger const HOURS_PER_DAY = 24_bigint; +// 8.5.1 IsValidEpochNanoseconds ( epochNanoseconds ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidepochnanoseconds +bool is_valid_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanoseconds) +{ + // 1. If ℝ(epochNanoseconds) < nsMinInstant or ℝ(epochNanoseconds) > nsMaxInstant, then + if (epoch_nanoseconds < NANOSECONDS_MIN_INSTANT || epoch_nanoseconds > NANOSECONDS_MAX_INSTANT) { + // a. Return false. + return false; + } + + // 2. Return true. + return true; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/Instant.h b/Libraries/LibJS/Runtime/Temporal/Instant.h index 9c90733e5c49..fc61ea048d4d 100644 --- a/Libraries/LibJS/Runtime/Temporal/Instant.h +++ b/Libraries/LibJS/Runtime/Temporal/Instant.h @@ -36,4 +36,6 @@ extern Crypto::UnsignedBigInteger const SECONDS_PER_MINUTE; extern Crypto::UnsignedBigInteger const MINUTES_PER_HOUR; extern Crypto::UnsignedBigInteger const HOURS_PER_DAY; +bool is_valid_epoch_nanoseconds(Crypto::SignedBigInteger const& epoch_nanoseconds); + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp index cb87ee86f798..09d3ef01f272 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,32 @@ ISODate create_iso_date_record(double year, double month, double day) return { .year = static_cast(year), .month = static_cast(month), .day = static_cast(day) }; } +// 3.5.5 ISODateSurpasses ( sign, y1, m1, d1, isoDate2 ), https://tc39.es/proposal-temporal/#sec-temporal-isodatesurpasses +bool iso_date_surpasses(i8 sign, double year1, double month1, double day1, ISODate iso_date2) +{ + // 1. If y1 ≠ isoDate2.[[Year]], then + if (year1 != iso_date2.year) { + // a. If sign × (y1 - isoDate2.[[Year]]) > 0, return true. + if (sign * (year1 - iso_date2.year) > 0) + return true; + } + // 2. Else if m1 ≠ isoDate2.[[Month]], then + else if (month1 != iso_date2.month) { + // a. If sign × (m1 - isoDate2.[[Month]]) > 0, return true. + if (sign * (month1 - iso_date2.month) > 0) + return true; + } + // 3. Else if d1 ≠ isoDate2.[[Day]], then + else if (day1 != iso_date2.day) { + // a. If sign × (d1 - isoDate2.[[Day]]) > 0, return true. + if (sign * (day1 - iso_date2.day) > 0) + return true; + } + + // 4. Return false. + return false; +} + // 3.5.6 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow) { @@ -86,6 +113,19 @@ bool is_valid_iso_date(double year, double month, double day) return true; } +// 3.5.8 BalanceISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisodate +ISODate balance_iso_date(double year, double month, double day) +{ + // 1. Let epochDays be ISODateToEpochDays(year, month - 1, day). + auto epoch_days = iso_date_to_epoch_days(year, month - 1, day); + + // 2. Let ms be EpochDaysToEpochMs(epochDays, 0). + auto ms = epoch_days_to_epoch_ms(epoch_days, 0); + + // 3. Return CreateISODateRecord(EpochTimeToEpochYear(ms), EpochTimeToMonthInYear(ms) + 1, EpochTimeToDate(ms)). + return create_iso_date_record(epoch_time_to_epoch_year(ms), epoch_time_to_month_in_year(ms) + 1.0, epoch_time_to_date(ms)); +} + // 3.5.9 PadISOYear ( y ), https://tc39.es/proposal-temporal/#sec-temporal-padisoyear String pad_iso_year(i32 year) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDate.h b/Libraries/LibJS/Runtime/Temporal/PlainDate.h index 7a235c9d7fa9..f4bb8067022a 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDate.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDate.h @@ -23,8 +23,10 @@ struct ISODate { }; ISODate create_iso_date_record(double year, double month, double day); +bool iso_date_surpasses(i8 sign, double year1, double month1, double day1, ISODate iso_date2); ThrowCompletionOr regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow); bool is_valid_iso_date(double year, double month, double day); +ISODate balance_iso_date(double year, double month, double day); String pad_iso_year(i32 year); bool iso_date_within_limits(ISODate); i8 compare_iso_date(ISODate, ISODate); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp index 84c21661e027..6e2467fa83f9 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp @@ -8,7 +8,9 @@ #include #include +#include #include +#include namespace JS::Temporal { @@ -52,4 +54,17 @@ bool iso_date_time_within_limits(ISODateTime iso_date_time) return true; } +// 5.5.7 BalanceISODateTime ( year, month, day, hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisodatetime +ISODateTime balance_iso_date_time(double year, double month, double day, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond) +{ + // 1. Let balancedTime be BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond). + auto balanced_time = balance_time(hour, minute, second, millisecond, microsecond, nanosecond); + + // 2. Let balancedDate be BalanceISODate(year, month, day + balancedTime.[[Days]]). + auto balanced_date = balance_iso_date(year, month, day + balanced_time.days); + + // 3. Return CombineISODateAndTimeRecord(balancedDate, balancedTime). + return combine_iso_date_and_time_record(balanced_date, balanced_time); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h index 31260cca3aeb..cea62f83237b 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainDateTime.h @@ -21,5 +21,6 @@ struct ISODateTime { ISODateTime combine_iso_date_and_time_record(ISODate, Time); bool iso_date_time_within_limits(ISODateTime); +ISODateTime balance_iso_date_time(double year, double month, double day, double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp index 85f6a39a3b09..6cc5fcff5c9b 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -7,7 +7,9 @@ */ #include +#include #include +#include namespace JS::Temporal { @@ -30,6 +32,13 @@ Time create_time_record(double hour, double minute, double second, double millis }; } +// 4.5.3 MidnightTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-midnighttimerecord +Time midnight_time_record() +{ + // 1. Return Time Record { [[Days]]: 0, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }. + return { .days = 0, .hour = 0, .minute = 0, .second = 0, .millisecond = 0, .microsecond = 0, .nanosecond = 0 }; +} + // 4.5.4 NoonTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-noontimerecord Time noon_time_record() { @@ -80,4 +89,47 @@ bool is_valid_time(double hour, double minute, double second, double millisecond return true; } +// 4.5.10 BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-balancetime +Time balance_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond) +{ + // 1. Set microsecond to microsecond + floor(nanosecond / 1000). + microsecond += floor(nanosecond / 1000.0); + + // 2. Set nanosecond to nanosecond modulo 1000. + nanosecond = modulo(nanosecond, 1000.0); + + // 3. Set millisecond to millisecond + floor(microsecond / 1000). + millisecond += floor(microsecond / 1000.0); + + // 4. Set microsecond to microsecond modulo 1000. + microsecond = modulo(microsecond, 1000.0); + + // 5. Set second to second + floor(millisecond / 1000). + second += floor(millisecond / 1000.0); + + // 6. Set millisecond to millisecond modulo 1000. + millisecond = modulo(millisecond, 1000.0); + + // 7. Set minute to minute + floor(second / 60). + minute += floor(second / 60.0); + + // 8. Set second to second modulo 60. + second = modulo(second, 60.0); + + // 9. Set hour to hour + floor(minute / 60). + hour += floor(minute / 60.0); + + // 10. Set minute to minute modulo 60. + minute = modulo(minute, 60.0); + + // 11. Let deltaDays be floor(hour / 24). + auto delta_days = floor(hour / 24.0); + + // 12. Set hour to hour modulo 24. + hour = modulo(hour, 24.0); + + // 13. Return CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond, deltaDays). + return create_time_record(hour, minute, second, millisecond, microsecond, nanosecond, delta_days); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Libraries/LibJS/Runtime/Temporal/PlainTime.h index 40eefc37a11c..eec0c0cb2a60 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.h @@ -24,7 +24,9 @@ struct Time { }; Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days = 0); +Time midnight_time_record(); Time noon_time_record(); bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); +Time balance_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp index f737d92a9492..292089d9175a 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -127,6 +130,19 @@ bool iso_year_month_within_limits(ISODate iso_date) return true; } +// 9.5.4 BalanceISOYearMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-balanceisoyearmonth +ISOYearMonth balance_iso_year_month(double year, double month) +{ + // 1. Set year to year + floor((month - 1) / 12). + year += floor((month - 1.0) / 12.0); + + // 2. Set month to ((month - 1) modulo 12) + 1. + month = modulo(month - 1, 12.0) + 1.0; + + // 3. Return ISO Year-Month Record { [[Year]]: year, [[Month]]: month }. + return { .year = static_cast(year), .month = static_cast(month) }; +} + // 9.5.5 CreateTemporalYearMonth ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalyearmonth ThrowCompletionOr> create_temporal_year_month(VM& vm, ISODate iso_date, String calendar, GC::Ptr new_target) { @@ -176,4 +192,82 @@ String temporal_year_month_to_string(PlainYearMonth const& year_month, ShowCalen return result; } +// 9.5.7 DifferenceTemporalPlainYearMonth ( operation, yearMonth, other, options ), https://tc39.es/proposal-temporal/#sec-temporal-differencetemporalplainyearmonth +ThrowCompletionOr> difference_temporal_plain_year_month(VM& vm, DurationOperation operation, PlainYearMonth const& year_month, Value other_value, Value options) +{ + // 1. Set other to ? ToTemporalYearMonth(other). + auto other = TRY(to_temporal_year_month(vm, other_value)); + + // 2. Let calendar be yearMonth.[[Calendar]]. + auto const& calendar = year_month.calendar(); + + // 3. If CalendarEquals(calendar, other.[[Calendar]]) is false, throw a RangeError exception. + if (!calendar_equals(calendar, other->calendar())) + return vm.throw_completion(ErrorType::TemporalDifferentCalendars); + + // 4. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 5. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, DATE, « WEEK, DAY », MONTH, YEAR). + auto settings = TRY(get_difference_settings(vm, operation, resolved_options, UnitGroup::Date, { { Unit::Week, Unit::Day } }, Unit::Month, Unit::Year)); + + // 6. If CompareISODate(yearMonth.[[ISODate]], other.[[ISODate]]) = 0, then + if (compare_iso_date(year_month.iso_date(), other->iso_date()) == 0) { + // a. Return ! CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0). + return MUST(create_temporal_duration(vm, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } + + // 7. Let thisFields be ISODateToFields(calendar, yearMonth.[[ISODate]], YEAR-MONTH). + auto this_fields = iso_date_to_fields(calendar, year_month.iso_date(), DateType::YearMonth); + + // 8. Set thisFields.[[Day]] to 1. + this_fields.day = 1; + + // 9. Let thisDate be ? CalendarDateFromFields(calendar, thisFields, CONSTRAIN). + auto this_date = TRY(calendar_date_from_fields(vm, calendar, move(this_fields), Overflow::Constrain)); + + // 10. Let otherFields be ISODateToFields(calendar, other.[[ISODate]], YEAR-MONTH). + auto other_fields = iso_date_to_fields(calendar, other->iso_date(), DateType::YearMonth); + + // 11. Set otherFields.[[Day]] to 1. + other_fields.day = 1; + + // 12. Let otherDate be ? CalendarDateFromFields(calendar, otherFields, CONSTRAIN). + auto other_date = TRY(calendar_date_from_fields(vm, calendar, move(other_fields), Overflow::Constrain)); + + // 13. Let dateDifference be CalendarDateUntil(calendar, thisDate, otherDate, settings.[[LargestUnit]]). + auto date_difference = calendar_date_until(vm, calendar, this_date, other_date, settings.largest_unit); + + // 14. Let yearsMonthsDifference be ! AdjustDateDurationRecord(dateDifference, 0, 0). + auto years_months_difference = MUST(adjust_date_duration_record(vm, date_difference, 0, 0)); + + // 15. Let duration be ! CombineDateAndTimeDuration(yearsMonthsDifference, 0). + auto duration = MUST(combine_date_and_time_duration(vm, years_months_difference, TimeDuration { 0 })); + + // 16. If settings.[[SmallestUnit]] is not MONTH or settings.[[RoundingIncrement]] ≠ 1, then + if (settings.smallest_unit != Unit::Month || settings.rounding_increment != 1) { + // a. Let isoDateTime be CombineISODateAndTimeRecord(thisDate, MidnightTimeRecord()). + auto iso_date_time = combine_iso_date_and_time_record(this_date, midnight_time_record()); + + // b. Let isoDateTimeOther be CombineISODateAndTimeRecord(otherDate, MidnightTimeRecord()). + auto iso_date_time_other = combine_iso_date_and_time_record(other_date, midnight_time_record()); + + // c. Let destEpochNs be GetUTCEpochNanoseconds(isoDateTimeOther). + auto dest_epoch_ns = get_utc_epoch_nanoseconds(iso_date_time_other); + + // d. Set duration to ? RoundRelativeDuration(duration, destEpochNs, isoDateTime, UNSET, calendar, settings.[[LargestUnit]], settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]). + duration = TRY(round_relative_duration(vm, move(duration), dest_epoch_ns, iso_date_time, {}, calendar, settings.largest_unit, settings.rounding_increment, settings.smallest_unit, settings.rounding_mode)); + } + + // 17. Let result be ? TemporalDurationFromInternal(duration, DAY). + auto result = TRY(temporal_duration_from_internal(vm, duration, Unit::Day)); + + // 18. If operation is SINCE, set result to CreateNegatedTemporalDuration(result). + if (operation == DurationOperation::Since) + result = create_negated_temporal_duration(vm, result); + + // 19. Return result. + return result; +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h index 20b876d244af..0e51648e4d92 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h @@ -32,9 +32,17 @@ class PlainYearMonth final : public Object { String m_calendar; // [[Calendar]] }; +// 9.5.1 ISO Year-Month Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-year-month-records +struct ISOYearMonth { + i32 year { 0 }; + u8 month { 0 }; +}; + ThrowCompletionOr> to_temporal_year_month(VM&, Value item, Value options = js_undefined()); bool iso_year_month_within_limits(ISODate); +ISOYearMonth balance_iso_year_month(double year, double month); ThrowCompletionOr> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr new_target = {}); String temporal_year_month_to_string(PlainYearMonth const&, ShowCalendar); +ThrowCompletionOr> difference_temporal_plain_year_month(VM&, DurationOperation, PlainYearMonth const&, Value other, Value options); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index b3d5c7421c85..893186b38693 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace JS::Temporal { @@ -41,6 +42,8 @@ void PlainYearMonthPrototype::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(realm, vm.names.with, with, 1, attr); + define_native_function(realm, vm.names.until, until, 1, attr); + define_native_function(realm, vm.names.since, since, 1, attr); define_native_function(realm, vm.names.equals, equals, 1, attr); define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); @@ -170,6 +173,34 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::with) return MUST(create_temporal_year_month(vm, iso_date, calendar)); } +// 9.3.16 Temporal.PlainYearMonth.prototype.until ( other [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.until +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::until) +{ + auto other = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return ? DifferenceTemporalPlainYearMonth(UNTIL, yearMonth, other, options). + return TRY(difference_temporal_plain_year_month(vm, DurationOperation::Until, year_month, other, options)); +} + +// 9.3.17 Temporal.PlainYearMonth.prototype.since ( other [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.since +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::since) +{ + auto other = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return ? DifferenceTemporalPlainYearMonth(SINCE, yearMonth, other, options). + return TRY(difference_temporal_plain_year_month(vm, DurationOperation::Since, year_month, other, options)); +} + // 9.3.18 Temporal.PlainYearMonth.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.equals JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::equals) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 737412ea623e..8f8215fe47db 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -34,6 +34,8 @@ class PlainYearMonthPrototype final : public PrototypeObject #include #include +#include #include +#include +#include #include #include @@ -70,6 +73,125 @@ ThrowCompletionOr to_temporal_time_zone_identifier(VM& vm, Value tempora return time_zone_identifier_record->identifier; } +// 11.1.11 GetEpochNanosecondsFor ( timeZone, isoDateTime, disambiguation ), https://tc39.es/proposal-temporal/#sec-temporal-getepochnanosecondsfor +ThrowCompletionOr get_epoch_nanoseconds_for(VM& vm, StringView time_zone, ISODateTime const& iso_date_time, Disambiguation disambiguation) +{ + // 1. Let possibleEpochNs be ? GetPossibleEpochNanoseconds(timeZone, isoDateTime). + auto possible_epoch_ns = TRY(get_possible_epoch_nanoseconds(vm, time_zone, iso_date_time)); + + // 2. Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation). + return TRY(disambiguate_possible_epoch_nanoseconds(vm, move(possible_epoch_ns), time_zone, iso_date_time, disambiguation)); +} + +// 11.1.12 DisambiguatePossibleEpochNanoseconds ( possibleEpochNs, timeZone, isoDateTime, disambiguation ), https://tc39.es/proposal-temporal/#sec-temporal-disambiguatepossibleepochnanoseconds +ThrowCompletionOr disambiguate_possible_epoch_nanoseconds(VM& vm, Vector possible_epoch_ns, StringView time_zone, ISODateTime const& iso_date_time, Disambiguation disambiguation) +{ + // 1. Let n be possibleEpochNs's length. + auto n = possible_epoch_ns.size(); + + // 2. If n = 1, then + if (n == 1) { + // a. Return possibleEpochNs[0]. + return move(possible_epoch_ns[0]); + } + + // 3. If n ≠ 0, then + if (n != 0) { + // a. If disambiguation is EARLIER or COMPATIBLE, then + if (disambiguation == Disambiguation::Earlier || disambiguation == Disambiguation::Compatible) { + // i. Return possibleEpochNs[0]. + return move(possible_epoch_ns[0]); + } + + // b. If disambiguation is LATER, then + if (disambiguation == Disambiguation::Later) { + // i. Return possibleEpochNs[n - 1]. + return move(possible_epoch_ns[n - 1]); + } + + // c. Assert: disambiguation is REJECT. + VERIFY(disambiguation == Disambiguation::Reject); + + // d. Throw a RangeError exception. + return vm.throw_completion(ErrorType::TemporalDisambiguatePossibleEpochNSRejectMoreThanOne); + } + + // 4. Assert: n = 0. + VERIFY(n == 0); + + // 5. If disambiguation is REJECT, then + if (disambiguation == Disambiguation::Reject) { + // a. Throw a RangeError exception. + return vm.throw_completion(ErrorType::TemporalDisambiguatePossibleEpochNSRejectZero); + } + + // FIXME: GetNamedTimeZoneEpochNanoseconds currently does not produce zero instants. + (void)time_zone; + (void)iso_date_time; + TODO(); +} + +// 11.1.13 GetPossibleEpochNanoseconds ( timeZone, isoDateTime ), https://tc39.es/proposal-temporal/#sec-temporal-getpossibleepochnanoseconds +ThrowCompletionOr> get_possible_epoch_nanoseconds(VM& vm, StringView time_zone, ISODateTime const& iso_date_time) +{ + Vector possible_epoch_nanoseconds; + + // 1. Let parseResult be ! ParseTimeZoneIdentifier(timeZone). + auto parse_result = parse_time_zone_identifier(time_zone); + + // 2. If parseResult.[[OffsetMinutes]] is not empty, then + if (parse_result.offset_minutes.has_value()) { + // a. Let balanced be BalanceISODateTime(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]], isoDateTime.[[ISODate]].[[Day]], isoDateTime.[[Time]].[[Hour]], isoDateTime.[[Time]].[[Minute]] - parseResult.[[OffsetMinutes]], isoDateTime.[[Time]].[[Second]], isoDateTime.[[Time]].[[Millisecond]], isoDateTime.[[Time]].[[Microsecond]], isoDateTime.[[Time]].[[Nanosecond]]). + auto balanced = balance_iso_date_time( + iso_date_time.iso_date.year, + iso_date_time.iso_date.month, + iso_date_time.iso_date.day, + iso_date_time.time.hour, + static_cast(iso_date_time.time.minute) - static_cast(*parse_result.offset_minutes), + iso_date_time.time.second, + iso_date_time.time.millisecond, + iso_date_time.time.microsecond, + iso_date_time.time.nanosecond); + + // b. Perform ? CheckISODaysRange(balanced.[[ISODate]]). + TRY(check_iso_days_range(vm, balanced.iso_date)); + + // c. Let epochNanoseconds be GetUTCEpochNanoseconds(balanced). + auto epoch_nanoseconds = get_utc_epoch_nanoseconds(balanced); + + // d. Let possibleEpochNanoseconds be « epochNanoseconds ». + possible_epoch_nanoseconds.append(move(epoch_nanoseconds)); + } + // 3. Else, + else { + // a. Perform ? CheckISODaysRange(isoDateTime.[[ISODate]]). + TRY(check_iso_days_range(vm, iso_date_time.iso_date)); + + // b. Let possibleEpochNanoseconds be GetNamedTimeZoneEpochNanoseconds(parseResult.[[Name]], isoDateTime). + possible_epoch_nanoseconds = get_named_time_zone_epoch_nanoseconds( + *parse_result.name, + iso_date_time.iso_date.year, + iso_date_time.iso_date.month, + iso_date_time.iso_date.day, + iso_date_time.time.hour, + iso_date_time.time.minute, + iso_date_time.time.second, + iso_date_time.time.millisecond, + iso_date_time.time.microsecond, + iso_date_time.time.nanosecond); + } + + // 4. For each value epochNanoseconds in possibleEpochNanoseconds, do + for (auto const& epoch_nanoseconds : possible_epoch_nanoseconds) { + // a. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. + if (!is_valid_epoch_nanoseconds(epoch_nanoseconds)) + return vm.throw_completion(ErrorType::TemporalInvalidEpochNanoseconds); + } + + // 5. Return possibleEpochNanoseconds. + return possible_epoch_nanoseconds; +} + // 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier ThrowCompletionOr parse_time_zone_identifier(VM& vm, StringView identifier) { diff --git a/Libraries/LibJS/Runtime/Temporal/TimeZone.h b/Libraries/LibJS/Runtime/Temporal/TimeZone.h index 97972ab0c88d..711cf1e7758f 100644 --- a/Libraries/LibJS/Runtime/Temporal/TimeZone.h +++ b/Libraries/LibJS/Runtime/Temporal/TimeZone.h @@ -8,6 +8,8 @@ #pragma once #include +#include +#include #include #include #include @@ -19,8 +21,18 @@ struct TimeZone { Optional offset_minutes; }; +enum class Disambiguation { + Compatible, + Earlier, + Later, + Reject, +}; + String format_offset_time_zone_identifier(i64 offset_minutes, Optional = {}); ThrowCompletionOr to_temporal_time_zone_identifier(VM&, Value temporal_time_zone_like); +ThrowCompletionOr get_epoch_nanoseconds_for(VM&, StringView time_zone, ISODateTime const&, Disambiguation); +ThrowCompletionOr disambiguate_possible_epoch_nanoseconds(VM&, Vector possible_epoch_ns, StringView time_zone, ISODateTime const&, Disambiguation); +ThrowCompletionOr> get_possible_epoch_nanoseconds(VM&, StringView time_zone, ISODateTime const&); ThrowCompletionOr parse_time_zone_identifier(VM&, StringView identifier); TimeZone parse_time_zone_identifier(StringView identifier); TimeZone parse_time_zone_identifier(ParseResult const&); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js new file mode 100644 index 000000000000..6cab7d59f24a --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.since.js @@ -0,0 +1,108 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.prototype.since).toHaveLength(1); + }); + + test("basic functionality", () => { + const values = [ + [[0, 1], [0, 1], "PT0S"], + [[2, 3], [1, 2], "P1Y1M"], + [[1, 2], [0, 1], "P1Y1M"], + [[0, 1], [1, 2], "-P1Y1M"], + [[0, 12], [0, 1], "P11M"], + [[0, 1], [0, 12], "-P11M"], + ]; + for (const [args, argsOther, expected] of values) { + const plainYearMonth = new Temporal.PlainYearMonth(...args); + const other = new Temporal.PlainYearMonth(...argsOther); + expect(plainYearMonth.since(other).toString()).toBe(expected); + } + }); + + test("smallestUnit option", () => { + const plainYearMonth = new Temporal.PlainYearMonth(1, 2); + const other = new Temporal.PlainYearMonth(0, 1); + const values = [ + ["year", "P1Y"], + ["month", "P1Y1M"], + ]; + for (const [smallestUnit, expected] of values) { + expect(plainYearMonth.since(other, { smallestUnit }).toString()).toBe(expected); + } + }); + + test("largestUnit option", () => { + const plainYearMonth = new Temporal.PlainYearMonth(1, 2); + const other = new Temporal.PlainYearMonth(0, 1); + const values = [ + ["year", "P1Y1M"], + ["month", "P13M"], + ]; + for (const [largestUnit, expected] of values) { + expect(plainYearMonth.since(other, { largestUnit }).toString()).toBe(expected); + } + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.since.call("foo", {}); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); + + test("disallowed smallestUnit option values", () => { + const values = [ + "week", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + ]; + for (const smallestUnit of values) { + const plainYearMonth = new Temporal.PlainYearMonth(1970, 1); + const other = new Temporal.PlainYearMonth(1970, 1); + expect(() => { + plainYearMonth.since(other, { smallestUnit }); + }).toThrowWithMessage( + RangeError, + `${smallestUnit} is not a valid value for option smallestUnit` + ); + } + }); + + test("disallowed largestUnit option values", () => { + const values = [ + "week", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + ]; + for (const largestUnit of values) { + const plainYearMonth = new Temporal.PlainYearMonth(1970, 1); + const other = new Temporal.PlainYearMonth(1970, 1); + expect(() => { + plainYearMonth.since(other, { largestUnit }); + }).toThrowWithMessage( + RangeError, + `${largestUnit} is not a valid value for option largestUnit` + ); + } + }); + + test("cannot compare dates from different calendars", () => { + const plainYearMonthOne = new Temporal.PlainYearMonth(1970, 1, "iso8601"); + const plainYearMonthTwo = new Temporal.PlainYearMonth(1970, 1, "gregory"); + + expect(() => { + plainYearMonthOne.since(plainYearMonthTwo); + }).toThrowWithMessage(RangeError, "Cannot compare dates from two different calendars"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js new file mode 100644 index 000000000000..f05fa44c4e84 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.until.js @@ -0,0 +1,108 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.prototype.until).toHaveLength(1); + }); + + test("basic functionality", () => { + const values = [ + [[0, 1], [0, 1], "PT0S"], + [[1, 2], [2, 3], "P1Y1M"], + [[0, 1], [1, 2], "P1Y1M"], + [[1, 2], [0, 1], "-P1Y1M"], + [[0, 1], [0, 12], "P11M"], + [[0, 12], [0, 1], "-P11M"], + ]; + for (const [args, argsOther, expected] of values) { + const plainYearMonth = new Temporal.PlainYearMonth(...args); + const other = new Temporal.PlainYearMonth(...argsOther); + expect(plainYearMonth.until(other).toString()).toBe(expected); + } + }); + + test("smallestUnit option", () => { + const plainYearMonth = new Temporal.PlainYearMonth(0, 1); + const other = new Temporal.PlainYearMonth(1, 2); + const values = [ + ["year", "P1Y"], + ["month", "P1Y1M"], + ]; + for (const [smallestUnit, expected] of values) { + expect(plainYearMonth.until(other, { smallestUnit }).toString()).toBe(expected); + } + }); + + test("largestUnit option", () => { + const plainYearMonth = new Temporal.PlainYearMonth(0, 1); + const other = new Temporal.PlainYearMonth(1, 2); + const values = [ + ["year", "P1Y1M"], + ["month", "P13M"], + ]; + for (const [largestUnit, expected] of values) { + expect(plainYearMonth.until(other, { largestUnit }).toString()).toBe(expected); + } + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.until.call("foo", {}); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); + + test("disallowed smallestUnit option values", () => { + const values = [ + "week", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + ]; + for (const smallestUnit of values) { + const plainYearMonth = new Temporal.PlainYearMonth(1970, 1); + const other = new Temporal.PlainYearMonth(1970, 1); + expect(() => { + plainYearMonth.until(other, { smallestUnit }); + }).toThrowWithMessage( + RangeError, + `${smallestUnit} is not a valid value for option smallestUnit` + ); + } + }); + + test("disallowed largestUnit option values", () => { + const values = [ + "week", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", + ]; + for (const largestUnit of values) { + const plainYearMonth = new Temporal.PlainYearMonth(1970, 1); + const other = new Temporal.PlainYearMonth(1970, 1); + expect(() => { + plainYearMonth.until(other, { largestUnit }); + }).toThrowWithMessage( + RangeError, + `${largestUnit} is not a valid value for option largestUnit` + ); + } + }); + + test("cannot compare dates from different calendars", () => { + const plainYearMonthOne = new Temporal.PlainYearMonth(1970, 1, "iso8601"); + const plainYearMonthTwo = new Temporal.PlainYearMonth(1970, 1, "gregory"); + + expect(() => { + plainYearMonthOne.until(plainYearMonthTwo); + }).toThrowWithMessage(RangeError, "Cannot compare dates from two different calendars"); + }); +}); From 35f22dcf79d9204f2572cef57f8776006dbd4a8b Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 19:09:21 -0500 Subject: [PATCH 008/397] LibJS: Implement Temporal.PlainYearMonth.prototype.add/subtract --- Libraries/LibJS/Runtime/Temporal/Duration.cpp | 13 ++++ Libraries/LibJS/Runtime/Temporal/Duration.h | 1 + .../LibJS/Runtime/Temporal/PlainYearMonth.cpp | 69 +++++++++++++++++++ .../LibJS/Runtime/Temporal/PlainYearMonth.h | 1 + .../Temporal/PlainYearMonthPrototype.cpp | 30 ++++++++ .../Temporal/PlainYearMonthPrototype.h | 2 + .../PlainYearMonth.prototype.add.js | 19 +++++ .../PlainYearMonth.prototype.subtract.js | 19 +++++ 8 files changed, 154 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.add.js create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.subtract.js diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.cpp b/Libraries/LibJS/Runtime/Temporal/Duration.cpp index 03b35d95fff7..1a59e9d2e92c 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Duration.cpp @@ -108,6 +108,19 @@ InternalDuration to_internal_duration_record_with_24_hour_days(VM& vm, Duration return MUST(combine_date_and_time_duration(vm, date_duration, move(time_duration))); } +// 7.5.7 ToDateDurationRecordWithoutTime ( duration ), https://tc39.es/proposal-temporal/#sec-temporal-todatedurationrecordwithouttime +ThrowCompletionOr to_date_duration_record_without_time(VM& vm, Duration const& duration) +{ + // 1. Let internalDuration be ToInternalDurationRecordWith24HourDays(duration). + auto internal_duration = to_internal_duration_record_with_24_hour_days(vm, duration); + + // 2. Let days be truncate(internalDuration.[[Time]] / nsPerDay). + auto days = internal_duration.time.divided_by(NANOSECONDS_PER_DAY).quotient; + + // 3. Return ? CreateDateDurationRecord(internalDuration.[[Date]].[[Years]], internalDuration.[[Date]].[[Months]], internalDuration.[[Date]].[[Weeks]], days). + return TRY(create_date_duration_record(vm, duration.years(), duration.months(), duration.weeks(), days.to_double())); +} + // 7.5.8 TemporalDurationFromInternal ( internalDuration, largestUnit ), https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationfrominternal ThrowCompletionOr> temporal_duration_from_internal(VM& vm, InternalDuration const& internal_duration, Unit largest_unit) { diff --git a/Libraries/LibJS/Runtime/Temporal/Duration.h b/Libraries/LibJS/Runtime/Temporal/Duration.h index ab3696e08aa7..70697331d9af 100644 --- a/Libraries/LibJS/Runtime/Temporal/Duration.h +++ b/Libraries/LibJS/Runtime/Temporal/Duration.h @@ -116,6 +116,7 @@ struct CalendarNudgeResult { DateDuration zero_date_duration(VM&); InternalDuration to_internal_duration_record(VM&, Duration const&); InternalDuration to_internal_duration_record_with_24_hour_days(VM&, Duration const&); +ThrowCompletionOr to_date_duration_record_without_time(VM&, Duration const&); ThrowCompletionOr> temporal_duration_from_internal(VM&, InternalDuration const&, Unit largest_unit); ThrowCompletionOr create_date_duration_record(VM&, double years, double months, double weeks, double days); ThrowCompletionOr adjust_date_duration_record(VM&, DateDuration const&, double days, Optional weeks = {}, Optional months = {}); diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp index 292089d9175a..2e425df14189 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.cpp @@ -270,4 +270,73 @@ ThrowCompletionOr> difference_temporal_plain_year_month(VM& vm return result; } +// 9.5.8 AddDurationToYearMonth ( operation, yearMonth, temporalDurationLike, options ) +ThrowCompletionOr> add_duration_to_year_month(VM& vm, ArithmeticOperation operation, PlainYearMonth const& year_month, Value temporal_duration_like, Value options) +{ + // 1. Let duration be ? ToTemporalDuration(temporalDurationLike). + auto duration = TRY(to_temporal_duration(vm, temporal_duration_like)); + + // 2. If operation is SUBTRACT, set duration to CreateNegatedTemporalDuration(duration). + if (operation == ArithmeticOperation::Subtract) + duration = create_negated_temporal_duration(vm, duration); + + // 3. Let resolvedOptions be ? GetOptionsObject(options). + auto resolved_options = TRY(get_options_object(vm, options)); + + // 4. Let overflow be ? GetTemporalOverflowOption(resolvedOptions). + auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options)); + + // 5. Let sign be DurationSign(duration). + auto sign = duration_sign(duration); + + // 6. Let calendar be yearMonth.[[Calendar]]. + auto const& calendar = year_month.calendar(); + + // 7. Let fields be ISODateToFields(calendar, yearMonth.[[ISODate]], YEAR-MONTH). + auto fields = iso_date_to_fields(calendar, year_month.iso_date(), DateType::YearMonth); + + // 8. Set fields.[[Day]] to 1. + fields.day = 1; + + // 9. Let intermediateDate be ? CalendarDateFromFields(calendar, fields, CONSTRAIN). + auto intermediate_date = TRY(calendar_date_from_fields(vm, calendar, move(fields), Overflow::Constrain)); + + ISODate date; + + // 10. If sign < 0, then + if (sign < 0) { + // a. Let oneMonthDuration be ! CreateDateDurationRecord(0, 1, 0, 0). + auto one_month_duration = MUST(create_date_duration_record(vm, 0, 1, 0, 0)); + + // b. Let nextMonth be ? CalendarDateAdd(calendar, intermediateDate, oneMonthDuration, CONSTRAIN). + auto next_month = TRY(calendar_date_add(vm, calendar, intermediate_date, one_month_duration, Overflow::Constrain)); + + // c. Let date be BalanceISODate(nextMonth.[[Year]], nextMonth.[[Month]], nextMonth.[[Day]] - 1). + date = balance_iso_date(next_month.year, next_month.month, next_month.day - 1); + + // d. Assert: ISODateWithinLimits(date) is true. + VERIFY(iso_date_within_limits(date)); + } + // 11. Else, + else { + // a. Let date be intermediateDate. + date = intermediate_date; + } + + // 12. Let durationToAdd be ? ToDateDurationRecordWithoutTime(duration). + auto duration_to_add = TRY(to_date_duration_record_without_time(vm, duration)); + + // 13. Let addedDate be ? CalendarDateAdd(calendar, date, durationToAdd, overflow). + auto added_date = TRY(calendar_date_add(vm, calendar, date, duration_to_add, overflow)); + + // 14. Let addedDateFields be ISODateToFields(calendar, addedDate, YEAR-MONTH). + auto added_date_fields = iso_date_to_fields(calendar, added_date, DateType::YearMonth); + + // 15. Let isoDate be ? CalendarYearMonthFromFields(calendar, addedDateFields, overflow). + auto iso_date = TRY(calendar_year_month_from_fields(vm, calendar, move(added_date_fields), overflow)); + + // 16. Return ! CreateTemporalYearMonth(isoDate, calendar). + return MUST(create_temporal_year_month(vm, iso_date, calendar)); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h index 0e51648e4d92..a505a33868d5 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonth.h @@ -44,5 +44,6 @@ ISOYearMonth balance_iso_year_month(double year, double month); ThrowCompletionOr> create_temporal_year_month(VM&, ISODate, String calendar, GC::Ptr new_target = {}); String temporal_year_month_to_string(PlainYearMonth const&, ShowCalendar); ThrowCompletionOr> difference_temporal_plain_year_month(VM&, DurationOperation, PlainYearMonth const&, Value other, Value options); +ThrowCompletionOr> add_duration_to_year_month(VM&, ArithmeticOperation, PlainYearMonth const&, Value temporal_duration_like, Value options); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index 893186b38693..bdbdb8a99665 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -42,6 +42,8 @@ void PlainYearMonthPrototype::initialize(Realm& realm) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(realm, vm.names.with, with, 1, attr); + define_native_function(realm, vm.names.add, add, 1, attr); + define_native_function(realm, vm.names.subtract, subtract, 1, attr); define_native_function(realm, vm.names.until, until, 1, attr); define_native_function(realm, vm.names.since, since, 1, attr); define_native_function(realm, vm.names.equals, equals, 1, attr); @@ -173,6 +175,34 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::with) return MUST(create_temporal_year_month(vm, iso_date, calendar)); } +// 9.3.14 Temporal.PlainYearMonth.prototype.add ( temporalDurationLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.add +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::add) +{ + auto temporal_duration_like = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return ? AddDurationToYearMonth(ADD, yearMonth, temporalDurationLike, options). + return TRY(add_duration_to_year_month(vm, ArithmeticOperation::Add, year_month, temporal_duration_like, options)); +} + +// 9.3.15 Temporal.PlainYearMonth.prototype.subtract ( temporalDurationLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.subtract +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::subtract) +{ + auto temporal_duration_like = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let yearMonth be the this value. + // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). + auto year_month = TRY(typed_this_object(vm)); + + // 3. Return ? AddDurationToYearMonth(SUBTRACT, yearMonth, temporalDurationLike, options). + return TRY(add_duration_to_year_month(vm, ArithmeticOperation::Subtract, year_month, temporal_duration_like, options)); +} + // 9.3.16 Temporal.PlainYearMonth.prototype.until ( other [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.until JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::until) { diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 8f8215fe47db..2696ec314389 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -34,6 +34,8 @@ class PlainYearMonthPrototype final : public PrototypeObject { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.prototype.add).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(1970, 1); + const result = plainYearMonth.add(new Temporal.Duration(51, 6)); + expect(result.equals(new Temporal.PlainYearMonth(2021, 7))).toBeTrue(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.add.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.subtract.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.subtract.js new file mode 100644 index 000000000000..1c04a95cca4a --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.subtract.js @@ -0,0 +1,19 @@ +describe("correct behavior", () => { + test("length is 1", () => { + expect(Temporal.PlainYearMonth.prototype.subtract).toHaveLength(1); + }); + + test("basic functionality", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + const result = plainYearMonth.subtract(new Temporal.Duration(51, 6)); + expect(result.equals(new Temporal.PlainYearMonth(1970, 1))).toBeTrue(); + }); +}); + +describe("errors", () => { + test("this value must be a Temporal.PlainYearMonth object", () => { + expect(() => { + Temporal.PlainYearMonth.prototype.subtract.call("foo"); + }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainYearMonth"); + }); +}); From b64ccb95ec7239d648dfd69a1eead73ff2ae0f3a Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 21 Nov 2024 19:11:16 -0500 Subject: [PATCH 009/397] LibJS: Implement Temporal.PlainYearMonth.prototype.valueOf --- .../Runtime/Temporal/PlainYearMonthPrototype.cpp | 8 ++++++++ .../LibJS/Runtime/Temporal/PlainYearMonthPrototype.h | 1 + .../PlainYearMonth.prototype.valueOf.js | 11 +++++++++++ 3 files changed, 20 insertions(+) create mode 100644 Libraries/LibJS/Tests/builtins/Temporal/PlainYearMonth/PlainYearMonth.prototype.valueOf.js diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp index bdbdb8a99665..d1130e1f9620 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.cpp @@ -50,6 +50,7 @@ void PlainYearMonthPrototype::initialize(Realm& realm) define_native_function(realm, vm.names.toString, to_string, 0, attr); define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(realm, vm.names.toJSON, to_json, 0, attr); + define_native_function(realm, vm.names.valueOf, value_of, 0, attr); } // 9.3.3 get Temporal.PlainYearMonth.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainyearmonth.prototype.calendarid @@ -289,4 +290,11 @@ JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::to_json) return PrimitiveString::create(vm, temporal_year_month_to_string(year_month, ShowCalendar::Auto)); } +// 9.3.22 Temporal.PlainYearMonth.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainyearmonth.prototype.valueof +JS_DEFINE_NATIVE_FUNCTION(PlainYearMonthPrototype::value_of) +{ + // 1. Throw a TypeError exception. + return vm.throw_completion(ErrorType::Convert, "Temporal.PlainYearMonth", "a primitive value"); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h index 2696ec314389..30a9fd1ddbe0 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainYearMonthPrototype.h @@ -42,6 +42,7 @@ class PlainYearMonthPrototype final : public PrototypeObject { + test("throws TypeError", () => { + const plainYearMonth = new Temporal.PlainYearMonth(2021, 7); + expect(() => { + plainYearMonth.valueOf(); + }).toThrowWithMessage( + TypeError, + "Cannot convert Temporal.PlainYearMonth to a primitive value" + ); + }); +}); From d6f7fccf49379a47bb28e2c72acbfc0a83baa661 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 22 Nov 2024 16:02:24 +0100 Subject: [PATCH 010/397] Tests: Import a bunch of WPT tests from /css/css-transforms --- .../individual-transform-combine-ref.html | 35 ++ .../individual-transform-ordering-ref.html | 44 ++ .../individual-transform-1-ref.html | 90 ++++ .../individual-transform-2-ref.html | 30 ++ .../stacking-context-ref.html | 27 ++ .../reference/greensquare200x200.html | 3 + .../individual-transform-2a.html | 32 ++ .../individual-transform-2b.html | 32 ++ .../individual-transform-2c.html | 32 ++ .../individual-transform-2d.html | 31 ++ .../individual-transform-2e.html | 31 ++ .../stacking-context-001.html | 40 ++ .../translate-fill-box.html | 37 ++ .../translate-view-box.html | 35 ++ .../animation/translate-interpolation.txt | 418 ++++++++++++++++++ .../parsing/rotate-parsing-computed.txt | 27 ++ .../parsing/rotate-parsing-invalid.txt | 19 + .../parsing/rotate-parsing-valid.txt | 28 ++ .../parsing/scale-parsing-computed.txt | 32 ++ .../parsing/scale-parsing-invalid.txt | 15 + .../parsing/scale-parsing-valid.txt | 32 ++ .../parsing/translate-parsing-computed.txt | 29 ++ .../parsing/translate-parsing-invalid.txt | 16 + .../parsing/translate-parsing-valid.txt | 30 ++ .../animation/translate-interpolation.html | 271 ++++++++++++ .../parsing/rotate-parsing-computed.html | 47 ++ .../parsing/rotate-parsing-invalid.html | 27 ++ .../parsing/rotate-parsing-valid.html | 45 ++ .../parsing/scale-parsing-computed.html | 44 ++ .../parsing/scale-parsing-invalid.html | 23 + .../parsing/scale-parsing-valid.html | 42 ++ .../parsing/translate-parsing-computed.html | 42 ++ .../parsing/translate-parsing-invalid.html | 24 + .../parsing/translate-parsing-valid.html | 42 ++ 34 files changed, 1752 insertions(+) create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-combine-ref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-ordering-ref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-1-ref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-2-ref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/stacking-context-ref.html create mode 100644 Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform-box/reference/greensquare200x200.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2a.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2b.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2c.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2d.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2e.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/stacking-context-001.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-fill-box.html create mode 100644 Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-view-box.html create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/animation/translate-interpolation.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-computed.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-valid.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-computed.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-valid.html diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-combine-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-combine-ref.html new file mode 100644 index 000000000000..7c8eb206a9fb --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-combine-ref.html @@ -0,0 +1,35 @@ + + + + + Individual transform: combine individual transform properties + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-ordering-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-ordering-ref.html new file mode 100644 index 000000000000..b7dce51e8bab --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/animation/individual-transform-ordering-ref.html @@ -0,0 +1,44 @@ + + + + + Individual transform: combine individual transform properties + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-1-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-1-ref.html new file mode 100644 index 000000000000..dbc5f05d8925 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-1-ref.html @@ -0,0 +1,90 @@ + + + + + Individual transform: compare individual transform with transform functions + + + + + + + +
+
+
+
+
+
+
+
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-2-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-2-ref.html new file mode 100644 index 000000000000..ee956aa301f7 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/individual-transform-2-ref.html @@ -0,0 +1,30 @@ + + + + + Individual transform: combine individual transform properties + + + + + + + + +
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/stacking-context-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/stacking-context-ref.html new file mode 100644 index 000000000000..0b8c52303d6f --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/individual-transform/stacking-context-ref.html @@ -0,0 +1,27 @@ + + + + + Reference: Individual transform properties' animations create stacking context in delay phase + + + + +
+
+ + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform-box/reference/greensquare200x200.html b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform-box/reference/greensquare200x200.html new file mode 100644 index 000000000000..bee8bc70fe5d --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/css-transforms/transform-box/reference/greensquare200x200.html @@ -0,0 +1,3 @@ + +

There should be a green 200x200 rectangle below, and no red.

+
diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2a.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2a.html new file mode 100644 index 000000000000..1efd76b33293 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2a.html @@ -0,0 +1,32 @@ + + + + + Individual transform: combine individual transform properties + + + + + + + + + +
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2b.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2b.html new file mode 100644 index 000000000000..26f066cf2dd4 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2b.html @@ -0,0 +1,32 @@ + + + + + Individual transform: combine individual transform properties + + + + + + + + + +
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2c.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2c.html new file mode 100644 index 000000000000..9e79cd21b894 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2c.html @@ -0,0 +1,32 @@ + + + + + Individual transform: combine individual transform properties + + + + + + + + + +
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2d.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2d.html new file mode 100644 index 000000000000..f4d29420ff96 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2d.html @@ -0,0 +1,31 @@ + + + + + Individual transform: combine individual transform properties + + + + + + + + + +
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2e.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2e.html new file mode 100644 index 000000000000..8bb0ba6ff310 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/individual-transform-2e.html @@ -0,0 +1,31 @@ + + + + + Individual transform: combine individual transform properties + + + + + + + + + +
+ + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/stacking-context-001.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/stacking-context-001.html new file mode 100644 index 000000000000..03e9eaaf072e --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/stacking-context-001.html @@ -0,0 +1,40 @@ + + + + + Individual transform properties' animations create stacking context in delay phase + + + + + + + +
+
+ + + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-fill-box.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-fill-box.html new file mode 100644 index 000000000000..498cde8b1de1 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-fill-box.html @@ -0,0 +1,37 @@ + +transform-box: fill-box + + + + + +

There should be a green 200x200 rectangle below, and no red.

+ + + + + + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-view-box.html b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-view-box.html new file mode 100644 index 000000000000..6d157852aad5 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/css-transforms/individual-transform/translate-view-box.html @@ -0,0 +1,35 @@ + +transform-box: view-box + + + + + +

There should be a green 200x200 rectangle below, and no red.

+ + + + + + diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt new file mode 100644 index 000000000000..eaf81fbe2d12 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt @@ -0,0 +1,418 @@ +Summary + +Harness status: OK + +Rerun + +Found 408 tests + +408 Fail +Details +Result Test Name MessageFail CSS Transitions: property from [-100px] to [100px] at (-1) should be [-300px] +Fail CSS Transitions: property from [-100px] to [100px] at (0) should be [-100px] +Fail CSS Transitions: property from [-100px] to [100px] at (0.25) should be [-50px] +Fail CSS Transitions: property from [-100px] to [100px] at (0.75) should be [50px] +Fail CSS Transitions: property from [-100px] to [100px] at (1) should be [100px] +Fail CSS Transitions: property from [-100px] to [100px] at (2) should be [300px] +Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (-1) should be [-300px] +Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (0) should be [-100px] +Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (0.25) should be [-50px] +Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (0.75) should be [50px] +Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (1) should be [100px] +Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (2) should be [300px] +Fail CSS Animations: property from [-100px] to [100px] at (-1) should be [-300px] +Fail CSS Animations: property from [-100px] to [100px] at (0) should be [-100px] +Fail CSS Animations: property from [-100px] to [100px] at (0.25) should be [-50px] +Fail CSS Animations: property from [-100px] to [100px] at (0.75) should be [50px] +Fail CSS Animations: property from [-100px] to [100px] at (1) should be [100px] +Fail CSS Animations: property from [-100px] to [100px] at (2) should be [300px] +Fail Web Animations: property from [-100px] to [100px] at (-1) should be [-300px] +Fail Web Animations: property from [-100px] to [100px] at (0) should be [-100px] +Fail Web Animations: property from [-100px] to [100px] at (0.25) should be [-50px] +Fail Web Animations: property from [-100px] to [100px] at (0.75) should be [50px] +Fail Web Animations: property from [-100px] to [100px] at (1) should be [100px] +Fail Web Animations: property from [-100px] to [100px] at (2) should be [300px] +Fail CSS Transitions: property from [-100%] to [100%] at (-1) should be [-300%] +Fail CSS Transitions: property from [-100%] to [100%] at (0) should be [-100%] +Fail CSS Transitions: property from [-100%] to [100%] at (0.25) should be [-50%] +Fail CSS Transitions: property from [-100%] to [100%] at (0.75) should be [50%] +Fail CSS Transitions: property from [-100%] to [100%] at (1) should be [100%] +Fail CSS Transitions: property from [-100%] to [100%] at (2) should be [300%] +Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (-1) should be [-300%] +Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (0) should be [-100%] +Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (0.25) should be [-50%] +Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (0.75) should be [50%] +Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (1) should be [100%] +Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (2) should be [300%] +Fail CSS Animations: property from [-100%] to [100%] at (-1) should be [-300%] +Fail CSS Animations: property from [-100%] to [100%] at (0) should be [-100%] +Fail CSS Animations: property from [-100%] to [100%] at (0.25) should be [-50%] +Fail CSS Animations: property from [-100%] to [100%] at (0.75) should be [50%] +Fail CSS Animations: property from [-100%] to [100%] at (1) should be [100%] +Fail CSS Animations: property from [-100%] to [100%] at (2) should be [300%] +Fail Web Animations: property from [-100%] to [100%] at (-1) should be [-300%] +Fail Web Animations: property from [-100%] to [100%] at (0) should be [-100%] +Fail Web Animations: property from [-100%] to [100%] at (0.25) should be [-50%] +Fail Web Animations: property from [-100%] to [100%] at (0.75) should be [50%] +Fail Web Animations: property from [-100%] to [100%] at (1) should be [100%] +Fail Web Animations: property from [-100%] to [100%] at (2) should be [300%] +Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] +Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] +Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] +Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] +Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] +Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] +Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] +Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] +Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] +Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] +Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] +Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] +Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] +Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] +Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] +Fail Web Animations: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] +Fail Web Animations: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] +Fail Web Animations: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] +Fail Web Animations: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] +Fail Web Animations: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Fail Web Animations: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] +Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (-1) should be [140px 80px 20px] +Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (0) should be [220px 240px 260px] +Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (0.125) should be [230px 260px 290px] +Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (0.875) should be [290px 380px 470px] +Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (1) should be [300px 400px 500px] +Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (2) should be [380px 560px 740px] +Fail CSS Transitions with transition: all: property from [220px 240px 260px] to [300px 400px 500px] at (-1) should be [140px 80px 20px] +Fail CSS Transitions with transition: all: property from [220px 240px 260px] to [300px 400px 500px] at (0) should be [220px 240px 260px] +Fail CSS Transitions with transition: all: property from [220px 240px 260px] to [300px 400px 500px] at (0.125) should be [230px 260px 290px] +Fail CSS Transitions with transition: all: property from [220px 240px 260px] to [300px 400px 500px] at (0.875) should be [290px 380px 470px] +Fail CSS Transitions with transition: all: property from [220px 240px 260px] to [300px 400px 500px] at (1) should be [300px 400px 500px] +Fail CSS Transitions with transition: all: property from [220px 240px 260px] to [300px 400px 500px] at (2) should be [380px 560px 740px] +Fail CSS Animations: property from [220px 240px 260px] to [300px 400px 500px] at (-1) should be [140px 80px 20px] +Fail CSS Animations: property from [220px 240px 260px] to [300px 400px 500px] at (0) should be [220px 240px 260px] +Fail CSS Animations: property from [220px 240px 260px] to [300px 400px 500px] at (0.125) should be [230px 260px 290px] +Fail CSS Animations: property from [220px 240px 260px] to [300px 400px 500px] at (0.875) should be [290px 380px 470px] +Fail CSS Animations: property from [220px 240px 260px] to [300px 400px 500px] at (1) should be [300px 400px 500px] +Fail CSS Animations: property from [220px 240px 260px] to [300px 400px 500px] at (2) should be [380px 560px 740px] +Fail Web Animations: property from [220px 240px 260px] to [300px 400px 500px] at (-1) should be [140px 80px 20px] +Fail Web Animations: property from [220px 240px 260px] to [300px 400px 500px] at (0) should be [220px 240px 260px] +Fail Web Animations: property from [220px 240px 260px] to [300px 400px 500px] at (0.125) should be [230px 260px 290px] +Fail Web Animations: property from [220px 240px 260px] to [300px 400px 500px] at (0.875) should be [290px 380px 470px] +Fail Web Animations: property from [220px 240px 260px] to [300px 400px 500px] at (1) should be [300px 400px 500px] +Fail Web Animations: property from [220px 240px 260px] to [300px 400px 500px] at (2) should be [380px 560px 740px] +Fail CSS Transitions: property from [0px] to [-100px -50px 100px] at (-1) should be [100px 50px -100px] +Fail CSS Transitions: property from [0px] to [-100px -50px 100px] at (0) should be [0px] +Fail CSS Transitions: property from [0px] to [-100px -50px 100px] at (0.25) should be [-25px -12.5px 25px] +Fail CSS Transitions: property from [0px] to [-100px -50px 100px] at (0.75) should be [-75px -37.5px 75px] +Fail CSS Transitions: property from [0px] to [-100px -50px 100px] at (1) should be [-100px -50px 100px] +Fail CSS Transitions: property from [0px] to [-100px -50px 100px] at (2) should be [-200px -100px 200px] +Fail CSS Transitions with transition: all: property from [0px] to [-100px -50px 100px] at (-1) should be [100px 50px -100px] +Fail CSS Transitions with transition: all: property from [0px] to [-100px -50px 100px] at (0) should be [0px] +Fail CSS Transitions with transition: all: property from [0px] to [-100px -50px 100px] at (0.25) should be [-25px -12.5px 25px] +Fail CSS Transitions with transition: all: property from [0px] to [-100px -50px 100px] at (0.75) should be [-75px -37.5px 75px] +Fail CSS Transitions with transition: all: property from [0px] to [-100px -50px 100px] at (1) should be [-100px -50px 100px] +Fail CSS Transitions with transition: all: property from [0px] to [-100px -50px 100px] at (2) should be [-200px -100px 200px] +Fail CSS Animations: property from [0px] to [-100px -50px 100px] at (-1) should be [100px 50px -100px] +Fail CSS Animations: property from [0px] to [-100px -50px 100px] at (0) should be [0px] +Fail CSS Animations: property from [0px] to [-100px -50px 100px] at (0.25) should be [-25px -12.5px 25px] +Fail CSS Animations: property from [0px] to [-100px -50px 100px] at (0.75) should be [-75px -37.5px 75px] +Fail CSS Animations: property from [0px] to [-100px -50px 100px] at (1) should be [-100px -50px 100px] +Fail CSS Animations: property from [0px] to [-100px -50px 100px] at (2) should be [-200px -100px 200px] +Fail Web Animations: property from [0px] to [-100px -50px 100px] at (-1) should be [100px 50px -100px] +Fail Web Animations: property from [0px] to [-100px -50px 100px] at (0) should be [0px] +Fail Web Animations: property from [0px] to [-100px -50px 100px] at (0.25) should be [-25px -12.5px 25px] +Fail Web Animations: property from [0px] to [-100px -50px 100px] at (0.75) should be [-75px -37.5px 75px] +Fail Web Animations: property from [0px] to [-100px -50px 100px] at (1) should be [-100px -50px 100px] +Fail Web Animations: property from [0px] to [-100px -50px 100px] at (2) should be [-200px -100px 200px] +Fail CSS Transitions: property from [-100px -50px 100px] to [0px] at (-1) should be [-200px -100px 200px] +Fail CSS Transitions: property from [-100px -50px 100px] to [0px] at (0) should be [-100px -50px 100px] +Fail CSS Transitions: property from [-100px -50px 100px] to [0px] at (0.25) should be [-75px -37.5px 75px] +Fail CSS Transitions: property from [-100px -50px 100px] to [0px] at (0.75) should be [-25px -12.5px 25px] +Fail CSS Transitions: property from [-100px -50px 100px] to [0px] at (1) should be [0px] +Fail CSS Transitions: property from [-100px -50px 100px] to [0px] at (2) should be [100px 50px -100px] +Fail CSS Transitions with transition: all: property from [-100px -50px 100px] to [0px] at (-1) should be [-200px -100px 200px] +Fail CSS Transitions with transition: all: property from [-100px -50px 100px] to [0px] at (0) should be [-100px -50px 100px] +Fail CSS Transitions with transition: all: property from [-100px -50px 100px] to [0px] at (0.25) should be [-75px -37.5px 75px] +Fail CSS Transitions with transition: all: property from [-100px -50px 100px] to [0px] at (0.75) should be [-25px -12.5px 25px] +Fail CSS Transitions with transition: all: property from [-100px -50px 100px] to [0px] at (1) should be [0px] +Fail CSS Transitions with transition: all: property from [-100px -50px 100px] to [0px] at (2) should be [100px 50px -100px] +Fail CSS Animations: property from [-100px -50px 100px] to [0px] at (-1) should be [-200px -100px 200px] +Fail CSS Animations: property from [-100px -50px 100px] to [0px] at (0) should be [-100px -50px 100px] +Fail CSS Animations: property from [-100px -50px 100px] to [0px] at (0.25) should be [-75px -37.5px 75px] +Fail CSS Animations: property from [-100px -50px 100px] to [0px] at (0.75) should be [-25px -12.5px 25px] +Fail CSS Animations: property from [-100px -50px 100px] to [0px] at (1) should be [0px] +Fail CSS Animations: property from [-100px -50px 100px] to [0px] at (2) should be [100px 50px -100px] +Fail Web Animations: property from [-100px -50px 100px] to [0px] at (-1) should be [-200px -100px 200px] +Fail Web Animations: property from [-100px -50px 100px] to [0px] at (0) should be [-100px -50px 100px] +Fail Web Animations: property from [-100px -50px 100px] to [0px] at (0.25) should be [-75px -37.5px 75px] +Fail Web Animations: property from [-100px -50px 100px] to [0px] at (0.75) should be [-25px -12.5px 25px] +Fail Web Animations: property from [-100px -50px 100px] to [0px] at (1) should be [0px] +Fail Web Animations: property from [-100px -50px 100px] to [0px] at (2) should be [100px 50px -100px] +Fail CSS Transitions: property from [480px 400px 320px] to [240% 160%] at (-1) should be [calc(960px - 240%) calc(800px - 160%) 640px] +Fail CSS Transitions: property from [480px 400px 320px] to [240% 160%] at (0) should be [calc(0% + 480px) calc(0% + 400px) 320px] +Fail CSS Transitions: property from [480px 400px 320px] to [240% 160%] at (0.125) should be [calc(420px + 30%) calc(350px + 20%) 280px] +Fail CSS Transitions: property from [480px 400px 320px] to [240% 160%] at (0.875) should be [calc(210% + 60px) calc(140% + 50px) 40px] +Fail CSS Transitions: property from [480px 400px 320px] to [240% 160%] at (1) should be [240% 160%] +Fail CSS Transitions: property from [480px 400px 320px] to [240% 160%] at (2) should be [calc(480% - 480px) calc(320% - 400px) -320px] +Fail CSS Transitions with transition: all: property from [480px 400px 320px] to [240% 160%] at (-1) should be [calc(960px - 240%) calc(800px - 160%) 640px] +Fail CSS Transitions with transition: all: property from [480px 400px 320px] to [240% 160%] at (0) should be [calc(0% + 480px) calc(0% + 400px) 320px] +Fail CSS Transitions with transition: all: property from [480px 400px 320px] to [240% 160%] at (0.125) should be [calc(420px + 30%) calc(350px + 20%) 280px] +Fail CSS Transitions with transition: all: property from [480px 400px 320px] to [240% 160%] at (0.875) should be [calc(210% + 60px) calc(140% + 50px) 40px] +Fail CSS Transitions with transition: all: property from [480px 400px 320px] to [240% 160%] at (1) should be [240% 160%] +Fail CSS Transitions with transition: all: property from [480px 400px 320px] to [240% 160%] at (2) should be [calc(480% - 480px) calc(320% - 400px) -320px] +Fail CSS Animations: property from [480px 400px 320px] to [240% 160%] at (-1) should be [calc(960px - 240%) calc(800px - 160%) 640px] +Fail CSS Animations: property from [480px 400px 320px] to [240% 160%] at (0) should be [calc(0% + 480px) calc(0% + 400px) 320px] +Fail CSS Animations: property from [480px 400px 320px] to [240% 160%] at (0.125) should be [calc(420px + 30%) calc(350px + 20%) 280px] +Fail CSS Animations: property from [480px 400px 320px] to [240% 160%] at (0.875) should be [calc(210% + 60px) calc(140% + 50px) 40px] +Fail CSS Animations: property from [480px 400px 320px] to [240% 160%] at (1) should be [240% 160%] +Fail CSS Animations: property from [480px 400px 320px] to [240% 160%] at (2) should be [calc(480% - 480px) calc(320% - 400px) -320px] +Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (-1) should be [calc(960px - 240%) calc(800px - 160%) 640px] +Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (0) should be [calc(0% + 480px) calc(0% + 400px) 320px] +Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (0.125) should be [calc(420px + 30%) calc(350px + 20%) 280px] +Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (0.875) should be [calc(210% + 60px) calc(140% + 50px) 40px] +Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (1) should be [240% 160%] +Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (2) should be [calc(480% - 480px) calc(320% - 400px) -320px] +Fail CSS Transitions: property from [none] to [none] at (-1) should be [none] +Fail CSS Transitions: property from [none] to [none] at (0) should be [none] +Fail CSS Transitions: property from [none] to [none] at (0.125) should be [none] +Fail CSS Transitions: property from [none] to [none] at (0.875) should be [none] +Fail CSS Transitions: property from [none] to [none] at (1) should be [none] +Fail CSS Transitions: property from [none] to [none] at (2) should be [none] +Fail CSS Transitions with transition: all: property from [none] to [none] at (-1) should be [none] +Fail CSS Transitions with transition: all: property from [none] to [none] at (0) should be [none] +Fail CSS Transitions with transition: all: property from [none] to [none] at (0.125) should be [none] +Fail CSS Transitions with transition: all: property from [none] to [none] at (0.875) should be [none] +Fail CSS Transitions with transition: all: property from [none] to [none] at (1) should be [none] +Fail CSS Transitions with transition: all: property from [none] to [none] at (2) should be [none] +Fail CSS Animations: property from [none] to [none] at (-1) should be [none] +Fail CSS Animations: property from [none] to [none] at (0) should be [none] +Fail CSS Animations: property from [none] to [none] at (0.125) should be [none] +Fail CSS Animations: property from [none] to [none] at (0.875) should be [none] +Fail CSS Animations: property from [none] to [none] at (1) should be [none] +Fail CSS Animations: property from [none] to [none] at (2) should be [none] +Fail Web Animations: property from [none] to [none] at (-1) should be [none] +Fail Web Animations: property from [none] to [none] at (0) should be [none] +Fail Web Animations: property from [none] to [none] at (0.125) should be [none] +Fail Web Animations: property from [none] to [none] at (0.875) should be [none] +Fail Web Animations: property from [none] to [none] at (1) should be [none] +Fail Web Animations: property from [none] to [none] at (2) should be [none] +Fail CSS Transitions: property from [none] to [8px 80% 800px] at (-1) should be [-8px -80% -800px] +Fail CSS Transitions: property from [none] to [8px 80% 800px] at (0) should be [0px 0%] +Fail CSS Transitions: property from [none] to [8px 80% 800px] at (0.125) should be [1px 10% 100px] +Fail CSS Transitions: property from [none] to [8px 80% 800px] at (0.875) should be [7px 70% 700px] +Fail CSS Transitions: property from [none] to [8px 80% 800px] at (1) should be [8px 80% 800px] +Fail CSS Transitions: property from [none] to [8px 80% 800px] at (2) should be [16px 160% 1600px] +Fail CSS Transitions with transition: all: property from [none] to [8px 80% 800px] at (-1) should be [-8px -80% -800px] +Fail CSS Transitions with transition: all: property from [none] to [8px 80% 800px] at (0) should be [0px 0%] +Fail CSS Transitions with transition: all: property from [none] to [8px 80% 800px] at (0.125) should be [1px 10% 100px] +Fail CSS Transitions with transition: all: property from [none] to [8px 80% 800px] at (0.875) should be [7px 70% 700px] +Fail CSS Transitions with transition: all: property from [none] to [8px 80% 800px] at (1) should be [8px 80% 800px] +Fail CSS Transitions with transition: all: property from [none] to [8px 80% 800px] at (2) should be [16px 160% 1600px] +Fail CSS Animations: property from [none] to [8px 80% 800px] at (-1) should be [-8px -80% -800px] +Fail CSS Animations: property from [none] to [8px 80% 800px] at (0) should be [0px 0%] +Fail CSS Animations: property from [none] to [8px 80% 800px] at (0.125) should be [1px 10% 100px] +Fail CSS Animations: property from [none] to [8px 80% 800px] at (0.875) should be [7px 70% 700px] +Fail CSS Animations: property from [none] to [8px 80% 800px] at (1) should be [8px 80% 800px] +Fail CSS Animations: property from [none] to [8px 80% 800px] at (2) should be [16px 160% 1600px] +Fail Web Animations: property from [none] to [8px 80% 800px] at (-1) should be [-8px -80% -800px] +Fail Web Animations: property from [none] to [8px 80% 800px] at (0) should be [0px 0%] +Fail Web Animations: property from [none] to [8px 80% 800px] at (0.125) should be [1px 10% 100px] +Fail Web Animations: property from [none] to [8px 80% 800px] at (0.875) should be [7px 70% 700px] +Fail Web Animations: property from [none] to [8px 80% 800px] at (1) should be [8px 80% 800px] +Fail Web Animations: property from [none] to [8px 80% 800px] at (2) should be [16px 160% 1600px] +Fail CSS Transitions: property from neutral to [20px] at (-1) should be [0px] +Fail CSS Transitions: property from neutral to [20px] at (0) should be [10px] +Fail CSS Transitions: property from neutral to [20px] at (0.25) should be [12.5px] +Fail CSS Transitions: property from neutral to [20px] at (0.75) should be [17.5px] +Fail CSS Transitions: property from neutral to [20px] at (1) should be [20px] +Fail CSS Transitions: property from neutral to [20px] at (2) should be [30px] +Fail CSS Transitions with transition: all: property from neutral to [20px] at (-1) should be [0px] +Fail CSS Transitions with transition: all: property from neutral to [20px] at (0) should be [10px] +Fail CSS Transitions with transition: all: property from neutral to [20px] at (0.25) should be [12.5px] +Fail CSS Transitions with transition: all: property from neutral to [20px] at (0.75) should be [17.5px] +Fail CSS Transitions with transition: all: property from neutral to [20px] at (1) should be [20px] +Fail CSS Transitions with transition: all: property from neutral to [20px] at (2) should be [30px] +Fail CSS Animations: property from neutral to [20px] at (-1) should be [0px] +Fail CSS Animations: property from neutral to [20px] at (0) should be [10px] +Fail CSS Animations: property from neutral to [20px] at (0.25) should be [12.5px] +Fail CSS Animations: property from neutral to [20px] at (0.75) should be [17.5px] +Fail CSS Animations: property from neutral to [20px] at (1) should be [20px] +Fail CSS Animations: property from neutral to [20px] at (2) should be [30px] +Fail Web Animations: property from neutral to [20px] at (-1) should be [0px] +Fail Web Animations: property from neutral to [20px] at (0) should be [10px] +Fail Web Animations: property from neutral to [20px] at (0.25) should be [12.5px] +Fail Web Animations: property from neutral to [20px] at (0.75) should be [17.5px] +Fail Web Animations: property from neutral to [20px] at (1) should be [20px] +Fail Web Animations: property from neutral to [20px] at (2) should be [30px] +Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (-1) should be [-200px -100px -200px] +Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (0) should be [0px] +Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (0.25) should be [50px 25px 50px] +Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (0.75) should be [150px 75px 150px] +Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (2) should be [400px 200px 400px] +Fail CSS Transitions with transition: all: property from [initial] to [200px 100px 200px] at (-1) should be [-200px -100px -200px] +Fail CSS Transitions with transition: all: property from [initial] to [200px 100px 200px] at (0) should be [0px] +Fail CSS Transitions with transition: all: property from [initial] to [200px 100px 200px] at (0.25) should be [50px 25px 50px] +Fail CSS Transitions with transition: all: property from [initial] to [200px 100px 200px] at (0.75) should be [150px 75px 150px] +Fail CSS Transitions with transition: all: property from [initial] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail CSS Transitions with transition: all: property from [initial] to [200px 100px 200px] at (2) should be [400px 200px 400px] +Fail CSS Animations: property from [initial] to [200px 100px 200px] at (-1) should be [-200px -100px -200px] +Fail CSS Animations: property from [initial] to [200px 100px 200px] at (0) should be [0px] +Fail CSS Animations: property from [initial] to [200px 100px 200px] at (0.25) should be [50px 25px 50px] +Fail CSS Animations: property from [initial] to [200px 100px 200px] at (0.75) should be [150px 75px 150px] +Fail CSS Animations: property from [initial] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail CSS Animations: property from [initial] to [200px 100px 200px] at (2) should be [400px 200px 400px] +Fail Web Animations: property from [initial] to [200px 100px 200px] at (-1) should be [-200px -100px -200px] +Fail Web Animations: property from [initial] to [200px 100px 200px] at (0) should be [0px] +Fail Web Animations: property from [initial] to [200px 100px 200px] at (0.25) should be [50px 25px 50px] +Fail Web Animations: property from [initial] to [200px 100px 200px] at (0.75) should be [150px 75px 150px] +Fail Web Animations: property from [initial] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail Web Animations: property from [initial] to [200px 100px 200px] at (2) should be [400px 200px 400px] +Fail CSS Transitions: property from [200px 100px 400px] to [initial] at (-1) should be [400px 200px 800px] +Fail CSS Transitions: property from [200px 100px 400px] to [initial] at (0) should be [200px 100px 400px] +Fail CSS Transitions: property from [200px 100px 400px] to [initial] at (0.25) should be [150px 75px 300px] +Fail CSS Transitions: property from [200px 100px 400px] to [initial] at (0.75) should be [50px 25px 100px] +Fail CSS Transitions: property from [200px 100px 400px] to [initial] at (1) should be [0px] +Fail CSS Transitions: property from [200px 100px 400px] to [initial] at (2) should be [-200px -100px -400px] +Fail CSS Transitions with transition: all: property from [200px 100px 400px] to [initial] at (-1) should be [400px 200px 800px] +Fail CSS Transitions with transition: all: property from [200px 100px 400px] to [initial] at (0) should be [200px 100px 400px] +Fail CSS Transitions with transition: all: property from [200px 100px 400px] to [initial] at (0.25) should be [150px 75px 300px] +Fail CSS Transitions with transition: all: property from [200px 100px 400px] to [initial] at (0.75) should be [50px 25px 100px] +Fail CSS Transitions with transition: all: property from [200px 100px 400px] to [initial] at (1) should be [0px] +Fail CSS Transitions with transition: all: property from [200px 100px 400px] to [initial] at (2) should be [-200px -100px -400px] +Fail CSS Animations: property from [200px 100px 400px] to [initial] at (-1) should be [400px 200px 800px] +Fail CSS Animations: property from [200px 100px 400px] to [initial] at (0) should be [200px 100px 400px] +Fail CSS Animations: property from [200px 100px 400px] to [initial] at (0.25) should be [150px 75px 300px] +Fail CSS Animations: property from [200px 100px 400px] to [initial] at (0.75) should be [50px 25px 100px] +Fail CSS Animations: property from [200px 100px 400px] to [initial] at (1) should be [0px] +Fail CSS Animations: property from [200px 100px 400px] to [initial] at (2) should be [-200px -100px -400px] +Fail Web Animations: property from [200px 100px 400px] to [initial] at (-1) should be [400px 200px 800px] +Fail Web Animations: property from [200px 100px 400px] to [initial] at (0) should be [200px 100px 400px] +Fail Web Animations: property from [200px 100px 400px] to [initial] at (0.25) should be [150px 75px 300px] +Fail Web Animations: property from [200px 100px 400px] to [initial] at (0.75) should be [50px 25px 100px] +Fail Web Animations: property from [200px 100px 400px] to [initial] at (1) should be [0px] +Fail Web Animations: property from [200px 100px 400px] to [initial] at (2) should be [-200px -100px -400px] +Fail CSS Transitions: property from [unset] to [20px] at (-1) should be [-20px] +Fail CSS Transitions: property from [unset] to [20px] at (0) should be [0px] +Fail CSS Transitions: property from [unset] to [20px] at (0.25) should be [5px] +Fail CSS Transitions: property from [unset] to [20px] at (0.75) should be [15px] +Fail CSS Transitions: property from [unset] to [20px] at (1) should be [20px] +Fail CSS Transitions: property from [unset] to [20px] at (2) should be [40px] +Fail CSS Transitions with transition: all: property from [unset] to [20px] at (-1) should be [-20px] +Fail CSS Transitions with transition: all: property from [unset] to [20px] at (0) should be [0px] +Fail CSS Transitions with transition: all: property from [unset] to [20px] at (0.25) should be [5px] +Fail CSS Transitions with transition: all: property from [unset] to [20px] at (0.75) should be [15px] +Fail CSS Transitions with transition: all: property from [unset] to [20px] at (1) should be [20px] +Fail CSS Transitions with transition: all: property from [unset] to [20px] at (2) should be [40px] +Fail CSS Animations: property from [unset] to [20px] at (-1) should be [-20px] +Fail CSS Animations: property from [unset] to [20px] at (0) should be [0px] +Fail CSS Animations: property from [unset] to [20px] at (0.25) should be [5px] +Fail CSS Animations: property from [unset] to [20px] at (0.75) should be [15px] +Fail CSS Animations: property from [unset] to [20px] at (1) should be [20px] +Fail CSS Animations: property from [unset] to [20px] at (2) should be [40px] +Fail Web Animations: property from [unset] to [20px] at (-1) should be [-20px] +Fail Web Animations: property from [unset] to [20px] at (0) should be [0px] +Fail Web Animations: property from [unset] to [20px] at (0.25) should be [5px] +Fail Web Animations: property from [unset] to [20px] at (0.75) should be [15px] +Fail Web Animations: property from [unset] to [20px] at (1) should be [20px] +Fail Web Animations: property from [unset] to [20px] at (2) should be [40px] +Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (-1) should be [0px 300px 400px] +Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (0) should be [100px 200px 300px] +Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (0.25) should be [125px 175px 275px] +Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (0.75) should be [175px 125px 225px] +Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (2) should be [300px 0px 100px] +Fail CSS Transitions with transition: all: property from [inherit] to [200px 100px 200px] at (-1) should be [0px 300px 400px] +Fail CSS Transitions with transition: all: property from [inherit] to [200px 100px 200px] at (0) should be [100px 200px 300px] +Fail CSS Transitions with transition: all: property from [inherit] to [200px 100px 200px] at (0.25) should be [125px 175px 275px] +Fail CSS Transitions with transition: all: property from [inherit] to [200px 100px 200px] at (0.75) should be [175px 125px 225px] +Fail CSS Transitions with transition: all: property from [inherit] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail CSS Transitions with transition: all: property from [inherit] to [200px 100px 200px] at (2) should be [300px 0px 100px] +Fail CSS Animations: property from [inherit] to [200px 100px 200px] at (-1) should be [0px 300px 400px] +Fail CSS Animations: property from [inherit] to [200px 100px 200px] at (0) should be [100px 200px 300px] +Fail CSS Animations: property from [inherit] to [200px 100px 200px] at (0.25) should be [125px 175px 275px] +Fail CSS Animations: property from [inherit] to [200px 100px 200px] at (0.75) should be [175px 125px 225px] +Fail CSS Animations: property from [inherit] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail CSS Animations: property from [inherit] to [200px 100px 200px] at (2) should be [300px 0px 100px] +Fail Web Animations: property from [inherit] to [200px 100px 200px] at (-1) should be [0px 300px 400px] +Fail Web Animations: property from [inherit] to [200px 100px 200px] at (0) should be [100px 200px 300px] +Fail Web Animations: property from [inherit] to [200px 100px 200px] at (0.25) should be [125px 175px 275px] +Fail Web Animations: property from [inherit] to [200px 100px 200px] at (0.75) should be [175px 125px 225px] +Fail Web Animations: property from [inherit] to [200px 100px 200px] at (1) should be [200px 100px 200px] +Fail Web Animations: property from [inherit] to [200px 100px 200px] at (2) should be [300px 0px 100px] +Fail CSS Transitions: property from [200px 100px 200px] to [inherit] at (-1) should be [300px 0px 100px] +Fail CSS Transitions: property from [200px 100px 200px] to [inherit] at (0) should be [200px 100px 200px] +Fail CSS Transitions: property from [200px 100px 200px] to [inherit] at (0.25) should be [175px 125px 225px] +Fail CSS Transitions: property from [200px 100px 200px] to [inherit] at (0.75) should be [125px 175px 275px] +Fail CSS Transitions: property from [200px 100px 200px] to [inherit] at (1) should be [100px 200px 300px] +Fail CSS Transitions: property from [200px 100px 200px] to [inherit] at (2) should be [0px 300px 400px] +Fail CSS Transitions with transition: all: property from [200px 100px 200px] to [inherit] at (-1) should be [300px 0px 100px] +Fail CSS Transitions with transition: all: property from [200px 100px 200px] to [inherit] at (0) should be [200px 100px 200px] +Fail CSS Transitions with transition: all: property from [200px 100px 200px] to [inherit] at (0.25) should be [175px 125px 225px] +Fail CSS Transitions with transition: all: property from [200px 100px 200px] to [inherit] at (0.75) should be [125px 175px 275px] +Fail CSS Transitions with transition: all: property from [200px 100px 200px] to [inherit] at (1) should be [100px 200px 300px] +Fail CSS Transitions with transition: all: property from [200px 100px 200px] to [inherit] at (2) should be [0px 300px 400px] +Fail CSS Animations: property from [200px 100px 200px] to [inherit] at (-1) should be [300px 0px 100px] +Fail CSS Animations: property from [200px 100px 200px] to [inherit] at (0) should be [200px 100px 200px] +Fail CSS Animations: property from [200px 100px 200px] to [inherit] at (0.25) should be [175px 125px 225px] +Fail CSS Animations: property from [200px 100px 200px] to [inherit] at (0.75) should be [125px 175px 275px] +Fail CSS Animations: property from [200px 100px 200px] to [inherit] at (1) should be [100px 200px 300px] +Fail CSS Animations: property from [200px 100px 200px] to [inherit] at (2) should be [0px 300px 400px] +Fail Web Animations: property from [200px 100px 200px] to [inherit] at (-1) should be [300px 0px 100px] +Fail Web Animations: property from [200px 100px 200px] to [inherit] at (0) should be [200px 100px 200px] +Fail Web Animations: property from [200px 100px 200px] to [inherit] at (0.25) should be [175px 125px 225px] +Fail Web Animations: property from [200px 100px 200px] to [inherit] at (0.75) should be [125px 175px 275px] +Fail Web Animations: property from [200px 100px 200px] to [inherit] at (1) should be [100px 200px 300px] +Fail Web Animations: property from [200px 100px 200px] to [inherit] at (2) should be [0px 300px 400px] +Fail CSS Transitions: property from [initial] to [inherit] at (-1) should be [-100px -200px -300px] +Fail CSS Transitions: property from [initial] to [inherit] at (0) should be [0px] +Fail CSS Transitions: property from [initial] to [inherit] at (0.25) should be [25px 50px 75px] +Fail CSS Transitions: property from [initial] to [inherit] at (0.75) should be [75px 150px 225px] +Fail CSS Transitions: property from [initial] to [inherit] at (1) should be [100px 200px 300px] +Fail CSS Transitions: property from [initial] to [inherit] at (2) should be [200px 400px 600px] +Fail CSS Transitions with transition: all: property from [initial] to [inherit] at (-1) should be [-100px -200px -300px] +Fail CSS Transitions with transition: all: property from [initial] to [inherit] at (0) should be [0px] +Fail CSS Transitions with transition: all: property from [initial] to [inherit] at (0.25) should be [25px 50px 75px] +Fail CSS Transitions with transition: all: property from [initial] to [inherit] at (0.75) should be [75px 150px 225px] +Fail CSS Transitions with transition: all: property from [initial] to [inherit] at (1) should be [100px 200px 300px] +Fail CSS Transitions with transition: all: property from [initial] to [inherit] at (2) should be [200px 400px 600px] +Fail CSS Animations: property from [initial] to [inherit] at (-1) should be [-100px -200px -300px] +Fail CSS Animations: property from [initial] to [inherit] at (0) should be [0px] +Fail CSS Animations: property from [initial] to [inherit] at (0.25) should be [25px 50px 75px] +Fail CSS Animations: property from [initial] to [inherit] at (0.75) should be [75px 150px 225px] +Fail CSS Animations: property from [initial] to [inherit] at (1) should be [100px 200px 300px] +Fail CSS Animations: property from [initial] to [inherit] at (2) should be [200px 400px 600px] +Fail Web Animations: property from [initial] to [inherit] at (-1) should be [-100px -200px -300px] +Fail Web Animations: property from [initial] to [inherit] at (0) should be [0px] +Fail Web Animations: property from [initial] to [inherit] at (0.25) should be [25px 50px 75px] +Fail Web Animations: property from [initial] to [inherit] at (0.75) should be [75px 150px 225px] +Fail Web Animations: property from [initial] to [inherit] at (1) should be [100px 200px 300px] +Fail Web Animations: property from [initial] to [inherit] at (2) should be [200px 400px 600px] +Fail CSS Transitions: property from [inherit] to [initial] at (-1) should be [200px 400px 600px] +Fail CSS Transitions: property from [inherit] to [initial] at (0) should be [100px 200px 300px] +Fail CSS Transitions: property from [inherit] to [initial] at (0.25) should be [75px 150px 225px] +Fail CSS Transitions: property from [inherit] to [initial] at (0.75) should be [25px 50px 75px] +Fail CSS Transitions: property from [inherit] to [initial] at (1) should be [0px] +Fail CSS Transitions: property from [inherit] to [initial] at (2) should be [-100px -200px -300px] +Fail CSS Transitions with transition: all: property from [inherit] to [initial] at (-1) should be [200px 400px 600px] +Fail CSS Transitions with transition: all: property from [inherit] to [initial] at (0) should be [100px 200px 300px] +Fail CSS Transitions with transition: all: property from [inherit] to [initial] at (0.25) should be [75px 150px 225px] +Fail CSS Transitions with transition: all: property from [inherit] to [initial] at (0.75) should be [25px 50px 75px] +Fail CSS Transitions with transition: all: property from [inherit] to [initial] at (1) should be [0px] +Fail CSS Transitions with transition: all: property from [inherit] to [initial] at (2) should be [-100px -200px -300px] +Fail CSS Animations: property from [inherit] to [initial] at (-1) should be [200px 400px 600px] +Fail CSS Animations: property from [inherit] to [initial] at (0) should be [100px 200px 300px] +Fail CSS Animations: property from [inherit] to [initial] at (0.25) should be [75px 150px 225px] +Fail CSS Animations: property from [inherit] to [initial] at (0.75) should be [25px 50px 75px] +Fail CSS Animations: property from [inherit] to [initial] at (1) should be [0px] +Fail CSS Animations: property from [inherit] to [initial] at (2) should be [-100px -200px -300px] +Fail Web Animations: property from [inherit] to [initial] at (-1) should be [200px 400px 600px] +Fail Web Animations: property from [inherit] to [initial] at (0) should be [100px 200px 300px] +Fail Web Animations: property from [inherit] to [initial] at (0.25) should be [75px 150px 225px] +Fail Web Animations: property from [inherit] to [initial] at (0.75) should be [25px 50px 75px] +Fail Web Animations: property from [inherit] to [initial] at (1) should be [0px] +Fail Web Animations: property from [inherit] to [initial] at (2) should be [-100px -200px -300px] \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.txt new file mode 100644 index 000000000000..7ba0aee1d5f4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.txt @@ -0,0 +1,27 @@ +Summary + +Harness status: OK + +Rerun + +Found 17 tests + +17 Pass +Details +Result Test Name MessagePass Property rotate value 'none' +Pass Property rotate value '0deg' +Pass Property rotate value '100 200 300 400grad' +Pass Property rotate value '400grad 100 200 300' +Pass Property rotate value '0 0 0 400grad' +Pass Property rotate value 'x 400grad' +Pass Property rotate value '400grad x' +Pass Property rotate value '0.5 0 0 400grad' +Pass Property rotate value '1 0 0 400grad' +Pass Property rotate value 'y 400grad' +Pass Property rotate value '400grad y' +Pass Property rotate value '0 0.5 0 400grad' +Pass Property rotate value '0 1 0 400grad' +Pass Property rotate value '400grad' +Pass Property rotate value '400grad z' +Pass Property rotate value '0 0 0.5 400grad' +Pass Property rotate value '0 0 1 400grad' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.txt new file mode 100644 index 000000000000..53d3c999d641 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.txt @@ -0,0 +1,19 @@ +Summary + +Harness status: OK + +Rerun + +Found 9 tests + +9 Pass +Details +Result Test Name MessagePass e.style['rotate'] = "100px" should not set the property value +Pass e.style['rotate'] = "100 400deg" should not set the property value +Pass e.style['rotate'] = "100 200 400deg" should not set the property value +Pass e.style['rotate'] = "100 200 300 500 400deg" should not set the property value +Pass e.style['rotate'] = "x y 45deg" should not set the property value +Pass e.style['rotate'] = "45deg x y" should not set the property value +Pass e.style['rotate'] = "z" should not set the property value +Pass e.style['rotate'] = "1 2" should not set the property value +Pass e.style['rotate'] = "1 2 3" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.txt new file mode 100644 index 000000000000..52b781fc2316 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.txt @@ -0,0 +1,28 @@ +Summary + +Harness status: OK + +Rerun + +Found 17 tests + +2 Pass +15 Fail +Details +Result Test Name MessagePass e.style['rotate'] = "none" should set the property value +Pass e.style['rotate'] = "0deg" should set the property value +Fail e.style['rotate'] = "100 200 300 400grad" should set the property value +Fail e.style['rotate'] = "400grad 100 200 300" should set the property value +Fail e.style['rotate'] = "0 0 0 400grad" should set the property value +Fail e.style['rotate'] = "x 400grad" should set the property value +Fail e.style['rotate'] = "400grad x" should set the property value +Fail e.style['rotate'] = "0.5 0 0 400grad" should set the property value +Fail e.style['rotate'] = "1 0 0 400grad" should set the property value +Fail e.style['rotate'] = "y 400grad" should set the property value +Fail e.style['rotate'] = "400grad y" should set the property value +Fail e.style['rotate'] = "0 0.5 0 400grad" should set the property value +Fail e.style['rotate'] = "0 1 0 400grad" should set the property value +Fail e.style['rotate'] = "400grad" should set the property value +Fail e.style['rotate'] = "400grad z" should set the property value +Fail e.style['rotate'] = "0 0 0.5 400grad" should set the property value +Fail e.style['rotate'] = "0 0 1 400grad" should set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt new file mode 100644 index 000000000000..e68f6c0f28cc --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt @@ -0,0 +1,32 @@ +Summary + +Harness status: OK + +Rerun + +Found 22 tests + +22 Fail +Details +Result Test Name MessageFail Property scale value 'none' +Fail Property scale value '1' +Fail Property scale value '1%' +Fail Property scale value '100' +Fail Property scale value '100%' +Fail Property scale value '100 100' +Fail Property scale value '100% 100%' +Fail Property scale value '100 100 1' +Fail Property scale value '100% 100% 1' +Fail Property scale value '-100' +Fail Property scale value '-100%' +Fail Property scale value '-100 -100' +Fail Property scale value '-100% -100%' +Fail Property scale value '-100 -100 1' +Fail Property scale value '-100% -100% 1' +Fail Property scale value '100 200' +Fail Property scale value '100% 200%' +Fail Property scale value '100 200 1' +Fail Property scale value '100% 200% 1' +Fail Property scale value '100 200 300' +Fail Property scale value '100 100 2' +Fail Property scale value '100% 200% 300%' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.txt new file mode 100644 index 000000000000..5b15ea519ac8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.txt @@ -0,0 +1,15 @@ +Summary + +Harness status: OK + +Rerun + +Found 5 tests + +5 Pass +Details +Result Test Name MessagePass e.style['scale'] = "100px" should not set the property value +Pass e.style['scale'] = "100 200 300 400" should not set the property value +Pass e.style['scale'] = "1 junk" should not set the property value +Pass e.style['scale'] = "1 2 junk" should not set the property value +Pass e.style['scale'] = "1 2 3 junk" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt new file mode 100644 index 000000000000..df230dc5ee65 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt @@ -0,0 +1,32 @@ +Summary + +Harness status: OK + +Rerun + +Found 22 tests + +22 Fail +Details +Result Test Name MessageFail e.style['scale'] = "none" should set the property value +Fail e.style['scale'] = "1" should set the property value +Fail e.style['scale'] = "1%" should set the property value +Fail e.style['scale'] = "100" should set the property value +Fail e.style['scale'] = "100%" should set the property value +Fail e.style['scale'] = "100 100" should set the property value +Fail e.style['scale'] = "100% 100%" should set the property value +Fail e.style['scale'] = "100 100 1" should set the property value +Fail e.style['scale'] = "100% 100% 1" should set the property value +Fail e.style['scale'] = "-100" should set the property value +Fail e.style['scale'] = "-100%" should set the property value +Fail e.style['scale'] = "-100 -100" should set the property value +Fail e.style['scale'] = "-100% -100%" should set the property value +Fail e.style['scale'] = "-100 -100 1" should set the property value +Fail e.style['scale'] = "-100% -100% 1" should set the property value +Fail e.style['scale'] = "100 200" should set the property value +Fail e.style['scale'] = "100% 200%" should set the property value +Fail e.style['scale'] = "100 200 1" should set the property value +Fail e.style['scale'] = "100% 200% 1" should set the property value +Fail e.style['scale'] = "100 200 300" should set the property value +Fail e.style['scale'] = "100 100 2" should set the property value +Fail e.style['scale'] = "100% 200% 300%" should set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt new file mode 100644 index 000000000000..9d39c55f0f47 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt @@ -0,0 +1,29 @@ +Summary + +Harness status: OK + +Rerun + +Found 19 tests + +19 Fail +Details +Result Test Name MessageFail Property translate value 'none' +Fail Property translate value '0px' +Fail Property translate value '100%' +Fail Property translate value '100px 0px' +Fail Property translate value '100px 0.1px' +Fail Property translate value '100px 0%' +Fail Property translate value '100px calc(10px - 10%)' +Fail Property translate value '100px 200%' +Fail Property translate value '100% 200px' +Fail Property translate value '100px 200px 0px' +Fail Property translate value '100px 0px 300px' +Fail Property translate value '100px 0px 0px' +Fail Property translate value '100px 200px 300px' +Fail Property translate value '100% 200% 300px' +Fail Property translate value '100% 0% 200px' +Fail Property translate value '0% 0% 100px' +Fail Property translate value '0em 0em 100px' +Fail Property translate value '0' +Fail Property translate value '1px 2px 0' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.txt new file mode 100644 index 000000000000..e768bddc40e8 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.txt @@ -0,0 +1,16 @@ +Summary + +Harness status: OK + +Rerun + +Found 6 tests + +6 Pass +Details +Result Test Name MessagePass e.style['translate'] = "100deg" should not set the property value +Pass e.style['translate'] = "100px 200px 300%" should not set the property value +Pass e.style['translate'] = "100px 200px calc(30px + 30%)" should not set the property value +Pass e.style['translate'] = "100px junk" should not set the property value +Pass e.style['translate'] = "100px 200px junk" should not set the property value +Pass e.style['translate'] = "100px 200px 300px junk" should not set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt new file mode 100644 index 000000000000..e1e0562b6a46 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt @@ -0,0 +1,30 @@ +Summary + +Harness status: OK + +Rerun + +Found 20 tests + +20 Fail +Details +Result Test Name MessageFail e.style['translate'] = "none" should set the property value +Fail e.style['translate'] = "0px" should set the property value +Fail e.style['translate'] = "100%" should set the property value +Fail e.style['translate'] = "100px 0px" should set the property value +Fail e.style['translate'] = "100px 0.1px" should set the property value +Fail e.style['translate'] = "100px 0%" should set the property value +Fail e.style['translate'] = "100px calc(10px - 10%)" should set the property value +Fail e.style['translate'] = "100px 200%" should set the property value +Fail e.style['translate'] = "100% 200px" should set the property value +Fail e.style['translate'] = "100px 200px 0px" should set the property value +Fail e.style['translate'] = "100px 0px 300px" should set the property value +Fail e.style['translate'] = "100px 0px 0px" should set the property value +Fail e.style['translate'] = "100px 200px 300px" should set the property value +Fail e.style['translate'] = "100% 200% 300px" should set the property value +Fail e.style['translate'] = "100% 0% 200px" should set the property value +Fail e.style['translate'] = "0% 0% 100px" should set the property value +Fail e.style['translate'] = "0em 0em 100px" should set the property value +Fail e.style['translate'] = "calc(10% + 10px) calc(20% + 20px) calc(30em + 30px)" should set the property value +Fail e.style['translate'] = "0" should set the property value +Fail e.style['translate'] = "1px 2px 0" should set the property value \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/animation/translate-interpolation.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/animation/translate-interpolation.html new file mode 100644 index 000000000000..bd69ad524d5a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/animation/translate-interpolation.html @@ -0,0 +1,271 @@ + + + + + translate interpolation + + + + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.html new file mode 100644 index 000000000000..1d705537084b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-computed.html @@ -0,0 +1,47 @@ + + + + +CSS Transform Module Level 2: rotate computed values. + + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.html new file mode 100644 index 000000000000..eed6d02a731d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-invalid.html @@ -0,0 +1,27 @@ + + + + +CSS Transform Module Level 2: parsing rotate with invalid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.html new file mode 100644 index 000000000000..03d0a366f001 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/rotate-parsing-valid.html @@ -0,0 +1,45 @@ + + + + +CSS Transform Module Level 2: parsing rotate with valid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-computed.html new file mode 100644 index 000000000000..e749aba17f80 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-computed.html @@ -0,0 +1,44 @@ + + + + +CSS Transform Module Level 2: scale computed values. + + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.html new file mode 100644 index 000000000000..96ae36ff5884 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-invalid.html @@ -0,0 +1,23 @@ + + + + +CSS Transform Module Level 2: parsing scale with invalid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-valid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-valid.html new file mode 100644 index 000000000000..b4da73fa3a0e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/scale-parsing-valid.html @@ -0,0 +1,42 @@ + + + + +CSS Transform Module Level 2: parsing scale with valid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-computed.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-computed.html new file mode 100644 index 000000000000..9b446cf66213 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-computed.html @@ -0,0 +1,42 @@ + + + + +CSS Transform Module Level 2: translate computed values. + + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.html new file mode 100644 index 000000000000..0a956a92433a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-invalid.html @@ -0,0 +1,24 @@ + + + + +CSS Transform Module Level 2: parsing translate with invalid values + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-valid.html b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-valid.html new file mode 100644 index 000000000000..05503ed376cb --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/css/css-transforms/parsing/translate-parsing-valid.html @@ -0,0 +1,42 @@ + + + + +CSS Transform Module Level 2: parsing translate with valid values + + + + + + + + + + From 6836d4edb16da905e81b3ba46b5a27d2c4a3c7a4 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 22 Nov 2024 16:11:33 +0100 Subject: [PATCH 011/397] LibWeb: Fix incomplete plumbing for individual `rotate` CSS property --- Libraries/LibWeb/CSS/ComputedValues.h | 2 +- Libraries/LibWeb/Layout/Node.cpp | 7 ++++++- Libraries/LibWeb/Layout/Node.h | 9 ++++++++- Libraries/LibWeb/Painting/PaintableBox.cpp | 5 ++++- Libraries/LibWeb/Painting/PaintableBox.h | 9 ++++++++- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index f82485ee8577..25c25d17c7cb 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -796,7 +796,7 @@ class MutableComputedValues final : public ComputedValues { void set_justify_items(CSS::JustifyItems value) { m_noninherited.justify_items = value; } void set_justify_self(CSS::JustifySelf value) { m_noninherited.justify_self = value; } void set_box_shadow(Vector&& value) { m_noninherited.box_shadow = move(value); } - void set_rotate(CSS::Transformation value) { m_noninherited.rotate = value; } + void set_rotate(CSS::Transformation value) { m_noninherited.rotate = move(value); } void set_transformations(Vector value) { m_noninherited.transformations = move(value); } void set_transform_box(CSS::TransformBox value) { m_noninherited.transform_box = value; } void set_transform_origin(CSS::TransformOrigin value) { m_noninherited.transform_origin = value; } diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 3d35c79dc1cc..2cef0046948f 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -82,6 +82,8 @@ bool Node::can_contain_boxes_with_position_absolute() const // Any computed value other than none for the transform affects containing block and stacking context if (!computed_values().transformations().is_empty()) return true; + if (computed_values().rotate().has_value()) + return true; return false; } @@ -175,6 +177,9 @@ bool Node::establishes_stacking_context() const if (!computed_values().transformations().is_empty()) return true; + if (computed_values().rotate().has_value()) + return true; + // Element that is a child of a flex container, with z-index value other than auto. if (parent() && parent()->display().is_flex_inside() && computed_values().z_index().has_value()) return true; @@ -710,7 +715,7 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) computed_values.set_box_shadow(computed_style.box_shadow(*this)); if (auto rotate_value = computed_style.rotate(*this); rotate_value.has_value()) - computed_values.set_rotate(rotate_value.value()); + computed_values.set_rotate(rotate_value.release_value()); computed_values.set_transformations(computed_style.transformations()); if (auto transform_box = computed_style.transform_box(); transform_box.has_value()) diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 45fc3b3ee4fd..6b42ae715e42 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -178,7 +178,14 @@ class Node // https://www.w3.org/TR/CSS22/visuren.html#positioning-scheme bool is_in_flow() const { return !is_out_of_flow(); } - bool has_css_transform() const { return computed_values().transformations().size() > 0; } + [[nodiscard]] bool has_css_transform() const + { + if (!computed_values().transformations().is_empty()) + return true; + if (computed_values().rotate().has_value()) + return true; + return false; + } protected: Node(DOM::Document&, DOM::Node*); diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index aaff8189b745..f13a76a981f8 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1128,8 +1128,11 @@ void PaintableBox::resolve_paint_properties() set_box_shadow_data(move(resolved_box_shadow_data)); auto const& transformations = computed_values.transformations(); - if (!transformations.is_empty()) { + auto const& rotate = computed_values.rotate(); + if (!transformations.is_empty() || rotate.has_value()) { auto matrix = Gfx::FloatMatrix4x4::identity(); + if (rotate.has_value()) + matrix = matrix * rotate->to_matrix(*this).release_value(); for (auto const& transform : transformations) matrix = matrix * transform.to_matrix(*this).release_value(); set_transform(matrix); diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 44c3707df205..5570b49bcf3d 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -129,7 +129,14 @@ class PaintableBox : public Paintable return m_overflow_data->has_scrollable_overflow; } - bool has_css_transform() const { return computed_values().transformations().size() > 0; } + [[nodiscard]] bool has_css_transform() const + { + if (!computed_values().transformations().is_empty()) + return true; + if (computed_values().rotate().has_value()) + return true; + return false; + } [[nodiscard]] Optional scrollable_overflow_rect() const { From 66a821e731d1bf4173d860d03806bce5ee9ef86b Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 22 Nov 2024 16:42:20 +0100 Subject: [PATCH 012/397] LibWeb: Support individual `translate` CSS property --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/CSS/CSSStyleValue.cpp | 9 +- Libraries/LibWeb/CSS/CSSStyleValue.h | 5 + Libraries/LibWeb/CSS/ComputedValues.h | 3 + Libraries/LibWeb/CSS/Parser/Parser.cpp | 34 +++++- Libraries/LibWeb/CSS/Parser/Parser.h | 1 + Libraries/LibWeb/CSS/Properties.json | 7 ++ Libraries/LibWeb/CSS/StyleProperties.cpp | 15 +++ Libraries/LibWeb/CSS/StyleProperties.h | 1 + .../CSS/StyleValues/TranslationStyleValue.cpp | 44 ++++++++ .../CSS/StyleValues/TranslationStyleValue.h | 49 +++++++++ Libraries/LibWeb/Forward.h | 1 + Libraries/LibWeb/Layout/Node.cpp | 8 ++ Libraries/LibWeb/Layout/Node.h | 2 + Libraries/LibWeb/Painting/PaintableBox.cpp | 5 +- Libraries/LibWeb/Painting/PaintableBox.h | 2 + ...upported-properties-and-default-values.txt | 3 +- ...eclaration-has-indexed-property-getter.txt | 15 +-- .../css/getComputedStyle-print-all.txt | 1 + .../animation/translate-interpolation.txt | 103 +++++++++--------- .../parsing/translate-parsing-computed.txt | 19 ++-- .../parsing/translate-parsing-valid.txt | 19 ++-- 22 files changed, 267 insertions(+), 80 deletions(-) create mode 100644 Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.cpp create mode 100644 Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.h diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index d6fd8e49567c..207f95eb9d17 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -154,6 +154,7 @@ set(SOURCES CSS/StyleValues/StyleValueList.cpp CSS/StyleValues/TransformationStyleValue.cpp CSS/StyleValues/TransitionStyleValue.cpp + CSS/StyleValues/TranslationStyleValue.cpp CSS/StyleValues/UnresolvedStyleValue.cpp CSS/Supports.cpp CSS/SyntaxHighlighter/SyntaxHighlighter.cpp diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.cpp b/Libraries/LibWeb/CSS/CSSStyleValue.cpp index 78b5b0a220dc..fde6552afd29 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleValue.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023, Andreas Kling + * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2021-2024, Sam Atkins * Copyright (c) 2021, Tobias Christiansen * Copyright (c) 2022-2023, MacDue @@ -56,6 +56,7 @@ #include #include #include +#include #include #include @@ -336,6 +337,12 @@ TransitionStyleValue const& CSSStyleValue::as_transition() const return static_cast(*this); } +TranslationStyleValue const& CSSStyleValue::as_translation() const +{ + VERIFY(is_translation()); + return static_cast(*this); +} + UnresolvedStyleValue const& CSSStyleValue::as_unresolved() const { VERIFY(is_unresolved()); diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.h b/Libraries/LibWeb/CSS/CSSStyleValue.h index 312b5d6a8bca..b4a29721d637 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.h +++ b/Libraries/LibWeb/CSS/CSSStyleValue.h @@ -132,6 +132,7 @@ class CSSStyleValue : public RefCounted { Time, Transformation, Transition, + Translation, Unresolved, URL, ValueList, @@ -322,6 +323,10 @@ class CSSStyleValue : public RefCounted { TransitionStyleValue const& as_transition() const; TransitionStyleValue& as_transition() { return const_cast(const_cast(*this).as_transition()); } + bool is_translation() const { return type() == Type::Translation; } + TranslationStyleValue const& as_translation() const; + TranslationStyleValue& as_translation() { return const_cast(const_cast(*this).as_translation()); } + bool is_unresolved() const { return type() == Type::Unresolved; } UnresolvedStyleValue const& as_unresolved() const; UnresolvedStyleValue& as_unresolved() { return const_cast(const_cast(*this).as_unresolved()); } diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index 25c25d17c7cb..d712f30e7dea 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -508,6 +508,7 @@ class ComputedValues { CSS::TransformBox const& transform_box() const { return m_noninherited.transform_box; } CSS::TransformOrigin const& transform_origin() const { return m_noninherited.transform_origin; } Optional const& rotate() const { return m_noninherited.rotate; } + Optional const& translate() const { return m_noninherited.translate; } Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; } CSSPixels font_size() const { return m_inherited.font_size; } @@ -684,6 +685,7 @@ class ComputedValues { CSS::ObjectPosition object_position { InitialValues::object_position() }; CSS::UnicodeBidi unicode_bidi { InitialValues::unicode_bidi() }; Optional rotate; + Optional translate; Optional mask; CSS::MaskType mask_type { InitialValues::mask_type() }; @@ -800,6 +802,7 @@ class MutableComputedValues final : public ComputedValues { void set_transformations(Vector value) { m_noninherited.transformations = move(value); } void set_transform_box(CSS::TransformBox value) { m_noninherited.transform_box = value; } void set_transform_origin(CSS::TransformOrigin value) { m_noninherited.transform_origin = value; } + void set_translate(CSS::Transformation value) { m_noninherited.translate = move(value); } void set_box_sizing(CSS::BoxSizing value) { m_noninherited.box_sizing = value; } void set_vertical_align(Variant value) { m_noninherited.vertical_align = move(value); } void set_visibility(CSS::Visibility value) { m_inherited.visibility = value; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 5edc59d3e928..dc9f7839912e 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2022, Andreas Kling + * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2020-2021, the SerenityOS developers. * Copyright (c) 2021-2024, Sam Atkins * Copyright (c) 2021, Tobias Christiansen @@ -76,6 +76,7 @@ #include #include #include +#include #include #include #include @@ -6928,6 +6929,33 @@ Optional Parser::parse_grid_size(ComponentValue const& component_ return {}; } +RefPtr Parser::parse_translate_value(TokenStream& tokens) +{ + if (tokens.remaining_token_count() == 1) { + // "none" + if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None)) + return none; + } + + auto transaction = tokens.begin_transaction(); + + auto maybe_x = parse_length_percentage(tokens); + if (!maybe_x.has_value()) + return nullptr; + + if (!tokens.has_next_token()) { + transaction.commit(); + return TranslationStyleValue::create(maybe_x.release_value(), LengthPercentage(Length::make_px(0))); + } + + auto maybe_y = parse_length_percentage(tokens); + if (!maybe_y.has_value()) + return nullptr; + + transaction.commit(); + return TranslationStyleValue::create(maybe_x.release_value(), maybe_y.release_value()); +} + Optional Parser::parse_fit_content(Vector const& component_values) { // https://www.w3.org/TR/css-grid-2/#valdef-grid-template-columns-fit-content @@ -7945,6 +7973,10 @@ Parser::ParseErrorOr> Parser::parse_css_value(Prope if (auto parsed_value = parse_transition_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); return ParseError::SyntaxError; + case PropertyID::Translate: + if (auto parsed_value = parse_translate_value(tokens); parsed_value && !tokens.has_next_token()) + return parsed_value.release_nonnull(); + return ParseError::SyntaxError; default: break; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 94b4d117cbaf..c015040534e8 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -344,6 +344,7 @@ class Parser { RefPtr parse_transform_value(TokenStream&); RefPtr parse_transform_origin_value(TokenStream&); RefPtr parse_transition_value(TokenStream&); + RefPtr parse_translate_value(TokenStream&); RefPtr parse_grid_track_size_list(TokenStream&, bool allow_separate_line_name_blocks = false); RefPtr parse_grid_auto_track_sizes(TokenStream&); RefPtr parse_grid_auto_flow_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index 756011a7c6b9..b3252cae1b84 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -2725,6 +2725,13 @@ "easing-function" ] }, + "translate": { + "animation-type": "custom", + "inherited": false, + "initial": "none", + "affects-layout": false, + "affects-stacking-context": true + }, "unicode-bidi": { "animation-type": "none", "inherited": false, diff --git a/Libraries/LibWeb/CSS/StyleProperties.cpp b/Libraries/LibWeb/CSS/StyleProperties.cpp index 3d1efb25652b..cc8ba99ce042 100644 --- a/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -599,6 +600,20 @@ Optional StyleProperties::rotate(Layout::Node const& layout return CSS::Transformation(CSS::TransformFunction::Rotate3d, move(values)); } +Optional StyleProperties::translate() const +{ + auto const& value = property(CSS::PropertyID::Translate); + if (!value.is_translation()) + return {}; + auto const& translation = value.as_translation(); + + Vector values; + values.append(translation.x()); + values.append(translation.y()); + + return CSS::Transformation(CSS::TransformFunction::Translate, move(values)); +} + static Optional length_percentage_for_style_value(CSSStyleValue const& value) { if (value.is_length()) diff --git a/Libraries/LibWeb/CSS/StyleProperties.h b/Libraries/LibWeb/CSS/StyleProperties.h index 7551757df5f3..6d4b7033eddb 100644 --- a/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Libraries/LibWeb/CSS/StyleProperties.h @@ -176,6 +176,7 @@ class StyleProperties { Optional transform_box() const; CSS::TransformOrigin transform_origin() const; Optional rotate(Layout::Node const&) const; + Optional translate() const; Optional mask_type() const; Color stop_color() const; diff --git a/Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.cpp new file mode 100644 index 000000000000..06d8e098c4dc --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.cpp @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::CSS { + +// https://www.w3.org/TR/2021/WD-css-transforms-2-20211109/#individual-transform-serialization +String TranslationStyleValue::to_string() const +{ + auto resolve_to_string = [](LengthPercentage const& value) -> Optional { + if (value.is_length()) { + if (value.length().raw_value() == 0) + return {}; + } + if (value.is_percentage()) { + if (value.percentage().value() == 0) + return {}; + } + return value.to_string(); + }; + + auto x_value = resolve_to_string(m_properties.x); + auto y_value = resolve_to_string(m_properties.y); + + StringBuilder builder; + builder.append(x_value.value_or("0px"_string)); + if (y_value.has_value()) { + builder.append(" "sv); + builder.append(y_value.value()); + } + + return builder.to_string_without_validation(); +} + +} diff --git a/Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.h new file mode 100644 index 000000000000..6265befd3642 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/TranslationStyleValue.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +class TranslationStyleValue : public StyleValueWithDefaultOperators { +public: + static ValueComparingNonnullRefPtr create(LengthPercentage x, LengthPercentage y) + { + return adopt_ref(*new (nothrow) TranslationStyleValue(move(x), move(y))); + } + + virtual ~TranslationStyleValue() override = default; + + LengthPercentage const& x() const { return m_properties.x; } + LengthPercentage const& y() const { return m_properties.y; } + + virtual String to_string() const override; + + bool properties_equal(TranslationStyleValue const& other) const { return m_properties == other.m_properties; } + +private: + explicit TranslationStyleValue( + LengthPercentage x, + LengthPercentage y) + : StyleValueWithDefaultOperators(Type::Translation) + , m_properties { + .x = move(x), + .y = move(y), + } + { + } + + struct Properties { + LengthPercentage x; + LengthPercentage y; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index cfbedd731a18..48f4fea8ead8 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -227,6 +227,7 @@ class TimeStyleValue; class Transformation; class TransformationStyleValue; class TransitionStyleValue; +class TranslationStyleValue; class UnresolvedStyleValue; class URLStyleValue; class VisualViewport; diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 2cef0046948f..943f421b02bc 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -82,6 +82,8 @@ bool Node::can_contain_boxes_with_position_absolute() const // Any computed value other than none for the transform affects containing block and stacking context if (!computed_values().transformations().is_empty()) return true; + if (computed_values().translate().has_value()) + return true; if (computed_values().rotate().has_value()) return true; @@ -177,6 +179,9 @@ bool Node::establishes_stacking_context() const if (!computed_values().transformations().is_empty()) return true; + if (computed_values().translate().has_value()) + return true; + if (computed_values().rotate().has_value()) return true; @@ -717,6 +722,9 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) if (auto rotate_value = computed_style.rotate(*this); rotate_value.has_value()) computed_values.set_rotate(rotate_value.release_value()); + if (auto translate_value = computed_style.translate(); translate_value.has_value()) + computed_values.set_translate(translate_value.release_value()); + computed_values.set_transformations(computed_style.transformations()); if (auto transform_box = computed_style.transform_box(); transform_box.has_value()) computed_values.set_transform_box(transform_box.value()); diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index 6b42ae715e42..b4f98110b031 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -184,6 +184,8 @@ class Node return true; if (computed_values().rotate().has_value()) return true; + if (computed_values().translate().has_value()) + return true; return false; } diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index f13a76a981f8..c8791587198f 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1128,9 +1128,12 @@ void PaintableBox::resolve_paint_properties() set_box_shadow_data(move(resolved_box_shadow_data)); auto const& transformations = computed_values.transformations(); + auto const& translate = computed_values.translate(); auto const& rotate = computed_values.rotate(); - if (!transformations.is_empty() || rotate.has_value()) { + if (!transformations.is_empty() || translate.has_value() || rotate.has_value()) { auto matrix = Gfx::FloatMatrix4x4::identity(); + if (translate.has_value()) + matrix = matrix * translate->to_matrix(*this).release_value(); if (rotate.has_value()) matrix = matrix * rotate->to_matrix(*this).release_value(); for (auto const& transform : transformations) diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 5570b49bcf3d..25bb4d97b781 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -135,6 +135,8 @@ class PaintableBox : public Paintable return true; if (computed_values().rotate().has_value()) return true; + if (computed_values().translate().has_value()) + return true; return false; } diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt index a64eb0e32e63..b0b29e54c838 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt @@ -1,6 +1,6 @@ All supported properties and their default values exposed from CSSStyleDeclaration from getComputedStyle: 'cssText': '' -'length': '204' +'length': '205' 'parentRule': 'null' 'cssFloat': 'none' 'WebkitAlignContent': 'normal' @@ -552,6 +552,7 @@ All supported properties and their default values exposed from CSSStyleDeclarati 'transition-property': 'all' 'transitionTimingFunction': 'ease' 'transition-timing-function': 'ease' +'translate': 'none' 'unicodeBidi': 'normal' 'unicode-bidi': 'normal' 'userSelect': 'auto' diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt index 2a739619d4b9..5f6b0f5553dd 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt @@ -197,13 +197,14 @@ All properties associated with getComputedStyle(document.body): "194": "transition-duration", "195": "transition-property", "196": "transition-timing-function", - "197": "unicode-bidi", - "198": "user-select", - "199": "vertical-align", - "200": "width", - "201": "x", - "202": "y", - "203": "z-index" + "197": "translate", + "198": "unicode-bidi", + "199": "user-select", + "200": "vertical-align", + "201": "width", + "202": "x", + "203": "y", + "204": "z-index" } All properties associated with document.body.style by default: {} diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index 6fe6d664a501..1d91f41e986f 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -195,6 +195,7 @@ transition-delay: 0s transition-duration: 0s transition-property: all transition-timing-function: ease +translate: none unicode-bidi: normal user-select: auto vertical-align: baseline diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt index eaf81fbe2d12..b2bbe981ddda 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/animation/translate-interpolation.txt @@ -6,79 +6,80 @@ Rerun Found 408 tests -408 Fail +50 Pass +358 Fail Details Result Test Name MessageFail CSS Transitions: property from [-100px] to [100px] at (-1) should be [-300px] Fail CSS Transitions: property from [-100px] to [100px] at (0) should be [-100px] Fail CSS Transitions: property from [-100px] to [100px] at (0.25) should be [-50px] Fail CSS Transitions: property from [-100px] to [100px] at (0.75) should be [50px] -Fail CSS Transitions: property from [-100px] to [100px] at (1) should be [100px] +Pass CSS Transitions: property from [-100px] to [100px] at (1) should be [100px] Fail CSS Transitions: property from [-100px] to [100px] at (2) should be [300px] Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (-1) should be [-300px] Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (0) should be [-100px] Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (0.25) should be [-50px] Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (0.75) should be [50px] -Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (1) should be [100px] +Pass CSS Transitions with transition: all: property from [-100px] to [100px] at (1) should be [100px] Fail CSS Transitions with transition: all: property from [-100px] to [100px] at (2) should be [300px] Fail CSS Animations: property from [-100px] to [100px] at (-1) should be [-300px] -Fail CSS Animations: property from [-100px] to [100px] at (0) should be [-100px] +Pass CSS Animations: property from [-100px] to [100px] at (0) should be [-100px] Fail CSS Animations: property from [-100px] to [100px] at (0.25) should be [-50px] Fail CSS Animations: property from [-100px] to [100px] at (0.75) should be [50px] -Fail CSS Animations: property from [-100px] to [100px] at (1) should be [100px] +Pass CSS Animations: property from [-100px] to [100px] at (1) should be [100px] Fail CSS Animations: property from [-100px] to [100px] at (2) should be [300px] Fail Web Animations: property from [-100px] to [100px] at (-1) should be [-300px] -Fail Web Animations: property from [-100px] to [100px] at (0) should be [-100px] +Pass Web Animations: property from [-100px] to [100px] at (0) should be [-100px] Fail Web Animations: property from [-100px] to [100px] at (0.25) should be [-50px] Fail Web Animations: property from [-100px] to [100px] at (0.75) should be [50px] -Fail Web Animations: property from [-100px] to [100px] at (1) should be [100px] +Pass Web Animations: property from [-100px] to [100px] at (1) should be [100px] Fail Web Animations: property from [-100px] to [100px] at (2) should be [300px] Fail CSS Transitions: property from [-100%] to [100%] at (-1) should be [-300%] Fail CSS Transitions: property from [-100%] to [100%] at (0) should be [-100%] Fail CSS Transitions: property from [-100%] to [100%] at (0.25) should be [-50%] Fail CSS Transitions: property from [-100%] to [100%] at (0.75) should be [50%] -Fail CSS Transitions: property from [-100%] to [100%] at (1) should be [100%] +Pass CSS Transitions: property from [-100%] to [100%] at (1) should be [100%] Fail CSS Transitions: property from [-100%] to [100%] at (2) should be [300%] Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (-1) should be [-300%] Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (0) should be [-100%] Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (0.25) should be [-50%] Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (0.75) should be [50%] -Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (1) should be [100%] +Pass CSS Transitions with transition: all: property from [-100%] to [100%] at (1) should be [100%] Fail CSS Transitions with transition: all: property from [-100%] to [100%] at (2) should be [300%] Fail CSS Animations: property from [-100%] to [100%] at (-1) should be [-300%] -Fail CSS Animations: property from [-100%] to [100%] at (0) should be [-100%] +Pass CSS Animations: property from [-100%] to [100%] at (0) should be [-100%] Fail CSS Animations: property from [-100%] to [100%] at (0.25) should be [-50%] Fail CSS Animations: property from [-100%] to [100%] at (0.75) should be [50%] -Fail CSS Animations: property from [-100%] to [100%] at (1) should be [100%] +Pass CSS Animations: property from [-100%] to [100%] at (1) should be [100%] Fail CSS Animations: property from [-100%] to [100%] at (2) should be [300%] Fail Web Animations: property from [-100%] to [100%] at (-1) should be [-300%] -Fail Web Animations: property from [-100%] to [100%] at (0) should be [-100%] +Pass Web Animations: property from [-100%] to [100%] at (0) should be [-100%] Fail Web Animations: property from [-100%] to [100%] at (0.25) should be [-50%] Fail Web Animations: property from [-100%] to [100%] at (0.75) should be [50%] -Fail Web Animations: property from [-100%] to [100%] at (1) should be [100%] +Pass Web Animations: property from [-100%] to [100%] at (1) should be [100%] Fail Web Animations: property from [-100%] to [100%] at (2) should be [300%] Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] -Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Pass CSS Transitions: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] Fail CSS Transitions: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] -Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Pass CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] Fail CSS Transitions with transition: all: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] -Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] +Pass CSS Animations: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] -Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Pass CSS Animations: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] Fail CSS Animations: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] Fail Web Animations: property from [-100px -50px] to [100px 50px] at (-1) should be [-300px -150px] -Fail Web Animations: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] +Pass Web Animations: property from [-100px -50px] to [100px 50px] at (0) should be [-100px -50px] Fail Web Animations: property from [-100px -50px] to [100px 50px] at (0.25) should be [-50px -25px] Fail Web Animations: property from [-100px -50px] to [100px 50px] at (0.75) should be [50px 25px] -Fail Web Animations: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] +Pass Web Animations: property from [-100px -50px] to [100px 50px] at (1) should be [100px 50px] Fail Web Animations: property from [-100px -50px] to [100px 50px] at (2) should be [300px 150px] Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (-1) should be [140px 80px 20px] Fail CSS Transitions: property from [220px 240px 260px] to [300px 400px 500px] at (0) should be [220px 240px 260px] @@ -176,30 +177,30 @@ Fail Web Animations: property from [480px 400px 320px] to [240% 160% Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (0.875) should be [calc(210% + 60px) calc(140% + 50px) 40px] Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (1) should be [240% 160%] Fail Web Animations: property from [480px 400px 320px] to [240% 160%] at (2) should be [calc(480% - 480px) calc(320% - 400px) -320px] -Fail CSS Transitions: property from [none] to [none] at (-1) should be [none] -Fail CSS Transitions: property from [none] to [none] at (0) should be [none] -Fail CSS Transitions: property from [none] to [none] at (0.125) should be [none] -Fail CSS Transitions: property from [none] to [none] at (0.875) should be [none] -Fail CSS Transitions: property from [none] to [none] at (1) should be [none] -Fail CSS Transitions: property from [none] to [none] at (2) should be [none] -Fail CSS Transitions with transition: all: property from [none] to [none] at (-1) should be [none] -Fail CSS Transitions with transition: all: property from [none] to [none] at (0) should be [none] -Fail CSS Transitions with transition: all: property from [none] to [none] at (0.125) should be [none] -Fail CSS Transitions with transition: all: property from [none] to [none] at (0.875) should be [none] -Fail CSS Transitions with transition: all: property from [none] to [none] at (1) should be [none] -Fail CSS Transitions with transition: all: property from [none] to [none] at (2) should be [none] -Fail CSS Animations: property from [none] to [none] at (-1) should be [none] -Fail CSS Animations: property from [none] to [none] at (0) should be [none] -Fail CSS Animations: property from [none] to [none] at (0.125) should be [none] -Fail CSS Animations: property from [none] to [none] at (0.875) should be [none] -Fail CSS Animations: property from [none] to [none] at (1) should be [none] -Fail CSS Animations: property from [none] to [none] at (2) should be [none] -Fail Web Animations: property from [none] to [none] at (-1) should be [none] -Fail Web Animations: property from [none] to [none] at (0) should be [none] -Fail Web Animations: property from [none] to [none] at (0.125) should be [none] -Fail Web Animations: property from [none] to [none] at (0.875) should be [none] -Fail Web Animations: property from [none] to [none] at (1) should be [none] -Fail Web Animations: property from [none] to [none] at (2) should be [none] +Pass CSS Transitions: property from [none] to [none] at (-1) should be [none] +Pass CSS Transitions: property from [none] to [none] at (0) should be [none] +Pass CSS Transitions: property from [none] to [none] at (0.125) should be [none] +Pass CSS Transitions: property from [none] to [none] at (0.875) should be [none] +Pass CSS Transitions: property from [none] to [none] at (1) should be [none] +Pass CSS Transitions: property from [none] to [none] at (2) should be [none] +Pass CSS Transitions with transition: all: property from [none] to [none] at (-1) should be [none] +Pass CSS Transitions with transition: all: property from [none] to [none] at (0) should be [none] +Pass CSS Transitions with transition: all: property from [none] to [none] at (0.125) should be [none] +Pass CSS Transitions with transition: all: property from [none] to [none] at (0.875) should be [none] +Pass CSS Transitions with transition: all: property from [none] to [none] at (1) should be [none] +Pass CSS Transitions with transition: all: property from [none] to [none] at (2) should be [none] +Pass CSS Animations: property from [none] to [none] at (-1) should be [none] +Pass CSS Animations: property from [none] to [none] at (0) should be [none] +Pass CSS Animations: property from [none] to [none] at (0.125) should be [none] +Pass CSS Animations: property from [none] to [none] at (0.875) should be [none] +Pass CSS Animations: property from [none] to [none] at (1) should be [none] +Pass CSS Animations: property from [none] to [none] at (2) should be [none] +Pass Web Animations: property from [none] to [none] at (-1) should be [none] +Pass Web Animations: property from [none] to [none] at (0) should be [none] +Pass Web Animations: property from [none] to [none] at (0.125) should be [none] +Pass Web Animations: property from [none] to [none] at (0.875) should be [none] +Pass Web Animations: property from [none] to [none] at (1) should be [none] +Pass Web Animations: property from [none] to [none] at (2) should be [none] Fail CSS Transitions: property from [none] to [8px 80% 800px] at (-1) should be [-8px -80% -800px] Fail CSS Transitions: property from [none] to [8px 80% 800px] at (0) should be [0px 0%] Fail CSS Transitions: property from [none] to [8px 80% 800px] at (0.125) should be [1px 10% 100px] @@ -228,25 +229,25 @@ Fail CSS Transitions: property from neutral to [20px] at (-1) should Fail CSS Transitions: property from neutral to [20px] at (0) should be [10px] Fail CSS Transitions: property from neutral to [20px] at (0.25) should be [12.5px] Fail CSS Transitions: property from neutral to [20px] at (0.75) should be [17.5px] -Fail CSS Transitions: property from neutral to [20px] at (1) should be [20px] +Pass CSS Transitions: property from neutral to [20px] at (1) should be [20px] Fail CSS Transitions: property from neutral to [20px] at (2) should be [30px] Fail CSS Transitions with transition: all: property from neutral to [20px] at (-1) should be [0px] Fail CSS Transitions with transition: all: property from neutral to [20px] at (0) should be [10px] Fail CSS Transitions with transition: all: property from neutral to [20px] at (0.25) should be [12.5px] Fail CSS Transitions with transition: all: property from neutral to [20px] at (0.75) should be [17.5px] -Fail CSS Transitions with transition: all: property from neutral to [20px] at (1) should be [20px] +Pass CSS Transitions with transition: all: property from neutral to [20px] at (1) should be [20px] Fail CSS Transitions with transition: all: property from neutral to [20px] at (2) should be [30px] Fail CSS Animations: property from neutral to [20px] at (-1) should be [0px] Fail CSS Animations: property from neutral to [20px] at (0) should be [10px] Fail CSS Animations: property from neutral to [20px] at (0.25) should be [12.5px] Fail CSS Animations: property from neutral to [20px] at (0.75) should be [17.5px] -Fail CSS Animations: property from neutral to [20px] at (1) should be [20px] +Pass CSS Animations: property from neutral to [20px] at (1) should be [20px] Fail CSS Animations: property from neutral to [20px] at (2) should be [30px] Fail Web Animations: property from neutral to [20px] at (-1) should be [0px] Fail Web Animations: property from neutral to [20px] at (0) should be [10px] Fail Web Animations: property from neutral to [20px] at (0.25) should be [12.5px] Fail Web Animations: property from neutral to [20px] at (0.75) should be [17.5px] -Fail Web Animations: property from neutral to [20px] at (1) should be [20px] +Pass Web Animations: property from neutral to [20px] at (1) should be [20px] Fail Web Animations: property from neutral to [20px] at (2) should be [30px] Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (-1) should be [-200px -100px -200px] Fail CSS Transitions: property from [initial] to [200px 100px 200px] at (0) should be [0px] @@ -300,25 +301,25 @@ Fail CSS Transitions: property from [unset] to [20px] at (-1) should Fail CSS Transitions: property from [unset] to [20px] at (0) should be [0px] Fail CSS Transitions: property from [unset] to [20px] at (0.25) should be [5px] Fail CSS Transitions: property from [unset] to [20px] at (0.75) should be [15px] -Fail CSS Transitions: property from [unset] to [20px] at (1) should be [20px] +Pass CSS Transitions: property from [unset] to [20px] at (1) should be [20px] Fail CSS Transitions: property from [unset] to [20px] at (2) should be [40px] Fail CSS Transitions with transition: all: property from [unset] to [20px] at (-1) should be [-20px] Fail CSS Transitions with transition: all: property from [unset] to [20px] at (0) should be [0px] Fail CSS Transitions with transition: all: property from [unset] to [20px] at (0.25) should be [5px] Fail CSS Transitions with transition: all: property from [unset] to [20px] at (0.75) should be [15px] -Fail CSS Transitions with transition: all: property from [unset] to [20px] at (1) should be [20px] +Pass CSS Transitions with transition: all: property from [unset] to [20px] at (1) should be [20px] Fail CSS Transitions with transition: all: property from [unset] to [20px] at (2) should be [40px] Fail CSS Animations: property from [unset] to [20px] at (-1) should be [-20px] Fail CSS Animations: property from [unset] to [20px] at (0) should be [0px] Fail CSS Animations: property from [unset] to [20px] at (0.25) should be [5px] Fail CSS Animations: property from [unset] to [20px] at (0.75) should be [15px] -Fail CSS Animations: property from [unset] to [20px] at (1) should be [20px] +Pass CSS Animations: property from [unset] to [20px] at (1) should be [20px] Fail CSS Animations: property from [unset] to [20px] at (2) should be [40px] Fail Web Animations: property from [unset] to [20px] at (-1) should be [-20px] Fail Web Animations: property from [unset] to [20px] at (0) should be [0px] Fail Web Animations: property from [unset] to [20px] at (0.25) should be [5px] Fail Web Animations: property from [unset] to [20px] at (0.75) should be [15px] -Fail Web Animations: property from [unset] to [20px] at (1) should be [20px] +Pass Web Animations: property from [unset] to [20px] at (1) should be [20px] Fail Web Animations: property from [unset] to [20px] at (2) should be [40px] Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (-1) should be [0px 300px 400px] Fail CSS Transitions: property from [inherit] to [200px 100px 200px] at (0) should be [100px 200px 300px] diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt index 9d39c55f0f47..f29e3738f8da 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-computed.txt @@ -6,17 +6,18 @@ Rerun Found 19 tests -19 Fail +8 Pass +11 Fail Details -Result Test Name MessageFail Property translate value 'none' -Fail Property translate value '0px' -Fail Property translate value '100%' -Fail Property translate value '100px 0px' -Fail Property translate value '100px 0.1px' +Result Test Name MessagePass Property translate value 'none' +Pass Property translate value '0px' +Pass Property translate value '100%' +Pass Property translate value '100px 0px' +Pass Property translate value '100px 0.1px' Fail Property translate value '100px 0%' Fail Property translate value '100px calc(10px - 10%)' -Fail Property translate value '100px 200%' -Fail Property translate value '100% 200px' +Pass Property translate value '100px 200%' +Pass Property translate value '100% 200px' Fail Property translate value '100px 200px 0px' Fail Property translate value '100px 0px 300px' Fail Property translate value '100px 0px 0px' @@ -25,5 +26,5 @@ Fail Property translate value '100% 200% 300px' Fail Property translate value '100% 0% 200px' Fail Property translate value '0% 0% 100px' Fail Property translate value '0em 0em 100px' -Fail Property translate value '0' +Pass Property translate value '0' Fail Property translate value '1px 2px 0' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt index e1e0562b6a46..c568e5f256d2 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/translate-parsing-valid.txt @@ -6,17 +6,18 @@ Rerun Found 20 tests -20 Fail +8 Pass +12 Fail Details -Result Test Name MessageFail e.style['translate'] = "none" should set the property value -Fail e.style['translate'] = "0px" should set the property value -Fail e.style['translate'] = "100%" should set the property value -Fail e.style['translate'] = "100px 0px" should set the property value -Fail e.style['translate'] = "100px 0.1px" should set the property value +Result Test Name MessagePass e.style['translate'] = "none" should set the property value +Pass e.style['translate'] = "0px" should set the property value +Pass e.style['translate'] = "100%" should set the property value +Pass e.style['translate'] = "100px 0px" should set the property value +Pass e.style['translate'] = "100px 0.1px" should set the property value Fail e.style['translate'] = "100px 0%" should set the property value Fail e.style['translate'] = "100px calc(10px - 10%)" should set the property value -Fail e.style['translate'] = "100px 200%" should set the property value -Fail e.style['translate'] = "100% 200px" should set the property value +Pass e.style['translate'] = "100px 200%" should set the property value +Pass e.style['translate'] = "100% 200px" should set the property value Fail e.style['translate'] = "100px 200px 0px" should set the property value Fail e.style['translate'] = "100px 0px 300px" should set the property value Fail e.style['translate'] = "100px 0px 0px" should set the property value @@ -26,5 +27,5 @@ Fail e.style['translate'] = "100% 0% 200px" should set the property value Fail e.style['translate'] = "0% 0% 100px" should set the property value Fail e.style['translate'] = "0em 0em 100px" should set the property value Fail e.style['translate'] = "calc(10% + 10px) calc(20% + 20px) calc(30em + 30px)" should set the property value -Fail e.style['translate'] = "0" should set the property value +Pass e.style['translate'] = "0" should set the property value Fail e.style['translate'] = "1px 2px 0" should set the property value \ No newline at end of file From 9a7c9286c447d016d7ea72afba6b4dc63395c5ce Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Fri, 22 Nov 2024 18:07:16 +0100 Subject: [PATCH 013/397] LibWeb: Support individual `scale` CSS property --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/CSS/CSSStyleValue.cpp | 7 +++ Libraries/LibWeb/CSS/CSSStyleValue.h | 5 ++ Libraries/LibWeb/CSS/ComputedValues.h | 3 ++ Libraries/LibWeb/CSS/Parser/Parser.cpp | 32 ++++++++++++ Libraries/LibWeb/CSS/Parser/Parser.h | 1 + Libraries/LibWeb/CSS/Properties.json | 7 +++ Libraries/LibWeb/CSS/StyleProperties.cpp | 15 ++++++ Libraries/LibWeb/CSS/StyleProperties.h | 1 + .../CSS/StyleValues/ScaleStyleValue.cpp | 37 ++++++++++++++ .../LibWeb/CSS/StyleValues/ScaleStyleValue.h | 49 ++++++++++++++++++ Libraries/LibWeb/Forward.h | 1 + Libraries/LibWeb/Layout/Node.cpp | 8 +++ Libraries/LibWeb/Layout/Node.h | 2 + Libraries/LibWeb/Painting/PaintableBox.cpp | 5 +- Libraries/LibWeb/Painting/PaintableBox.h | 2 + ...upported-properties-and-default-values.txt | 3 +- ...eclaration-has-indexed-property-getter.txt | 51 ++++++++++--------- .../css/getComputedStyle-print-all.txt | 1 + .../parsing/scale-parsing-computed.txt | 29 ++++++----- .../parsing/scale-parsing-valid.txt | 29 ++++++----- 21 files changed, 234 insertions(+), 55 deletions(-) create mode 100644 Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp create mode 100644 Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 207f95eb9d17..2c1d0755969e 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -149,6 +149,7 @@ set(SOURCES CSS/StyleValues/RadialGradientStyleValue.cpp CSS/StyleValues/RectStyleValue.cpp CSS/StyleValues/RotationStyleValue.cpp + CSS/StyleValues/ScaleStyleValue.cpp CSS/StyleValues/ShadowStyleValue.cpp CSS/StyleValues/ShorthandStyleValue.cpp CSS/StyleValues/StyleValueList.cpp diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.cpp b/Libraries/LibWeb/CSS/CSSStyleValue.cpp index fde6552afd29..f3eeb55adb3a 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.cpp +++ b/Libraries/LibWeb/CSS/CSSStyleValue.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -295,6 +296,12 @@ RotationStyleValue const& CSSStyleValue::as_rotation() const return static_cast(*this); } +ScaleStyleValue const& CSSStyleValue::as_scale() const +{ + VERIFY(is_scale()); + return static_cast(*this); +} + ScrollbarGutterStyleValue const& CSSStyleValue::as_scrollbar_gutter() const { VERIFY(is_scrollbar_gutter()); diff --git a/Libraries/LibWeb/CSS/CSSStyleValue.h b/Libraries/LibWeb/CSS/CSSStyleValue.h index b4a29721d637..e8cf81690f0d 100644 --- a/Libraries/LibWeb/CSS/CSSStyleValue.h +++ b/Libraries/LibWeb/CSS/CSSStyleValue.h @@ -125,6 +125,7 @@ class CSSStyleValue : public RefCounted { Rect, Resolution, Rotation, + Scale, ScrollbarGutter, Shadow, Shorthand, @@ -295,6 +296,10 @@ class CSSStyleValue : public RefCounted { RotationStyleValue const& as_rotation() const; RotationStyleValue& as_rotation() { return const_cast(const_cast(*this).as_rotation()); } + bool is_scale() const { return type() == Type::Scale; } + ScaleStyleValue const& as_scale() const; + ScaleStyleValue& as_scale() { return const_cast(const_cast(*this).as_scale()); } + bool is_scrollbar_gutter() const { return type() == Type::ScrollbarGutter; } ScrollbarGutterStyleValue const& as_scrollbar_gutter() const; ScrollbarGutterStyleValue& as_scrollbar_gutter() { return const_cast(const_cast(*this).as_scrollbar_gutter()); } diff --git a/Libraries/LibWeb/CSS/ComputedValues.h b/Libraries/LibWeb/CSS/ComputedValues.h index d712f30e7dea..360df02495cb 100644 --- a/Libraries/LibWeb/CSS/ComputedValues.h +++ b/Libraries/LibWeb/CSS/ComputedValues.h @@ -509,6 +509,7 @@ class ComputedValues { CSS::TransformOrigin const& transform_origin() const { return m_noninherited.transform_origin; } Optional const& rotate() const { return m_noninherited.rotate; } Optional const& translate() const { return m_noninherited.translate; } + Optional const& scale() const { return m_noninherited.scale; } Gfx::FontCascadeList const& font_list() const { return *m_inherited.font_list; } CSSPixels font_size() const { return m_inherited.font_size; } @@ -686,6 +687,7 @@ class ComputedValues { CSS::UnicodeBidi unicode_bidi { InitialValues::unicode_bidi() }; Optional rotate; Optional translate; + Optional scale; Optional mask; CSS::MaskType mask_type { InitialValues::mask_type() }; @@ -799,6 +801,7 @@ class MutableComputedValues final : public ComputedValues { void set_justify_self(CSS::JustifySelf value) { m_noninherited.justify_self = value; } void set_box_shadow(Vector&& value) { m_noninherited.box_shadow = move(value); } void set_rotate(CSS::Transformation value) { m_noninherited.rotate = move(value); } + void set_scale(CSS::Transformation value) { m_noninherited.scale = move(value); } void set_transformations(Vector value) { m_noninherited.transformations = move(value); } void set_transform_box(CSS::TransformBox value) { m_noninherited.transform_box = value; } void set_transform_origin(CSS::TransformOrigin value) { m_noninherited.transform_origin = value; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index dc9f7839912e..0d85225db8f1 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -68,6 +68,7 @@ #include #include #include +#include #include #include #include @@ -6956,6 +6957,33 @@ RefPtr Parser::parse_translate_value(TokenStream& return TranslationStyleValue::create(maybe_x.release_value(), maybe_y.release_value()); } +RefPtr Parser::parse_scale_value(TokenStream& tokens) +{ + if (tokens.remaining_token_count() == 1) { + // "none" + if (auto none = parse_all_as_single_keyword_value(tokens, Keyword::None)) + return none; + } + + auto transaction = tokens.begin_transaction(); + + auto maybe_x = parse_number_percentage(tokens); + if (!maybe_x.has_value()) + return nullptr; + + if (!tokens.has_next_token()) { + transaction.commit(); + return ScaleStyleValue::create(maybe_x.value(), maybe_x.value()); + } + + auto maybe_y = parse_number_percentage(tokens); + if (!maybe_y.has_value()) + return nullptr; + + transaction.commit(); + return ScaleStyleValue::create(maybe_x.release_value(), maybe_y.release_value()); +} + Optional Parser::parse_fit_content(Vector const& component_values) { // https://www.w3.org/TR/css-grid-2/#valdef-grid-template-columns-fit-content @@ -7977,6 +8005,10 @@ Parser::ParseErrorOr> Parser::parse_css_value(Prope if (auto parsed_value = parse_translate_value(tokens); parsed_value && !tokens.has_next_token()) return parsed_value.release_nonnull(); return ParseError::SyntaxError; + case PropertyID::Scale: + if (auto parsed_value = parse_scale_value(tokens); parsed_value && !tokens.has_next_token()) + return parsed_value.release_nonnull(); + return ParseError::SyntaxError; default: break; } diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index c015040534e8..4698eeb06878 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -345,6 +345,7 @@ class Parser { RefPtr parse_transform_origin_value(TokenStream&); RefPtr parse_transition_value(TokenStream&); RefPtr parse_translate_value(TokenStream&); + RefPtr parse_scale_value(TokenStream&); RefPtr parse_grid_track_size_list(TokenStream&, bool allow_separate_line_name_blocks = false); RefPtr parse_grid_auto_track_sizes(TokenStream&); RefPtr parse_grid_auto_flow_value(TokenStream&); diff --git a/Libraries/LibWeb/CSS/Properties.json b/Libraries/LibWeb/CSS/Properties.json index b3252cae1b84..346c9e4ff681 100644 --- a/Libraries/LibWeb/CSS/Properties.json +++ b/Libraries/LibWeb/CSS/Properties.json @@ -2370,6 +2370,13 @@ "unitless-length" ] }, + "scale": { + "animation-type": "custom", + "inherited": false, + "initial": "none", + "affects-layout": false, + "affects-stacking-context": true + }, "scrollbar-gutter": { "affects-layout": false, "animation-type": "discrete", diff --git a/Libraries/LibWeb/CSS/StyleProperties.cpp b/Libraries/LibWeb/CSS/StyleProperties.cpp index cc8ba99ce042..396a8a98a68b 100644 --- a/Libraries/LibWeb/CSS/StyleProperties.cpp +++ b/Libraries/LibWeb/CSS/StyleProperties.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -614,6 +615,20 @@ Optional StyleProperties::translate() const return CSS::Transformation(CSS::TransformFunction::Translate, move(values)); } +Optional StyleProperties::scale() const +{ + auto const& value = property(CSS::PropertyID::Scale); + if (!value.is_scale()) + return {}; + auto const& scale = value.as_scale(); + + Vector values; + values.append(scale.x()); + values.append(scale.y()); + + return CSS::Transformation(CSS::TransformFunction::Scale, move(values)); +} + static Optional length_percentage_for_style_value(CSSStyleValue const& value) { if (value.is_length()) diff --git a/Libraries/LibWeb/CSS/StyleProperties.h b/Libraries/LibWeb/CSS/StyleProperties.h index 6d4b7033eddb..4eb45869b40a 100644 --- a/Libraries/LibWeb/CSS/StyleProperties.h +++ b/Libraries/LibWeb/CSS/StyleProperties.h @@ -177,6 +177,7 @@ class StyleProperties { CSS::TransformOrigin transform_origin() const; Optional rotate(Layout::Node const&) const; Optional translate() const; + Optional scale() const; Optional mask_type() const; Color stop_color() const; diff --git a/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp new file mode 100644 index 000000000000..0f53d2a3a365 --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::CSS { + +// https://www.w3.org/TR/2021/WD-css-transforms-2-20211109/#individual-transform-serialization +String ScaleStyleValue::to_string() const +{ + auto resolve_to_string = [](NumberPercentage const& value) -> String { + if (value.is_number()) { + return MUST(String::formatted("{}", value.number().value())); + } + if (value.is_percentage()) { + return MUST(String::formatted("{}", value.percentage().value() / 100.0)); + } + return value.to_string(); + }; + + auto x_value = resolve_to_string(m_properties.x); + auto y_value = resolve_to_string(m_properties.y); + + StringBuilder builder; + builder.append(x_value); + if (x_value != y_value) { + builder.append(" "sv); + builder.append(y_value); + } + return builder.to_string_without_validation(); +} + +} diff --git a/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h new file mode 100644 index 000000000000..f3b36dde99fc --- /dev/null +++ b/Libraries/LibWeb/CSS/StyleValues/ScaleStyleValue.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024, Andreas Kling + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Web::CSS { + +class ScaleStyleValue : public StyleValueWithDefaultOperators { +public: + static ValueComparingNonnullRefPtr create(NumberPercentage x, NumberPercentage y) + { + return adopt_ref(*new (nothrow) ScaleStyleValue(move(x), move(y))); + } + + virtual ~ScaleStyleValue() override = default; + + NumberPercentage const& x() const { return m_properties.x; } + NumberPercentage const& y() const { return m_properties.y; } + + virtual String to_string() const override; + + bool properties_equal(ScaleStyleValue const& other) const { return m_properties == other.m_properties; } + +private: + explicit ScaleStyleValue( + NumberPercentage x, + NumberPercentage y) + : StyleValueWithDefaultOperators(Type::Scale) + , m_properties { + .x = move(x), + .y = move(y), + } + { + } + + struct Properties { + NumberPercentage x; + NumberPercentage y; + bool operator==(Properties const&) const = default; + } m_properties; +}; + +} diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 48f4fea8ead8..46383286d9b4 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -205,6 +205,7 @@ class Resolution; class ResolutionOrCalculated; class ResolutionStyleValue; class RotationStyleValue; +class ScaleStyleValue; class Screen; class ScreenOrientation; class ScrollbarGutterStyleValue; diff --git a/Libraries/LibWeb/Layout/Node.cpp b/Libraries/LibWeb/Layout/Node.cpp index 943f421b02bc..ec54f02dd4ce 100644 --- a/Libraries/LibWeb/Layout/Node.cpp +++ b/Libraries/LibWeb/Layout/Node.cpp @@ -86,6 +86,8 @@ bool Node::can_contain_boxes_with_position_absolute() const return true; if (computed_values().rotate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; return false; } @@ -185,6 +187,9 @@ bool Node::establishes_stacking_context() const if (computed_values().rotate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; + // Element that is a child of a flex container, with z-index value other than auto. if (parent() && parent()->display().is_flex_inside() && computed_values().z_index().has_value()) return true; @@ -725,6 +730,9 @@ void NodeWithStyle::apply_style(const CSS::StyleProperties& computed_style) if (auto translate_value = computed_style.translate(); translate_value.has_value()) computed_values.set_translate(translate_value.release_value()); + if (auto scale_value = computed_style.scale(); scale_value.has_value()) + computed_values.set_scale(scale_value.release_value()); + computed_values.set_transformations(computed_style.transformations()); if (auto transform_box = computed_style.transform_box(); transform_box.has_value()) computed_values.set_transform_box(transform_box.value()); diff --git a/Libraries/LibWeb/Layout/Node.h b/Libraries/LibWeb/Layout/Node.h index b4f98110b031..8e751ca61537 100644 --- a/Libraries/LibWeb/Layout/Node.h +++ b/Libraries/LibWeb/Layout/Node.h @@ -186,6 +186,8 @@ class Node return true; if (computed_values().translate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; return false; } diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index c8791587198f..cffeb0110317 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1130,12 +1130,15 @@ void PaintableBox::resolve_paint_properties() auto const& transformations = computed_values.transformations(); auto const& translate = computed_values.translate(); auto const& rotate = computed_values.rotate(); - if (!transformations.is_empty() || translate.has_value() || rotate.has_value()) { + auto const& scale = computed_values.scale(); + if (!transformations.is_empty() || translate.has_value() || rotate.has_value() || scale.has_value()) { auto matrix = Gfx::FloatMatrix4x4::identity(); if (translate.has_value()) matrix = matrix * translate->to_matrix(*this).release_value(); if (rotate.has_value()) matrix = matrix * rotate->to_matrix(*this).release_value(); + if (scale.has_value()) + matrix = matrix * scale->to_matrix(*this).release_value(); for (auto const& transform : transformations) matrix = matrix * transform.to_matrix(*this).release_value(); set_transform(matrix); diff --git a/Libraries/LibWeb/Painting/PaintableBox.h b/Libraries/LibWeb/Painting/PaintableBox.h index 25bb4d97b781..8b3c8437649f 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.h +++ b/Libraries/LibWeb/Painting/PaintableBox.h @@ -137,6 +137,8 @@ class PaintableBox : public Paintable return true; if (computed_values().translate().has_value()) return true; + if (computed_values().scale().has_value()) + return true; return false; } diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt index b0b29e54c838..41ac64ef8352 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-all-supported-properties-and-default-values.txt @@ -1,6 +1,6 @@ All supported properties and their default values exposed from CSSStyleDeclaration from getComputedStyle: 'cssText': '' -'length': '205' +'length': '206' 'parentRule': 'null' 'cssFloat': 'none' 'WebkitAlignContent': 'normal' @@ -486,6 +486,7 @@ All supported properties and their default values exposed from CSSStyleDeclarati 'row-gap': 'normal' 'rx': 'auto' 'ry': 'auto' +'scale': 'none' 'scrollbarGutter': 'auto' 'scrollbar-gutter': 'auto' 'scrollbarWidth': 'auto' diff --git a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt index 5f6b0f5553dd..1354e1e73363 100644 --- a/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt +++ b/Tests/LibWeb/Text/expected/css/CSSStyleDeclaration-has-indexed-property-getter.txt @@ -180,31 +180,32 @@ All properties associated with getComputedStyle(document.body): "177": "row-gap", "178": "rx", "179": "ry", - "180": "scrollbar-gutter", - "181": "scrollbar-width", - "182": "stop-color", - "183": "stop-opacity", - "184": "table-layout", - "185": "text-decoration-color", - "186": "text-decoration-style", - "187": "text-decoration-thickness", - "188": "text-overflow", - "189": "top", - "190": "transform", - "191": "transform-box", - "192": "transform-origin", - "193": "transition-delay", - "194": "transition-duration", - "195": "transition-property", - "196": "transition-timing-function", - "197": "translate", - "198": "unicode-bidi", - "199": "user-select", - "200": "vertical-align", - "201": "width", - "202": "x", - "203": "y", - "204": "z-index" + "180": "scale", + "181": "scrollbar-gutter", + "182": "scrollbar-width", + "183": "stop-color", + "184": "stop-opacity", + "185": "table-layout", + "186": "text-decoration-color", + "187": "text-decoration-style", + "188": "text-decoration-thickness", + "189": "text-overflow", + "190": "top", + "191": "transform", + "192": "transform-box", + "193": "transform-origin", + "194": "transition-delay", + "195": "transition-duration", + "196": "transition-property", + "197": "transition-timing-function", + "198": "translate", + "199": "unicode-bidi", + "200": "user-select", + "201": "vertical-align", + "202": "width", + "203": "x", + "204": "y", + "205": "z-index" } All properties associated with document.body.style by default: {} diff --git a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt index 1d91f41e986f..dc729509efc1 100644 --- a/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt +++ b/Tests/LibWeb/Text/expected/css/getComputedStyle-print-all.txt @@ -178,6 +178,7 @@ rotate: none row-gap: normal rx: auto ry: auto +scale: none scrollbar-gutter: auto scrollbar-width: auto stop-color: rgb(0, 0, 0) diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt index e68f6c0f28cc..3e801e924294 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-computed.txt @@ -6,25 +6,26 @@ Rerun Found 22 tests -22 Fail +13 Pass +9 Fail Details -Result Test Name MessageFail Property scale value 'none' -Fail Property scale value '1' -Fail Property scale value '1%' -Fail Property scale value '100' -Fail Property scale value '100%' -Fail Property scale value '100 100' -Fail Property scale value '100% 100%' +Result Test Name MessagePass Property scale value 'none' +Pass Property scale value '1' +Pass Property scale value '1%' +Pass Property scale value '100' +Pass Property scale value '100%' +Pass Property scale value '100 100' +Pass Property scale value '100% 100%' Fail Property scale value '100 100 1' Fail Property scale value '100% 100% 1' -Fail Property scale value '-100' -Fail Property scale value '-100%' -Fail Property scale value '-100 -100' -Fail Property scale value '-100% -100%' +Pass Property scale value '-100' +Pass Property scale value '-100%' +Pass Property scale value '-100 -100' +Pass Property scale value '-100% -100%' Fail Property scale value '-100 -100 1' Fail Property scale value '-100% -100% 1' -Fail Property scale value '100 200' -Fail Property scale value '100% 200%' +Pass Property scale value '100 200' +Pass Property scale value '100% 200%' Fail Property scale value '100 200 1' Fail Property scale value '100% 200% 1' Fail Property scale value '100 200 300' diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt index df230dc5ee65..f641c6ed6da7 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-transforms/parsing/scale-parsing-valid.txt @@ -6,25 +6,26 @@ Rerun Found 22 tests -22 Fail +13 Pass +9 Fail Details -Result Test Name MessageFail e.style['scale'] = "none" should set the property value -Fail e.style['scale'] = "1" should set the property value -Fail e.style['scale'] = "1%" should set the property value -Fail e.style['scale'] = "100" should set the property value -Fail e.style['scale'] = "100%" should set the property value -Fail e.style['scale'] = "100 100" should set the property value -Fail e.style['scale'] = "100% 100%" should set the property value +Result Test Name MessagePass e.style['scale'] = "none" should set the property value +Pass e.style['scale'] = "1" should set the property value +Pass e.style['scale'] = "1%" should set the property value +Pass e.style['scale'] = "100" should set the property value +Pass e.style['scale'] = "100%" should set the property value +Pass e.style['scale'] = "100 100" should set the property value +Pass e.style['scale'] = "100% 100%" should set the property value Fail e.style['scale'] = "100 100 1" should set the property value Fail e.style['scale'] = "100% 100% 1" should set the property value -Fail e.style['scale'] = "-100" should set the property value -Fail e.style['scale'] = "-100%" should set the property value -Fail e.style['scale'] = "-100 -100" should set the property value -Fail e.style['scale'] = "-100% -100%" should set the property value +Pass e.style['scale'] = "-100" should set the property value +Pass e.style['scale'] = "-100%" should set the property value +Pass e.style['scale'] = "-100 -100" should set the property value +Pass e.style['scale'] = "-100% -100%" should set the property value Fail e.style['scale'] = "-100 -100 1" should set the property value Fail e.style['scale'] = "-100% -100% 1" should set the property value -Fail e.style['scale'] = "100 200" should set the property value -Fail e.style['scale'] = "100% 200%" should set the property value +Pass e.style['scale'] = "100 200" should set the property value +Pass e.style['scale'] = "100% 200%" should set the property value Fail e.style['scale'] = "100 200 1" should set the property value Fail e.style['scale'] = "100% 200% 1" should set the property value Fail e.style['scale'] = "100 200 300" should set the property value From e2b21157222f4c0bc59e2aabf9c386119192fba7 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Sat, 23 Nov 2024 02:34:07 +0100 Subject: [PATCH 014/397] LibWeb: Fix spec link in FlexFormattingContext --- Libraries/LibWeb/Layout/FlexFormattingContext.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/LibWeb/Layout/FlexFormattingContext.cpp b/Libraries/LibWeb/Layout/FlexFormattingContext.cpp index cecdf2d476b6..7b06113da79c 100644 --- a/Libraries/LibWeb/Layout/FlexFormattingContext.cpp +++ b/Libraries/LibWeb/Layout/FlexFormattingContext.cpp @@ -1201,7 +1201,7 @@ void FlexFormattingContext::calculate_cross_size_of_each_flex_line() } } -// https://www.w3.org/TR/css-flexbox-1/#cross-sizing +// https://www.w3.org/TR/css-flexbox-1/#algo-stretch void FlexFormattingContext::determine_used_cross_size_of_each_flex_item() { for (auto& flex_line : m_flex_lines) { From 3fffd1129c89d21755def287f3735d33515c7480 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 23 Nov 2024 08:01:49 +1300 Subject: [PATCH 015/397] LibWeb: Implement overload resolution for sequence types Fixes: #2508 --- .../LibWeb/WebIDL/OverloadResolution.cpp | 28 +++++++++++++++++-- ...annel_postMessage_clone_port_error.any.txt | 11 ++++++++ ...nnel_postMessage_clone_port_error.any.html | 15 ++++++++++ ...hannel_postMessage_clone_port_error.any.js | 14 ++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.js diff --git a/Libraries/LibWeb/WebIDL/OverloadResolution.cpp b/Libraries/LibWeb/WebIDL/OverloadResolution.cpp index a4d762bdcc1a..2362f3c37853 100644 --- a/Libraries/LibWeb/WebIDL/OverloadResolution.cpp +++ b/Libraries/LibWeb/WebIDL/OverloadResolution.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2022, Sam Atkins + * Copyright (c) 2024, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ @@ -77,7 +78,7 @@ JS::ThrowCompletionOr resolve_overload(JS::VM& vm, IDL::Effect auto distinguishing_argument_index = -1; // 7. Initialize method to undefined. - Optional method; + GC::Ptr method; // 8. If there is more than one entry in S, then set d to be the distinguishing argument index for the entries of S. if (overloads.size() > 1) @@ -253,7 +254,7 @@ JS::ThrowCompletionOr resolve_overload(JS::VM& vm, IDL::Effect overloads.remove_all_other_entries(); } - // FIXME: 9. Otherwise: if Type(V) is Object and there is an entry in S that has one of the following types at position i of its type list, + // 9. Otherwise: if Type(V) is Object and there is an entry in S that has one of the following types at position i of its type list, // - a sequence type // - a frozen array type // - a nullable version of any of the above types @@ -264,7 +265,28 @@ JS::ThrowCompletionOr resolve_overload(JS::VM& vm, IDL::Effect // 1. Let method be ? GetMethod(V, @@iterator). // } // method is not undefined, then remove from S all other entries. + else if (value.is_object() + && has_overload_with_argument_type_or_subtype_matching(overloads, i, [&vm, &method, &value](IDL::Type const& type) { + // - a sequence type + // FIXME: - a frozen array type + // - a nullable version of any of the above types + if (type.name() != "sequence") + return false; + + // and after performing the following steps, + // { + // 1. Let method be ? GetMethod(V, @@iterator). + // } + // method is not undefined, then remove from S all other entries. + auto maybe_method = value.get_method(vm, vm.well_known_symbol_iterator()); + if (maybe_method.is_error() || maybe_method.value() == nullptr) + return false; + method = *maybe_method.release_value(); + return true; + })) { + overloads.remove_all_other_entries(); + } // 10. Otherwise: if Type(V) is Object and there is an entry in S that has one of the following types at position i of its type list, // - a callback interface type // - a dictionary type @@ -375,7 +397,7 @@ JS::ThrowCompletionOr resolve_overload(JS::VM& vm, IDL::Effect auto const& callable = overloads.only_item(); // 14. If i = d and method is not undefined, then - if (i == distinguishing_argument_index && method.has_value()) { + if (i == distinguishing_argument_index && method) { // 1. Let V be args[i]. auto const& value = vm.argument(i); diff --git a/Tests/LibWeb/Text/expected/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.txt b/Tests/LibWeb/Text/expected/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.txt new file mode 100644 index 000000000000..ba4c49f7f5c9 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.txt @@ -0,0 +1,11 @@ +Summary + +Harness status: OK + +Rerun + +Found 1 tests + +1 Pass +Details +Result Test Name MessagePass Test Description: Throw a DataCloneError if transfer array in postMessage contains source port. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.html b/Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.html new file mode 100644 index 000000000000..cd8a947c744a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.html @@ -0,0 +1,15 @@ + + +postMessage() DataCloneError: cloning source port + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.js b/Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.js new file mode 100644 index 000000000000..cbee47270c46 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/webmessaging/Channel_postMessage_clone_port_error.any.js @@ -0,0 +1,14 @@ +// META: title=postMessage() DataCloneError: cloning source port + + var description = "Test Description: Throw a DataCloneError if transfer array in postMessage contains source port."; + + test(function() + { + var channel = new MessageChannel(); + channel.port1.start(); + + assert_throws_dom("DATA_CLONE_ERR", function() + { + channel.port1.postMessage("ports", [channel.port1]); + }); + }, description); From b22341fc772b538f725f2657c2e3af39cb1af143 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Sun, 10 Nov 2024 17:39:40 +0100 Subject: [PATCH 016/397] LibGfx: Support AVIF images with missing pixi property --- Libraries/LibGfx/ImageFormats/AVIFLoader.cpp | 5 +++++ Tests/LibGfx/TestImageDecoder.cpp | 6 ++++++ .../test-inputs/avif/missing-pixi-property.avif | Bin 0 -> 3315 bytes 3 files changed, 11 insertions(+) create mode 100644 Tests/LibGfx/test-inputs/avif/missing-pixi-property.avif diff --git a/Libraries/LibGfx/ImageFormats/AVIFLoader.cpp b/Libraries/LibGfx/ImageFormats/AVIFLoader.cpp index f10030bbcccb..1f68fc3c91b5 100644 --- a/Libraries/LibGfx/ImageFormats/AVIFLoader.cpp +++ b/Libraries/LibGfx/ImageFormats/AVIFLoader.cpp @@ -68,6 +68,11 @@ static ErrorOr decode_avif_header(AVIFLoadingContext& context) if (context.decoder == nullptr) { return Error::from_string_literal("failed to allocate AVIF decoder"); } + + // This makes the decoder not error if an item in the file is missing the mandatory pixi property. + // Reason for this is that older versions of ImageMagick do not set this property, which leads to + // broken web content if the error is not ignored. + context.decoder->strictFlags &= ~AVIF_STRICT_PIXI_REQUIRED; } avifResult result = avifDecoderSetIOMemory(context.decoder, context.data.data(), context.data.size()); diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index f9287720d77d..201553c870d4 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -1067,3 +1067,9 @@ TEST_CASE(test_avif_frame_out_of_bounds) auto frame1 = TRY_OR_FAIL(plugin_decoder->frame(0)); EXPECT(plugin_decoder->frame(1).is_error()); } + +TEST_CASE(test_avif_missing_pixi_property) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("avif/missing-pixi-property.avif"sv))); + EXPECT(Gfx::AVIFImageDecoderPlugin::sniff(file->bytes())); +} diff --git a/Tests/LibGfx/test-inputs/avif/missing-pixi-property.avif b/Tests/LibGfx/test-inputs/avif/missing-pixi-property.avif new file mode 100644 index 0000000000000000000000000000000000000000..0c538b1655fd94c5c19d9c4c93b99f0fa331a1aa GIT binary patch literal 3315 zcmXv}1y~bY7apULlI|EWKsp7HkdPQ5NK1?!IbtIe21qL)h|(pXbV(}BB&AVebcb{Z zL*U2n|GxJ;_q_3(`<#0L004udZvY&MggF9k@t56Uj$*fR+TFny`Zv1;r?b7A_x~jT z0EgN6{-^(AayZP_{U3vK+d(ilPrJWPRSAG|TYqu!F9YsiZygs5=JC(Me^T08;{g0S zbvp}1ivC>>FoVIp;kWz%2DkIPWokQ5H*XI+H-7-|wg^IQBT^_*OeK_z^i~3BU_Nk% zzv33<{~kIR-2J~V+`a=hGz|Cl642cq>PrP6A!K0PxyMWyBL0h>G8rcVfD?k}55&;` zgaGF5-RtV@6)ejT6O$=C-&k`Tlj=ITU;fxADRHFq@pZ}6c|C@+6$8$RO6uj@2|fQJ z^WoT8#S5C&v)@rOFw*fb7wArpzUF1|Xta%{ERRiU%*@P{UR>G z_)~RnQ5-^dzrQ^j@6F7MMusNa^pcMooge%>=qg`;c~8pT9L7hB@Q`ryFTX|B)EeYT zMY3|ecZB!^!Oy=>Kz$3m!!c=fUymLUbyFol9wo3;g=OTw|9)RCa|P{0rGTIfm#4aj zn8@ghS(Cr+rrcIiX-!{hp&LDF=204C)l(mTH>_UR1eZ28!^tzplNa5NdS?c`^+3!c-{o zd2-68TlanLg9%N$3=Z+mM#D-EOyzio0w}7mF2{3-^I(&R+<}6sgYa+%qhkpiU(3^Q zK}Lw{uE%7ms76oyT%pLZYtK#!jYgJty-n?CHnS3CCjBPjCAC{e#vm?r*Yh%Zh0KE? zXM_(^f)9%X3Tmjz=rbxos@=n#0jdQ~Dh!?;XG6%ZvY@{|e;QaLE2<;$%{)C;za0@9v6F9&-)io8VVcU-QVEuhY|+9mbQpD^l&Ad~xck z+?SrQ-#C4!4h>VN(J#*HGZsrjmRUD^lg&~k{GZcTm{q+hiV-Mk_V2KNQW8q8AN;oA zOuaNqdF%4-f~il$=C<^P>V2}HSLPgubC=qn&su`&(j#%A@kzQ(5I|=v`X=0}v5^;63_ijgEOthQ~U@mX(6Dqe*r6yR( z<(hp?+8b2r;E9@|eK5)BLEA8{3!w^qcXyN^->>PC0I{&OOlI9o%0$cfm?#c%We zc{5`g8$uWlSu6^;5^3|`VjmVeYS(YhRkA>*zn0pjfeUz+#BnF}2H?3U@YNmQh>XP_5%4QrdXR3cACdD0-zQ_y;kwN`wI zrb}Qu@WBMFh@jR4KrPyMJ+p=gv1+R-*}j*fL;<&-z9F-~$fit98;#s8$_m3$o8}~l zIS9#u@hb33w)rdGN4!l9VPRWfabiKlvb7%)zQ~+_WAF>Bze1QuL2b$E3tvXNOo}yH zguiBIX_ZU-!tjG&vJ3;TSrEzDe4QBaum%1A)inMSNUzhA39&zly&EP+o;z10)9yJbKNf3 za)8IIq_yr~OK?C~q&=vJ8$)Yq9~IT91AQpIhCX|Uxy~niLvY9`(g~}9op2-&TDT0m zg^=Vrc!k4>@hvrz=`Q$~yk;nQS-L#n2fFNGdTnOhvY6&=Sce$#ae@+Zp74u2BG62+=}jemRQs=sjbkS zQqMc?LAP+YE-;b7;_?2)x7V#}0-6Qq8gAT8;7`ktHEV}Z-T6(GFfsf&UEjQ^G`Xa> zaGRW-!+DM`ZGl>=2?+6(Zt56gUT4D!2Q;iVf`q29b!YQ~*Vf23_NDFK^B?(2c=1;h z1$>0MFTtw1=CjoE#i0^Q$eI0dLXpeJ+4}OK5e=P7heW8X)HG3f z?NUqyCj$I16lJ%X$iUi7qN8cm?S{`RekuNof}2Ew}2iI>BCq1HRgnT z7RVvlUNHUIBR^3!PRa`uT;{%FZVi@tWMIsm*fb!-@T@rCi69Urlvy_2xT_;CFqJKKi+o>e>b;T%uRu>_q zxAs{Gw&a^GYt`{!K-nJ}*T62R$SpQno#ldh5VpZ55qq5K?+MCZ!rE*} zI*&mG@G^3xd2C{3vxW9E1@n_9k@Xa%Y#IF8jXr1&S97{0it{SNm!l_IUXzBubdY2kTM(2phES>5y6>Ei3d$CYu4Pnw8PNyihldkRQ*aZ(%CFa zg!-N)oqcxrccLi~{#?M@`Tk%AV(5J8b>Mby-|tRw?o+sRLN>S{aBBGU zm({5_`zWv1jE~@6eCjLysIs~ZXkC!zWn0#qnw-hI$}9gFtM2!lZXod6PJ2u$tu2U| zWr6qXoyQ-h`ExO(p7RzrB6DE)%Q_QPu%AkcmHS~J~2zp_P zw8ZZnx;IyikyN&ZmQe$DISF+*$n4QO6e5!RtlBaSEw> zTwR%+0$nT(t;!l)vW03r=5$q~SCK22UGhOj7PHOd{P%P>RYdyjCwJ z+Iv0)Ba5$kOfLyQ)`1lyrcoC|Qt=Z~Wa>IKvv5ntAM3ft=NyAAIwS&|4^ezP+Cn`S zhmB})SGTz3#bRsh>k0>>6H7sj*9Y^4L){H0d8kMhGqhC)hYZlcbEy4^`PSh=Q4zg= z;AhLyr#ioqltm3c3^TBCj>`PDEQpp2-%=CmiQ!jFf#nseywTh@zOq;Wa31SMG!Fbp z2d2mEq)emZiMsKtX@ktzx&7QC9Ayae=e#4Q`a9;C2_Hs6qK!L1JXOHReP!Wql8aw7 z7EY=9mTe|FJv0<#j_$lP;zZYC#dFa60hOGpTxSO9ie6upzhPcxICPJ^jh$kTDDt@> zB-T(9Wet@3fo=tleGa_VrDB<*s?&X(<`9WN4u%veDM~yQ@|>S+=g=QCz$jd1fFhG4 bvSdGJrR^k^lknRB6cZ+ literal 0 HcmV?d00001 From 8965698ce7959df50a1984cd66831c4c80a12b2e Mon Sep 17 00:00:00 2001 From: sideshowbarker Date: Thu, 21 Nov 2024 05:03:28 +0900 Subject: [PATCH 017/397] LibWeb: Support accessible-name computation for SVG elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change adds support for computing accessible names for SVG elements, per the https://w3c.github.io/svg-aam/#mapping_additional_nd spec requirements. Otherwise, without this change, accessible names for SVG elements don’t get exposed as expected. --- Libraries/LibWeb/DOM/Node.cpp | 24 +++++-- .../svg-aam/name/comp_host_language_label.txt | 22 ++++++ .../name/comp_host_language_label.html | 71 +++++++++++++++++++ 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/svg-aam/name/comp_host_language_label.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/svg-aam/name/comp_host_language_label.html diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index bc267ef72989..eb5817c30d24 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -51,6 +51,8 @@ #include #include #include +#include +#include namespace Web::DOM { @@ -2354,11 +2356,25 @@ ErrorOr Node::name_or_description(NameOrDescription target, Document con // element (e.g. HTML label or SVG title) that defines a text alternative, return that alternative in the form // of a flat string as defined by the host language, unless the element is marked as presentational // (role="presentation" or role="none"). - if (role != ARIA::Role::presentation && role != ARIA::Role::none && is(*element)) { + // TODO: Confirm (through existing WPT test cases) whether HTMLLabelElement is already handled (by the code for + // step C. “Embedded Control” above) in conformance with the spec requirements — and if not, then add handling. + if (role != ARIA::Role::presentation && role != ARIA::Role::none && is(*element)) return element->alternative_text().release_value(); - // TODO: Add handling for SVGTitleElement, and also confirm (through existing WPT test cases) whether - // HTMLLabelElement is already handled (by the code for step C. “Embedded Control” above) in conformance - // with the spec requirements — and if not, then add handling for it here. + // https://w3c.github.io/svg-aam/#mapping_additional_nd + Optional title_element_text; + if (element->is_svg_element()) { + // If the current node has at least one direct child title element, select the appropriate title based on + // the language rules for the SVG specification, and return the title text alternative as a flat string. + element->for_each_child_of_type([&](SVG::SVGTitleElement const& title) mutable { + title_element_text = title.text_content(); + return IterationDecision::Break; + }); + if (title_element_text.has_value()) + return title_element_text.release_value(); + // If the current node is a link, and there was no child title element, but it has an xlink:title attribute, + // return the value of that attribute. + if (auto title_attribute = element->get_attribute_ns(Namespace::XLink, XLink::AttributeNames::title); title_attribute.has_value()) + return title_attribute.release_value(); } // F. Otherwise, if the current node's role allows name from content, or if the current node is referenced by aria-labelledby, aria-describedby, or is a native host language text alternative element (e.g. label in HTML), or is a descendant of a native host language text alternative element: diff --git a/Tests/LibWeb/Text/expected/wpt-import/svg-aam/name/comp_host_language_label.txt b/Tests/LibWeb/Text/expected/wpt-import/svg-aam/name/comp_host_language_label.txt new file mode 100644 index 000000000000..43fad3ab8e00 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/svg-aam/name/comp_host_language_label.txt @@ -0,0 +1,22 @@ +Summary + +Harness status: OK + +Rerun + +Found 12 tests + +12 Pass +Details +Result Test Name MessagePass circle > title +Pass rect > title +Pass polygon > title +Pass g > title +Pass [xlink:title][href] > circle +Pass [xlink:title][href] > rect +Pass [xlink:title][href] > polygon +Pass [xlink:title][href] > g +Pass [xlink:title][xlink:href] > circle +Pass [xlink:title][xlink:href] > rect +Pass [xlink:title][xlink:href] > polygon +Pass [xlink:title][xlink:href] > g \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/svg-aam/name/comp_host_language_label.html b/Tests/LibWeb/Text/input/wpt-import/svg-aam/name/comp_host_language_label.html new file mode 100644 index 000000000000..8922f789f6ba --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/svg-aam/name/comp_host_language_label.html @@ -0,0 +1,71 @@ + + + + Name Comp: Host Language Label + + + + + + + + + +

SVG-AAM: Label Tests

+ +

Tests SVG-specific host language label rules (title element and xlink:title attr) in SVG-AAM §8.1 Name and Description, but note the open issues in SVG-AAM #31. + + +

SVG * > title

+ + circle label + rect label + polygon label +
+ + + group label + + + + +
+ + +

SVG a[xlink:title][href]

+ + + + +
+ + + + + + + + +
+ +

SVG a[xlink:title][xlink:href]:not([href])

+ + + + +
+ + + + + + + + +
+ + + + From bb5678a175b6b99818479f69c456e6ad27d4b3cc Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:48:50 +1100 Subject: [PATCH 018/397] LibWeb: Don't allow trailing commas in selector lists comma-separated list != #-multiplier --- Libraries/LibWeb/CSS/Parser/Parser.cpp | 2 + .../LibWeb/CSS/Parser/SelectorParsing.cpp | 17 +- Tests/LibWeb/TestConfig.ini | 3 + .../ParentNode-querySelector-All-content.html | 377 ++++ .../nodes/ParentNode-querySelector-All.txt | 1986 +++++++++++++++++ .../nodes/ParentNode-querySelector-All.html | 120 + .../dom/nodes/ParentNode-querySelector-All.js | 261 +++ .../input/wpt-import/dom/nodes/selectors.js | 755 +++++++ 8 files changed, 3515 insertions(+), 6 deletions(-) create mode 100644 Tests/LibWeb/Text/data/ParentNode-querySelector-All-content.html create mode 100644 Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/dom/nodes/selectors.js diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 0d85225db8f1..7b9a7e28b41d 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -768,6 +768,8 @@ Vector Parser::consume_a_list_of_component_values(TokenStream } } } +template Vector Parser::consume_a_list_of_component_values(TokenStream& input, Optional stop_token, Nested nested); +template Vector Parser::consume_a_list_of_component_values(TokenStream& input, Optional stop_token, Nested nested); // https://drafts.csswg.org/css-syntax/#consume-simple-block template diff --git a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 109dcb53d5b0..724292cb64cf 100644 --- a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -86,10 +86,9 @@ static NonnullRefPtr create_invalid_selector(Selector::Combinator comb template Parser::ParseErrorOr Parser::parse_a_selector_list(TokenStream& tokens, SelectorType mode, SelectorParsingMode parsing_mode) { - auto comma_separated_lists = parse_a_comma_separated_list_of_component_values(tokens); - SelectorList selectors; - for (auto& selector_parts : comma_separated_lists) { + for (;;) { + auto selector_parts = consume_a_list_of_component_values(tokens, Token::Type::Comma); auto stream = TokenStream(selector_parts); auto selector = parse_complex_selector(stream, mode); if (selector.is_error()) { @@ -97,11 +96,17 @@ Parser::ParseErrorOr Parser::parse_a_selector_list(TokenStream& // Keep the invalid selector around for serialization and nesting auto combinator = mode == SelectorType::Standalone ? Selector::Combinator::None : Selector::Combinator::Descendant; selectors.append(create_invalid_selector(combinator, move(selector_parts))); - continue; + } else { + return selector.error(); } - return selector.error(); + } else { + selectors.append(selector.release_value()); } - selectors.append(selector.release_value()); + + if (tokens.is_empty()) + break; + + tokens.discard_a_token(); } if (selectors.is_empty() && parsing_mode != SelectorParsingMode::Forgiving) diff --git a/Tests/LibWeb/TestConfig.ini b/Tests/LibWeb/TestConfig.ini index 0a44d0dcae5d..60ca3299bb3a 100644 --- a/Tests/LibWeb/TestConfig.ini +++ b/Tests/LibWeb/TestConfig.ini @@ -165,3 +165,6 @@ Text/input/wpt-import/css/css-backgrounds/animations/discrete-no-interpolation.h ; https://github.com/LadybirdBrowser/ladybird/issues/2314 Text/input/test-http-test-server.html + +; Too slow for CI +Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.html diff --git a/Tests/LibWeb/Text/data/ParentNode-querySelector-All-content.html b/Tests/LibWeb/Text/data/ParentNode-querySelector-All-content.html new file mode 100644 index 000000000000..8dc13545516f --- /dev/null +++ b/Tests/LibWeb/Text/data/ParentNode-querySelector-All-content.html @@ -0,0 +1,377 @@ + + + + + Selectors-API Test Suite: HTML with Selectors Level 2 using TestHarness: Test Document + + + + + + + + +
+
+ +
+

Universal selector tests inside element with id="universal".

+
+
Some preformatted text with some embedded code
+

This is a normal link: W3C

+
Some more nested elements code hyperlink
+
+ +
+
+
+
+
+

+

+    
+
    + + + + +
    + +
    +
    +
    +
    +
    + +
    + + + + + + + + + +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + + + + + + + + + +

    +
    + +
    +
    +
    +
    +
    +
    + +
    + + + + +
    +
    +
    +
    +
    + +

    +
    + +
    + + + + +
    +
    +
    +
    + +

    +
    + +
    + + + + +
    +
    +
    +
    +
    +
    + +

    +
    + +
    + + + + +
    + +
      +
    1. +
    2. +
    3. +
    4. +
    5. +
    6. +
    7. +
    8. +
    9. +
    10. +
    11. +
    12. +
    + +

    + span1 + em1 + + em2 + span2 + strong1 + em3 + span3 + span4 + strong2 + em4 +

    +
    + +
    +
    +
    +
    + +

    +

    +

    +
    + +
    +

    +

    +

    + +
    +
    +
    +
    + +
    +

    + +

    +

    + + +

    +

    + + + +

    +
    > + +
    +

    +

    +

    +

    Text node

    +

    +
    + + + +
    +
    +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +
    +
    + +

    +

    +

    +
    + +
    All pseudo-element tests
    + +
    +

    +

    +

    + + +
    +
    +

    +
    +

    +
    +
    +
    +
    + + + + + + +
    + +
    +
    +
    + +
      +
    • +
    • +
    • +
    • +
    + + + + + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +

    +
    +
    +
    +

    +

    +
    + +
    +
    +
    +
    +
    +
    +

    +
    +
    +
    +

    +

    +
    + +
    + + +
    +
    + + diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt new file mode 100644 index 000000000000..b0d3625ff6a5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt @@ -0,0 +1,1986 @@ +Summary + +Harness status: OK + +Rerun + +Found 1975 tests + +1903 Pass +72 Fail +Details +Result Test Name MessagePass Selectors-API Test Suite: HTML +Pass Document supports querySelector +Pass Document supports querySelectorAll +Pass Document.querySelectorAll returns NodeList instance +Pass Detached Element supports querySelector +Pass Detached Element supports querySelectorAll +Pass Detached Element.querySelectorAll returns NodeList instance +Pass Fragment supports querySelector +Pass Fragment supports querySelectorAll +Pass Fragment.querySelectorAll returns NodeList instance +Pass In-document Element supports querySelector +Pass In-document Element supports querySelectorAll +Pass In-document Element.querySelectorAll returns NodeList instance +Pass Document.querySelectorAll null +Pass Document.querySelectorAll undefined +Pass Document.querySelectorAll no parameter +Pass Document.querySelector null +Pass Document.querySelector undefined +Pass Document.querySelector no parameter +Pass Document.querySelectorAll tree order +Pass Detached Element.querySelectorAll null +Pass Detached Element.querySelectorAll undefined +Pass Detached Element.querySelectorAll no parameter +Pass Detached Element.querySelector null +Pass Detached Element.querySelector undefined +Pass Detached Element.querySelector no parameter +Pass Detached Element.querySelectorAll tree order +Pass Fragment.querySelectorAll null +Pass Fragment.querySelectorAll undefined +Pass Fragment.querySelectorAll no parameter +Pass Fragment.querySelector null +Pass Fragment.querySelector undefined +Pass Fragment.querySelector no parameter +Pass Fragment.querySelectorAll tree order +Pass In-document Element.querySelectorAll null +Pass In-document Element.querySelectorAll undefined +Pass In-document Element.querySelectorAll no parameter +Pass In-document Element.querySelector null +Pass In-document Element.querySelector undefined +Pass In-document Element.querySelector no parameter +Pass In-document Element.querySelectorAll tree order +Pass Document: static NodeList +Pass Document: new NodeList +Pass Detached Element: static NodeList +Pass Detached Element: new NodeList +Pass Fragment: static NodeList +Pass Fragment: new NodeList +Pass In-document Element: static NodeList +Pass In-document Element: new NodeList +Pass Document.querySelector: Empty String: +Pass Document.querySelectorAll: Empty String: +Pass Document.querySelector: Invalid character: [ +Pass Document.querySelectorAll: Invalid character: [ +Pass Document.querySelector: Invalid character: ] +Pass Document.querySelectorAll: Invalid character: ] +Pass Document.querySelector: Invalid character: ( +Pass Document.querySelectorAll: Invalid character: ( +Pass Document.querySelector: Invalid character: ) +Pass Document.querySelectorAll: Invalid character: ) +Pass Document.querySelector: Invalid character: { +Pass Document.querySelectorAll: Invalid character: { +Pass Document.querySelector: Invalid character: } +Pass Document.querySelectorAll: Invalid character: } +Pass Document.querySelector: Invalid character: < +Pass Document.querySelectorAll: Invalid character: < +Pass Document.querySelector: Invalid character: > +Pass Document.querySelectorAll: Invalid character: > +Pass Document.querySelector: Invalid ID: # +Pass Document.querySelectorAll: Invalid ID: # +Pass Document.querySelector: Invalid group of selectors: div, +Pass Document.querySelectorAll: Invalid group of selectors: div, +Pass Document.querySelector: Invalid class: . +Pass Document.querySelectorAll: Invalid class: . +Pass Document.querySelector: Invalid class: .5cm +Pass Document.querySelectorAll: Invalid class: .5cm +Pass Document.querySelector: Invalid class: ..test +Pass Document.querySelectorAll: Invalid class: ..test +Pass Document.querySelector: Invalid class: .foo..quux +Pass Document.querySelectorAll: Invalid class: .foo..quux +Pass Document.querySelector: Invalid class: .bar. +Pass Document.querySelectorAll: Invalid class: .bar. +Pass Document.querySelector: Invalid combinator: div % address, p +Pass Document.querySelectorAll: Invalid combinator: div % address, p +Fail Document.querySelector: Invalid combinator: div ++ address, p +Fail Document.querySelectorAll: Invalid combinator: div ++ address, p +Fail Document.querySelector: Invalid combinator: div ~~ address, p +Fail Document.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Document.querySelector: Invalid [att=value] selector: [*=test] +Pass Document.querySelectorAll: Invalid [att=value] selector: [*=test] +Pass Document.querySelector: Invalid [att=value] selector: [*|*=test] +Pass Document.querySelectorAll: Invalid [att=value] selector: [*|*=test] +Pass Document.querySelector: Invalid [att=value] selector: [class= space unquoted ] +Pass Document.querySelectorAll: Invalid [att=value] selector: [class= space unquoted ] +Pass Document.querySelector: Unknown pseudo-class: div:example +Pass Document.querySelectorAll: Unknown pseudo-class: div:example +Pass Document.querySelector: Unknown pseudo-class: :example +Pass Document.querySelectorAll: Unknown pseudo-class: :example +Pass Document.querySelector: Unknown pseudo-class: div:linkexample +Pass Document.querySelectorAll: Unknown pseudo-class: div:linkexample +Pass Document.querySelector: Unknown pseudo-element: div::example +Pass Document.querySelectorAll: Unknown pseudo-element: div::example +Pass Document.querySelector: Unknown pseudo-element: ::example +Pass Document.querySelectorAll: Unknown pseudo-element: ::example +Pass Document.querySelector: Invalid pseudo-element: :::before +Pass Document.querySelectorAll: Invalid pseudo-element: :::before +Pass Document.querySelector: Invalid pseudo-element: :: before +Pass Document.querySelectorAll: Invalid pseudo-element: :: before +Fail Document.querySelector: Undeclared namespace: ns|div +Fail Document.querySelectorAll: Undeclared namespace: ns|div +Fail Document.querySelector: Undeclared namespace: :not(ns|div) +Fail Document.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Document.querySelector: Invalid namespace: ^|div +Pass Document.querySelectorAll: Invalid namespace: ^|div +Pass Document.querySelector: Invalid namespace: $|div +Pass Document.querySelectorAll: Invalid namespace: $|div +Pass Document.querySelector: Relative selector: >* +Pass Document.querySelectorAll: Relative selector: >* +Pass Detached Element.querySelector: Empty String: +Pass Detached Element.querySelectorAll: Empty String: +Pass Detached Element.querySelector: Invalid character: [ +Pass Detached Element.querySelectorAll: Invalid character: [ +Pass Detached Element.querySelector: Invalid character: ] +Pass Detached Element.querySelectorAll: Invalid character: ] +Pass Detached Element.querySelector: Invalid character: ( +Pass Detached Element.querySelectorAll: Invalid character: ( +Pass Detached Element.querySelector: Invalid character: ) +Pass Detached Element.querySelectorAll: Invalid character: ) +Pass Detached Element.querySelector: Invalid character: { +Pass Detached Element.querySelectorAll: Invalid character: { +Pass Detached Element.querySelector: Invalid character: } +Pass Detached Element.querySelectorAll: Invalid character: } +Pass Detached Element.querySelector: Invalid character: < +Pass Detached Element.querySelectorAll: Invalid character: < +Pass Detached Element.querySelector: Invalid character: > +Pass Detached Element.querySelectorAll: Invalid character: > +Pass Detached Element.querySelector: Invalid ID: # +Pass Detached Element.querySelectorAll: Invalid ID: # +Pass Detached Element.querySelector: Invalid group of selectors: div, +Pass Detached Element.querySelectorAll: Invalid group of selectors: div, +Pass Detached Element.querySelector: Invalid class: . +Pass Detached Element.querySelectorAll: Invalid class: . +Pass Detached Element.querySelector: Invalid class: .5cm +Pass Detached Element.querySelectorAll: Invalid class: .5cm +Pass Detached Element.querySelector: Invalid class: ..test +Pass Detached Element.querySelectorAll: Invalid class: ..test +Pass Detached Element.querySelector: Invalid class: .foo..quux +Pass Detached Element.querySelectorAll: Invalid class: .foo..quux +Pass Detached Element.querySelector: Invalid class: .bar. +Pass Detached Element.querySelectorAll: Invalid class: .bar. +Pass Detached Element.querySelector: Invalid combinator: div % address, p +Pass Detached Element.querySelectorAll: Invalid combinator: div % address, p +Fail Detached Element.querySelector: Invalid combinator: div ++ address, p +Fail Detached Element.querySelectorAll: Invalid combinator: div ++ address, p +Fail Detached Element.querySelector: Invalid combinator: div ~~ address, p +Fail Detached Element.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Detached Element.querySelector: Invalid [att=value] selector: [*=test] +Pass Detached Element.querySelectorAll: Invalid [att=value] selector: [*=test] +Pass Detached Element.querySelector: Invalid [att=value] selector: [*|*=test] +Pass Detached Element.querySelectorAll: Invalid [att=value] selector: [*|*=test] +Pass Detached Element.querySelector: Invalid [att=value] selector: [class= space unquoted ] +Pass Detached Element.querySelectorAll: Invalid [att=value] selector: [class= space unquoted ] +Pass Detached Element.querySelector: Unknown pseudo-class: div:example +Pass Detached Element.querySelectorAll: Unknown pseudo-class: div:example +Pass Detached Element.querySelector: Unknown pseudo-class: :example +Pass Detached Element.querySelectorAll: Unknown pseudo-class: :example +Pass Detached Element.querySelector: Unknown pseudo-class: div:linkexample +Pass Detached Element.querySelectorAll: Unknown pseudo-class: div:linkexample +Pass Detached Element.querySelector: Unknown pseudo-element: div::example +Pass Detached Element.querySelectorAll: Unknown pseudo-element: div::example +Pass Detached Element.querySelector: Unknown pseudo-element: ::example +Pass Detached Element.querySelectorAll: Unknown pseudo-element: ::example +Pass Detached Element.querySelector: Invalid pseudo-element: :::before +Pass Detached Element.querySelectorAll: Invalid pseudo-element: :::before +Pass Detached Element.querySelector: Invalid pseudo-element: :: before +Pass Detached Element.querySelectorAll: Invalid pseudo-element: :: before +Fail Detached Element.querySelector: Undeclared namespace: ns|div +Fail Detached Element.querySelectorAll: Undeclared namespace: ns|div +Fail Detached Element.querySelector: Undeclared namespace: :not(ns|div) +Fail Detached Element.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Detached Element.querySelector: Invalid namespace: ^|div +Pass Detached Element.querySelectorAll: Invalid namespace: ^|div +Pass Detached Element.querySelector: Invalid namespace: $|div +Pass Detached Element.querySelectorAll: Invalid namespace: $|div +Pass Detached Element.querySelector: Relative selector: >* +Pass Detached Element.querySelectorAll: Relative selector: >* +Pass Fragment.querySelector: Empty String: +Pass Fragment.querySelectorAll: Empty String: +Pass Fragment.querySelector: Invalid character: [ +Pass Fragment.querySelectorAll: Invalid character: [ +Pass Fragment.querySelector: Invalid character: ] +Pass Fragment.querySelectorAll: Invalid character: ] +Pass Fragment.querySelector: Invalid character: ( +Pass Fragment.querySelectorAll: Invalid character: ( +Pass Fragment.querySelector: Invalid character: ) +Pass Fragment.querySelectorAll: Invalid character: ) +Pass Fragment.querySelector: Invalid character: { +Pass Fragment.querySelectorAll: Invalid character: { +Pass Fragment.querySelector: Invalid character: } +Pass Fragment.querySelectorAll: Invalid character: } +Pass Fragment.querySelector: Invalid character: < +Pass Fragment.querySelectorAll: Invalid character: < +Pass Fragment.querySelector: Invalid character: > +Pass Fragment.querySelectorAll: Invalid character: > +Pass Fragment.querySelector: Invalid ID: # +Pass Fragment.querySelectorAll: Invalid ID: # +Pass Fragment.querySelector: Invalid group of selectors: div, +Pass Fragment.querySelectorAll: Invalid group of selectors: div, +Pass Fragment.querySelector: Invalid class: . +Pass Fragment.querySelectorAll: Invalid class: . +Pass Fragment.querySelector: Invalid class: .5cm +Pass Fragment.querySelectorAll: Invalid class: .5cm +Pass Fragment.querySelector: Invalid class: ..test +Pass Fragment.querySelectorAll: Invalid class: ..test +Pass Fragment.querySelector: Invalid class: .foo..quux +Pass Fragment.querySelectorAll: Invalid class: .foo..quux +Pass Fragment.querySelector: Invalid class: .bar. +Pass Fragment.querySelectorAll: Invalid class: .bar. +Pass Fragment.querySelector: Invalid combinator: div % address, p +Pass Fragment.querySelectorAll: Invalid combinator: div % address, p +Fail Fragment.querySelector: Invalid combinator: div ++ address, p +Fail Fragment.querySelectorAll: Invalid combinator: div ++ address, p +Fail Fragment.querySelector: Invalid combinator: div ~~ address, p +Fail Fragment.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Fragment.querySelector: Invalid [att=value] selector: [*=test] +Pass Fragment.querySelectorAll: Invalid [att=value] selector: [*=test] +Pass Fragment.querySelector: Invalid [att=value] selector: [*|*=test] +Pass Fragment.querySelectorAll: Invalid [att=value] selector: [*|*=test] +Pass Fragment.querySelector: Invalid [att=value] selector: [class= space unquoted ] +Pass Fragment.querySelectorAll: Invalid [att=value] selector: [class= space unquoted ] +Pass Fragment.querySelector: Unknown pseudo-class: div:example +Pass Fragment.querySelectorAll: Unknown pseudo-class: div:example +Pass Fragment.querySelector: Unknown pseudo-class: :example +Pass Fragment.querySelectorAll: Unknown pseudo-class: :example +Pass Fragment.querySelector: Unknown pseudo-class: div:linkexample +Pass Fragment.querySelectorAll: Unknown pseudo-class: div:linkexample +Pass Fragment.querySelector: Unknown pseudo-element: div::example +Pass Fragment.querySelectorAll: Unknown pseudo-element: div::example +Pass Fragment.querySelector: Unknown pseudo-element: ::example +Pass Fragment.querySelectorAll: Unknown pseudo-element: ::example +Pass Fragment.querySelector: Invalid pseudo-element: :::before +Pass Fragment.querySelectorAll: Invalid pseudo-element: :::before +Pass Fragment.querySelector: Invalid pseudo-element: :: before +Pass Fragment.querySelectorAll: Invalid pseudo-element: :: before +Fail Fragment.querySelector: Undeclared namespace: ns|div +Fail Fragment.querySelectorAll: Undeclared namespace: ns|div +Fail Fragment.querySelector: Undeclared namespace: :not(ns|div) +Fail Fragment.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Fragment.querySelector: Invalid namespace: ^|div +Pass Fragment.querySelectorAll: Invalid namespace: ^|div +Pass Fragment.querySelector: Invalid namespace: $|div +Pass Fragment.querySelectorAll: Invalid namespace: $|div +Pass Fragment.querySelector: Relative selector: >* +Pass Fragment.querySelectorAll: Relative selector: >* +Pass In-document Element.querySelector: Empty String: +Pass In-document Element.querySelectorAll: Empty String: +Pass In-document Element.querySelector: Invalid character: [ +Pass In-document Element.querySelectorAll: Invalid character: [ +Pass In-document Element.querySelector: Invalid character: ] +Pass In-document Element.querySelectorAll: Invalid character: ] +Pass In-document Element.querySelector: Invalid character: ( +Pass In-document Element.querySelectorAll: Invalid character: ( +Pass In-document Element.querySelector: Invalid character: ) +Pass In-document Element.querySelectorAll: Invalid character: ) +Pass In-document Element.querySelector: Invalid character: { +Pass In-document Element.querySelectorAll: Invalid character: { +Pass In-document Element.querySelector: Invalid character: } +Pass In-document Element.querySelectorAll: Invalid character: } +Pass In-document Element.querySelector: Invalid character: < +Pass In-document Element.querySelectorAll: Invalid character: < +Pass In-document Element.querySelector: Invalid character: > +Pass In-document Element.querySelectorAll: Invalid character: > +Pass In-document Element.querySelector: Invalid ID: # +Pass In-document Element.querySelectorAll: Invalid ID: # +Pass In-document Element.querySelector: Invalid group of selectors: div, +Pass In-document Element.querySelectorAll: Invalid group of selectors: div, +Pass In-document Element.querySelector: Invalid class: . +Pass In-document Element.querySelectorAll: Invalid class: . +Pass In-document Element.querySelector: Invalid class: .5cm +Pass In-document Element.querySelectorAll: Invalid class: .5cm +Pass In-document Element.querySelector: Invalid class: ..test +Pass In-document Element.querySelectorAll: Invalid class: ..test +Pass In-document Element.querySelector: Invalid class: .foo..quux +Pass In-document Element.querySelectorAll: Invalid class: .foo..quux +Pass In-document Element.querySelector: Invalid class: .bar. +Pass In-document Element.querySelectorAll: Invalid class: .bar. +Pass In-document Element.querySelector: Invalid combinator: div % address, p +Pass In-document Element.querySelectorAll: Invalid combinator: div % address, p +Fail In-document Element.querySelector: Invalid combinator: div ++ address, p +Fail In-document Element.querySelectorAll: Invalid combinator: div ++ address, p +Fail In-document Element.querySelector: Invalid combinator: div ~~ address, p +Fail In-document Element.querySelectorAll: Invalid combinator: div ~~ address, p +Pass In-document Element.querySelector: Invalid [att=value] selector: [*=test] +Pass In-document Element.querySelectorAll: Invalid [att=value] selector: [*=test] +Pass In-document Element.querySelector: Invalid [att=value] selector: [*|*=test] +Pass In-document Element.querySelectorAll: Invalid [att=value] selector: [*|*=test] +Pass In-document Element.querySelector: Invalid [att=value] selector: [class= space unquoted ] +Pass In-document Element.querySelectorAll: Invalid [att=value] selector: [class= space unquoted ] +Pass In-document Element.querySelector: Unknown pseudo-class: div:example +Pass In-document Element.querySelectorAll: Unknown pseudo-class: div:example +Pass In-document Element.querySelector: Unknown pseudo-class: :example +Pass In-document Element.querySelectorAll: Unknown pseudo-class: :example +Pass In-document Element.querySelector: Unknown pseudo-class: div:linkexample +Pass In-document Element.querySelectorAll: Unknown pseudo-class: div:linkexample +Pass In-document Element.querySelector: Unknown pseudo-element: div::example +Pass In-document Element.querySelectorAll: Unknown pseudo-element: div::example +Pass In-document Element.querySelector: Unknown pseudo-element: ::example +Pass In-document Element.querySelectorAll: Unknown pseudo-element: ::example +Pass In-document Element.querySelector: Invalid pseudo-element: :::before +Pass In-document Element.querySelectorAll: Invalid pseudo-element: :::before +Pass In-document Element.querySelector: Invalid pseudo-element: :: before +Pass In-document Element.querySelectorAll: Invalid pseudo-element: :: before +Fail In-document Element.querySelector: Undeclared namespace: ns|div +Fail In-document Element.querySelectorAll: Undeclared namespace: ns|div +Fail In-document Element.querySelector: Undeclared namespace: :not(ns|div) +Fail In-document Element.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass In-document Element.querySelector: Invalid namespace: ^|div +Pass In-document Element.querySelectorAll: Invalid namespace: ^|div +Pass In-document Element.querySelector: Invalid namespace: $|div +Pass In-document Element.querySelectorAll: Invalid namespace: $|div +Pass In-document Element.querySelector: Relative selector: >* +Pass In-document Element.querySelectorAll: Relative selector: >* +Pass Empty Element.querySelector: Empty String: +Pass Empty Element.querySelectorAll: Empty String: +Pass Empty Element.querySelector: Invalid character: [ +Pass Empty Element.querySelectorAll: Invalid character: [ +Pass Empty Element.querySelector: Invalid character: ] +Pass Empty Element.querySelectorAll: Invalid character: ] +Pass Empty Element.querySelector: Invalid character: ( +Pass Empty Element.querySelectorAll: Invalid character: ( +Pass Empty Element.querySelector: Invalid character: ) +Pass Empty Element.querySelectorAll: Invalid character: ) +Pass Empty Element.querySelector: Invalid character: { +Pass Empty Element.querySelectorAll: Invalid character: { +Pass Empty Element.querySelector: Invalid character: } +Pass Empty Element.querySelectorAll: Invalid character: } +Pass Empty Element.querySelector: Invalid character: < +Pass Empty Element.querySelectorAll: Invalid character: < +Pass Empty Element.querySelector: Invalid character: > +Pass Empty Element.querySelectorAll: Invalid character: > +Pass Empty Element.querySelector: Invalid ID: # +Pass Empty Element.querySelectorAll: Invalid ID: # +Pass Empty Element.querySelector: Invalid group of selectors: div, +Pass Empty Element.querySelectorAll: Invalid group of selectors: div, +Pass Empty Element.querySelector: Invalid class: . +Pass Empty Element.querySelectorAll: Invalid class: . +Pass Empty Element.querySelector: Invalid class: .5cm +Pass Empty Element.querySelectorAll: Invalid class: .5cm +Pass Empty Element.querySelector: Invalid class: ..test +Pass Empty Element.querySelectorAll: Invalid class: ..test +Pass Empty Element.querySelector: Invalid class: .foo..quux +Pass Empty Element.querySelectorAll: Invalid class: .foo..quux +Pass Empty Element.querySelector: Invalid class: .bar. +Pass Empty Element.querySelectorAll: Invalid class: .bar. +Pass Empty Element.querySelector: Invalid combinator: div % address, p +Pass Empty Element.querySelectorAll: Invalid combinator: div % address, p +Fail Empty Element.querySelector: Invalid combinator: div ++ address, p +Fail Empty Element.querySelectorAll: Invalid combinator: div ++ address, p +Fail Empty Element.querySelector: Invalid combinator: div ~~ address, p +Fail Empty Element.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Empty Element.querySelector: Invalid [att=value] selector: [*=test] +Pass Empty Element.querySelectorAll: Invalid [att=value] selector: [*=test] +Pass Empty Element.querySelector: Invalid [att=value] selector: [*|*=test] +Pass Empty Element.querySelectorAll: Invalid [att=value] selector: [*|*=test] +Pass Empty Element.querySelector: Invalid [att=value] selector: [class= space unquoted ] +Pass Empty Element.querySelectorAll: Invalid [att=value] selector: [class= space unquoted ] +Pass Empty Element.querySelector: Unknown pseudo-class: div:example +Pass Empty Element.querySelectorAll: Unknown pseudo-class: div:example +Pass Empty Element.querySelector: Unknown pseudo-class: :example +Pass Empty Element.querySelectorAll: Unknown pseudo-class: :example +Pass Empty Element.querySelector: Unknown pseudo-class: div:linkexample +Pass Empty Element.querySelectorAll: Unknown pseudo-class: div:linkexample +Pass Empty Element.querySelector: Unknown pseudo-element: div::example +Pass Empty Element.querySelectorAll: Unknown pseudo-element: div::example +Pass Empty Element.querySelector: Unknown pseudo-element: ::example +Pass Empty Element.querySelectorAll: Unknown pseudo-element: ::example +Pass Empty Element.querySelector: Invalid pseudo-element: :::before +Pass Empty Element.querySelectorAll: Invalid pseudo-element: :::before +Pass Empty Element.querySelector: Invalid pseudo-element: :: before +Pass Empty Element.querySelectorAll: Invalid pseudo-element: :: before +Fail Empty Element.querySelector: Undeclared namespace: ns|div +Fail Empty Element.querySelectorAll: Undeclared namespace: ns|div +Fail Empty Element.querySelector: Undeclared namespace: :not(ns|div) +Fail Empty Element.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Empty Element.querySelector: Invalid namespace: ^|div +Pass Empty Element.querySelectorAll: Invalid namespace: ^|div +Pass Empty Element.querySelector: Invalid namespace: $|div +Pass Empty Element.querySelectorAll: Invalid namespace: $|div +Pass Empty Element.querySelector: Relative selector: >* +Pass Empty Element.querySelectorAll: Relative selector: >* +Pass Document.querySelectorAll: Type selector, matching html element: html +Pass Document.querySelector: Type selector, matching html element: html +Pass Document.querySelectorAll: Type selector, matching body element: body +Pass Document.querySelector: Type selector, matching body element: body +Pass Document.querySelectorAll: Universal selector, matching all children of element with specified ID: #universal>* +Pass Document.querySelector: Universal selector, matching all children of element with specified ID: #universal>* +Pass Document.querySelectorAll: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass Document.querySelector: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass Document.querySelectorAll: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass Document.querySelector: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass Document.querySelectorAll: Universal selector, matching all descendants of element with specified ID: #universal * +Pass Document.querySelector: Universal selector, matching all descendants of element with specified ID: #universal * +Pass Document.querySelectorAll: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass Document.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass Document.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Pass Document.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Fail Document.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Fail Document.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Document.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass Document.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass Document.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass Document.querySelector: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass Document.querySelectorAll: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass Document.querySelector: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass Document.querySelectorAll: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass Document.querySelector: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass Document.querySelectorAll: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass Document.querySelector: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass Document.querySelectorAll: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass Document.querySelector: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass Document.querySelectorAll: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass Document.querySelector: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass Document.querySelectorAll: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass Document.querySelector: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass Document.querySelectorAll: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass Document.querySelector: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass Document.querySelectorAll: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass Document.querySelector: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass Document.querySelectorAll: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass Document.querySelector: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass Document.querySelectorAll: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass Document.querySelector: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass Document.querySelectorAll: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass Document.querySelector: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass Document.querySelectorAll: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass Document.querySelector: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass Document.querySelectorAll: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass Document.querySelector: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass Document.querySelectorAll: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass Document.querySelector: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass Document.querySelectorAll: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass Document.querySelector: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass Document.querySelector: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass Document.querySelector: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass Document.querySelector: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass Document.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass Document.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass Document.querySelector: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass Document.querySelector: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass Document.querySelector: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass Document.querySelector: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass Document.querySelectorAll: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass Document.querySelector: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass Document.querySelectorAll: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass Document.querySelector: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass Document.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass Document.querySelector: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass Document.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass Document.querySelector: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass Document.querySelectorAll: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass Document.querySelector: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass Document.querySelectorAll: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass Document.querySelector: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass Document.querySelectorAll: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass Document.querySelector: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass Document.querySelectorAll: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass Document.querySelector: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass Document.querySelectorAll: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass Document.querySelector: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass Document.querySelectorAll: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass Document.querySelector: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass Document.querySelectorAll: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass Document.querySelector: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass Document.querySelectorAll: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass Document.querySelector: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass Document.querySelectorAll: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass Document.querySelector: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass Document.querySelectorAll: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass Document.querySelector: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass Document.querySelectorAll: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass Document.querySelector: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass Document.querySelectorAll: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass Document.querySelector: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass Document.querySelectorAll: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass Document.querySelector: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass Document.querySelectorAll: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass Document.querySelector: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass Document.querySelectorAll: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass Document.querySelector: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass Document.querySelectorAll: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass Document.querySelector: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass Document.querySelectorAll: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass Document.querySelector: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass Document.querySelectorAll: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass Document.querySelector: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass Document.querySelectorAll: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass Document.querySelector: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass Document.querySelectorAll: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass Document.querySelector: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass Document.querySelectorAll: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass Document.querySelector: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass Document.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass Document.querySelector: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass Document.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass Document.querySelector: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass Document.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass Document.querySelector: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass Document.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass Document.querySelector: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass Document.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass Document.querySelector: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass Document.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass Document.querySelector: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass Document.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass Document.querySelector: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass Document.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass Document.querySelector: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass Document.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass Document.querySelector: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass Document.querySelectorAll: :root pseudo-class selector, matching document root element: :root +Pass Document.querySelector: :root pseudo-class selector, matching document root element: :root +Pass Document.querySelectorAll: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass Document.querySelector: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass Document.querySelectorAll: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass Document.querySelector: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass Document.querySelectorAll: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass Document.querySelector: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass Document.querySelectorAll: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass Document.querySelector: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass Document.querySelectorAll: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass Document.querySelector: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass Document.querySelectorAll: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass Document.querySelector: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass Document.querySelectorAll: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass Document.querySelector: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass Document.querySelectorAll: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass Document.querySelector: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass Document.querySelectorAll: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass Document.querySelector: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass Document.querySelectorAll: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass Document.querySelector: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass Document.querySelectorAll: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass Document.querySelector: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass Document.querySelectorAll: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass Document.querySelector: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass Document.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass Document.querySelector: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass Document.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass Document.querySelector: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass Document.querySelectorAll: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass Document.querySelector: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass Document.querySelectorAll: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass Document.querySelector: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass Document.querySelectorAll: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass Document.querySelector: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass Document.querySelectorAll: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass Document.querySelector: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass Document.querySelectorAll: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass Document.querySelector: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass Document.querySelectorAll: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass Document.querySelector: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass Document.querySelectorAll: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass Document.querySelector: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass Document.querySelectorAll: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass Document.querySelector: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass Document.querySelectorAll: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass Document.querySelector: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass Document.querySelectorAll: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass Document.querySelector: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass Document.querySelectorAll: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass Document.querySelector: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass Document.querySelectorAll: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass Document.querySelector: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass Document.querySelectorAll: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass Document.querySelector: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass Document.querySelectorAll: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass Document.querySelector: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass Document.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass Document.querySelector: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass Document.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass Document.querySelector: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass Document.querySelectorAll: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass Document.querySelector: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass Document.querySelectorAll: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass Document.querySelector: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass Document.querySelectorAll: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass Document.querySelector: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass Document.querySelectorAll: :link and :visited pseudo-class selectors, matching no elements: #head :link, #head :visited +Pass Document.querySelector: :link and :visited pseudo-class selectors, matching no elements: #head :link, #head :visited +Fail Document.querySelectorAll: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Fail Document.querySelector: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Pass Document.querySelectorAll: :lang pseudo-class selector, matching inherited language: #pseudo-lang-div1:lang(en) +Pass Document.querySelector: :lang pseudo-class selector, matching inherited language: #pseudo-lang-div1:lang(en) +Pass Document.querySelectorAll: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass Document.querySelector: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass Document.querySelectorAll: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass Document.querySelector: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass Document.querySelectorAll: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass Document.querySelector: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass Document.querySelectorAll: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass Document.querySelector: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass Document.querySelectorAll: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass Document.querySelector: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass Document.querySelectorAll: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass Document.querySelector: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass Document.querySelectorAll: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass Document.querySelector: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass Document.querySelectorAll: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass Document.querySelector: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass Document.querySelectorAll: :not pseudo-class selector, matching : #not>:not(div) +Pass Document.querySelector: :not pseudo-class selector, matching : #not>:not(div) +Pass Document.querySelectorAll: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass Document.querySelector: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass Document.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*) +Pass Document.querySelector: :not pseudo-class selector, matching nothing: :not(*) +Pass Document.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*|*) +Pass Document.querySelector: :not pseudo-class selector, matching nothing: :not(*|*) +Pass Document.querySelectorAll: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass Document.querySelector: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass Document.querySelectorAll: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass Document.querySelector: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass Document.querySelectorAll: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass Document.querySelector: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass Document.querySelectorAll: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass Document.querySelector: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass Document.querySelectorAll: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass Document.querySelector: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass Document.querySelectorAll: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass Document.querySelector: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass Document.querySelectorAll: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass Document.querySelector: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass Document.querySelectorAll: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass Document.querySelector: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass Document.querySelectorAll: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass Document.querySelector: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass Document.querySelectorAll: Class selector, matching element with specified class: .class-p +Pass Document.querySelector: Class selector, matching element with specified class: .class-p +Pass Document.querySelectorAll: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass Document.querySelector: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass Document.querySelectorAll: Class Selector, chained, with type selector: div.apple.banana.orange +Pass Document.querySelector: Class Selector, chained, with type selector: div.apple.banana.orange +Pass Document.querySelectorAll: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass Document.querySelector: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass Document.querySelectorAll: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass Document.querySelector: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass Document.querySelectorAll: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass Document.querySelector: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass Document.querySelectorAll: Class selector, matching element with class with escaped character: .foo\:bar +Pass Document.querySelector: Class selector, matching element with class with escaped character: .foo\:bar +Pass Document.querySelectorAll: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass Document.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass Document.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 +Pass Document.querySelector: ID selector, matching element with specified id: #id #id-div1 +Fail Document.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Document.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Document.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass Document.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass Document.querySelectorAll: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass Document.querySelector: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass Document.querySelectorAll: ID selector, not matching non-existent descendant: #id #none +Pass Document.querySelector: ID selector, not matching non-existent descendant: #id #none +Pass Document.querySelectorAll: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass Document.querySelector: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass Document.querySelectorAll: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass Document.querySelector: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass Document.querySelectorAll: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass Document.querySelector: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass Document.querySelectorAll: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass Document.querySelector: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass Document.querySelectorAll: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass Document.querySelector: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass Document.querySelectorAll: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass Document.querySelector: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass Document.querySelectorAll: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass Document.querySelector: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass Document.querySelectorAll: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass Document.querySelector: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass Document.querySelectorAll: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass Document.querySelector: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass Document.querySelectorAll: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass Document.querySelector: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass Document.querySelectorAll: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass Document.querySelector: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass Document.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element: body #descendant-div1 +Pass Document.querySelector: Descendant combinator, matching element with id that is a descendant of an element: body #descendant-div1 +Pass Document.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass Document.querySelector: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass Document.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass Document.querySelector: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass Document.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass Document.querySelector: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass Document.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass Document.querySelector: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass Document.querySelectorAll: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass Document.querySelector: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass Document.querySelectorAll: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass Document.querySelector: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass Document.querySelectorAll: Child combinator, matching element that is a child of an element with id: #child>div +Pass Document.querySelector: Child combinator, matching element that is a child of an element with id: #child>div +Pass Document.querySelectorAll: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass Document.querySelector: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass Document.querySelectorAll: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass Document.querySelector: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass Document.querySelectorAll: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass Document.querySelector: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass Document.querySelectorAll: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass Document.querySelector: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass Document.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass Document.querySelector: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass Document.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass Document.querySelector: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass Document.querySelectorAll: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass Document.querySelector: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass Document.querySelectorAll: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass Document.querySelector: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass Document.querySelectorAll: Child combinator, whitespace after: #child-div1> #child-div2 +Pass Document.querySelector: Child combinator, whitespace after: #child-div1> #child-div2 +Pass Document.querySelectorAll: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass Document.querySelector: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass Document.querySelectorAll: Child combinator, no whitespace: #child-div1>#child-div2 +Pass Document.querySelector: Child combinator, no whitespace: #child-div1>#child-div2 +Pass Document.querySelectorAll: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass Document.querySelector: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass Document.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass Document.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass Document.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass Document.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass Document.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass Document.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass Document.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass Document.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass Document.querySelectorAll: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass Document.querySelector: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass Document.querySelectorAll: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass Document.querySelector: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass Document.querySelectorAll: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass Document.querySelector: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass Document.querySelectorAll: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass Document.querySelector: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass Document.querySelectorAll: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass Document.querySelector: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass Document.querySelectorAll: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass Document.querySelector: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass Document.querySelectorAll: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass Document.querySelector: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass Document.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass Document.querySelector: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass Document.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass Document.querySelector: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass Document.querySelectorAll: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass Document.querySelector: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass Document.querySelectorAll: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass Document.querySelector: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass Document.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass Document.querySelector: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass Document.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass Document.querySelector: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass Document.querySelectorAll: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass Document.querySelector: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass Document.querySelectorAll: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass Document.querySelector: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass Document.querySelectorAll: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass Document.querySelector: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass Document.querySelectorAll: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass Document.querySelector: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass Document.querySelectorAll: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass Document.querySelector: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass Document.querySelectorAll: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass Document.querySelector: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass Document.querySelectorAll: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass Document.querySelector: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass Document.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Pass Document.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Fail Document.querySelectorAll: Slotted selector: ::slotted(foo) Failed to parse selector +Fail Document.querySelector: Slotted selector: ::slotted(foo) Failed to parse selector +Fail Document.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Fail Document.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Pass Detached Element.querySelectorAll: Type selector, matching html element: html +Pass Detached Element.querySelector: Type selector, matching html element: html +Pass Detached Element.querySelectorAll: Type selector, matching body element: body +Pass Detached Element.querySelector: Type selector, matching body element: body +Pass Detached Element.querySelectorAll: Universal selector, matching all children of element with specified ID: #universal>* +Pass Detached Element.querySelector: Universal selector, matching all children of element with specified ID: #universal>* +Pass Detached Element.querySelectorAll: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass Detached Element.querySelector: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass Detached Element.querySelectorAll: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass Detached Element.querySelector: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass Detached Element.querySelectorAll: Universal selector, matching all descendants of element with specified ID: #universal * +Pass Detached Element.querySelector: Universal selector, matching all descendants of element with specified ID: #universal * +Pass Detached Element.querySelectorAll: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass Detached Element.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass Detached Element.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Pass Detached Element.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Fail Detached Element.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Fail Detached Element.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Detached Element.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass Detached Element.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass Detached Element.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass Detached Element.querySelector: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass Detached Element.querySelectorAll: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass Detached Element.querySelector: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass Detached Element.querySelectorAll: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass Detached Element.querySelector: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass Detached Element.querySelectorAll: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass Detached Element.querySelector: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass Detached Element.querySelectorAll: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass Detached Element.querySelector: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass Detached Element.querySelectorAll: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass Detached Element.querySelector: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass Detached Element.querySelectorAll: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass Detached Element.querySelector: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass Detached Element.querySelectorAll: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass Detached Element.querySelector: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass Detached Element.querySelectorAll: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass Detached Element.querySelector: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass Detached Element.querySelectorAll: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass Detached Element.querySelector: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass Detached Element.querySelectorAll: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass Detached Element.querySelector: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass Detached Element.querySelectorAll: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass Detached Element.querySelector: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass Detached Element.querySelectorAll: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass Detached Element.querySelector: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass Detached Element.querySelectorAll: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass Detached Element.querySelector: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass Detached Element.querySelectorAll: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass Detached Element.querySelector: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass Detached Element.querySelectorAll: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass Detached Element.querySelector: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass Detached Element.querySelectorAll: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass Detached Element.querySelector: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass Detached Element.querySelectorAll: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass Detached Element.querySelector: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass Detached Element.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass Detached Element.querySelector: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass Detached Element.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass Detached Element.querySelector: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass Detached Element.querySelectorAll: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass Detached Element.querySelector: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass Detached Element.querySelectorAll: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass Detached Element.querySelector: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass Detached Element.querySelectorAll: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass Detached Element.querySelector: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass Detached Element.querySelectorAll: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass Detached Element.querySelector: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass Detached Element.querySelectorAll: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass Detached Element.querySelector: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass Detached Element.querySelectorAll: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass Detached Element.querySelector: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass Detached Element.querySelectorAll: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass Detached Element.querySelector: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass Detached Element.querySelectorAll: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass Detached Element.querySelector: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass Detached Element.querySelectorAll: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass Detached Element.querySelector: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass Detached Element.querySelectorAll: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass Detached Element.querySelector: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass Detached Element.querySelectorAll: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass Detached Element.querySelector: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass Detached Element.querySelectorAll: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass Detached Element.querySelector: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass Detached Element.querySelectorAll: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass Detached Element.querySelector: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass Detached Element.querySelectorAll: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass Detached Element.querySelector: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass Detached Element.querySelectorAll: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass Detached Element.querySelector: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass Detached Element.querySelectorAll: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass Detached Element.querySelector: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass Detached Element.querySelectorAll: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass Detached Element.querySelector: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass Detached Element.querySelectorAll: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass Detached Element.querySelector: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass Detached Element.querySelectorAll: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass Detached Element.querySelector: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass Detached Element.querySelectorAll: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass Detached Element.querySelector: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass Detached Element.querySelectorAll: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass Detached Element.querySelector: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass Detached Element.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass Detached Element.querySelector: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass Detached Element.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass Detached Element.querySelector: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass Detached Element.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass Detached Element.querySelector: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass Detached Element.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass Detached Element.querySelector: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass Detached Element.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass Detached Element.querySelector: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass Detached Element.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass Detached Element.querySelector: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass Detached Element.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass Detached Element.querySelector: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass Detached Element.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass Detached Element.querySelector: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass Detached Element.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass Detached Element.querySelector: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass Detached Element.querySelectorAll: :root pseudo-class selector, not matching document root element: :root +Pass Detached Element.querySelector: :root pseudo-class selector, not matching document root element: :root +Pass Detached Element.querySelectorAll: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass Detached Element.querySelector: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass Detached Element.querySelectorAll: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass Detached Element.querySelector: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass Detached Element.querySelectorAll: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass Detached Element.querySelector: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass Detached Element.querySelectorAll: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass Detached Element.querySelector: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass Detached Element.querySelectorAll: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass Detached Element.querySelector: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass Detached Element.querySelectorAll: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass Detached Element.querySelector: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass Detached Element.querySelectorAll: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass Detached Element.querySelector: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass Detached Element.querySelectorAll: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass Detached Element.querySelector: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass Detached Element.querySelectorAll: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass Detached Element.querySelector: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass Detached Element.querySelectorAll: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass Detached Element.querySelector: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass Detached Element.querySelectorAll: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass Detached Element.querySelector: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass Detached Element.querySelectorAll: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass Detached Element.querySelector: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass Detached Element.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass Detached Element.querySelector: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass Detached Element.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass Detached Element.querySelector: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass Detached Element.querySelectorAll: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass Detached Element.querySelector: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass Detached Element.querySelectorAll: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass Detached Element.querySelector: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass Detached Element.querySelectorAll: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass Detached Element.querySelector: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass Detached Element.querySelectorAll: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass Detached Element.querySelector: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass Detached Element.querySelectorAll: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass Detached Element.querySelector: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass Detached Element.querySelectorAll: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass Detached Element.querySelector: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass Detached Element.querySelectorAll: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass Detached Element.querySelector: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass Detached Element.querySelectorAll: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass Detached Element.querySelector: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass Detached Element.querySelectorAll: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass Detached Element.querySelector: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass Detached Element.querySelectorAll: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass Detached Element.querySelector: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass Detached Element.querySelectorAll: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass Detached Element.querySelector: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass Detached Element.querySelectorAll: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass Detached Element.querySelector: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass Detached Element.querySelectorAll: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass Detached Element.querySelector: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass Detached Element.querySelectorAll: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass Detached Element.querySelector: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass Detached Element.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass Detached Element.querySelector: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass Detached Element.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass Detached Element.querySelector: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass Detached Element.querySelectorAll: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass Detached Element.querySelector: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass Detached Element.querySelectorAll: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass Detached Element.querySelector: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass Detached Element.querySelectorAll: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass Detached Element.querySelector: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass Detached Element.querySelectorAll: :link and :visited pseudo-class selectors, not matching link elements with href attributes: #head :link, #head :visited +Pass Detached Element.querySelector: :link and :visited pseudo-class selectors, not matching link elements with href attributes: #head :link, #head :visited +Pass Detached Element.querySelectorAll: :link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing: :link:visited +Pass Detached Element.querySelector: :link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing: :link:visited +Pass Detached Element.querySelectorAll: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Pass Detached Element.querySelector: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Pass Detached Element.querySelectorAll: :lang pseudo-class selector, not matching element with no inherited language: #pseudo-lang-div1:lang(en) +Pass Detached Element.querySelector: :lang pseudo-class selector, not matching element with no inherited language: #pseudo-lang-div1:lang(en) +Pass Detached Element.querySelectorAll: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass Detached Element.querySelector: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass Detached Element.querySelectorAll: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass Detached Element.querySelector: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass Detached Element.querySelectorAll: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass Detached Element.querySelector: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass Detached Element.querySelectorAll: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass Detached Element.querySelector: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass Detached Element.querySelectorAll: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass Detached Element.querySelector: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass Detached Element.querySelectorAll: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass Detached Element.querySelector: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass Detached Element.querySelectorAll: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass Detached Element.querySelector: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass Detached Element.querySelectorAll: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass Detached Element.querySelector: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass Detached Element.querySelectorAll: :not pseudo-class selector, matching : #not>:not(div) +Pass Detached Element.querySelector: :not pseudo-class selector, matching : #not>:not(div) +Pass Detached Element.querySelectorAll: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass Detached Element.querySelector: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass Detached Element.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*) +Pass Detached Element.querySelector: :not pseudo-class selector, matching nothing: :not(*) +Pass Detached Element.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*|*) +Pass Detached Element.querySelector: :not pseudo-class selector, matching nothing: :not(*|*) +Pass Detached Element.querySelectorAll: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass Detached Element.querySelector: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass Detached Element.querySelectorAll: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass Detached Element.querySelector: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass Detached Element.querySelectorAll: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass Detached Element.querySelector: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass Detached Element.querySelectorAll: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass Detached Element.querySelector: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass Detached Element.querySelectorAll: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass Detached Element.querySelector: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass Detached Element.querySelectorAll: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass Detached Element.querySelector: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass Detached Element.querySelectorAll: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass Detached Element.querySelector: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass Detached Element.querySelectorAll: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass Detached Element.querySelector: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass Detached Element.querySelectorAll: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass Detached Element.querySelector: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass Detached Element.querySelectorAll: Class selector, matching element with specified class: .class-p +Pass Detached Element.querySelector: Class selector, matching element with specified class: .class-p +Pass Detached Element.querySelectorAll: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass Detached Element.querySelector: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass Detached Element.querySelectorAll: Class Selector, chained, with type selector: div.apple.banana.orange +Pass Detached Element.querySelector: Class Selector, chained, with type selector: div.apple.banana.orange +Pass Detached Element.querySelectorAll: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass Detached Element.querySelector: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass Detached Element.querySelectorAll: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass Detached Element.querySelector: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass Detached Element.querySelectorAll: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass Detached Element.querySelector: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass Detached Element.querySelectorAll: Class selector, matching element with class with escaped character: .foo\:bar +Pass Detached Element.querySelector: Class selector, matching element with class with escaped character: .foo\:bar +Pass Detached Element.querySelectorAll: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass Detached Element.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass Detached Element.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 +Pass Detached Element.querySelector: ID selector, matching element with specified id: #id #id-div1 +Fail Detached Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Detached Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Detached Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass Detached Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass Detached Element.querySelectorAll: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass Detached Element.querySelector: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass Detached Element.querySelectorAll: ID selector, not matching non-existent descendant: #id #none +Pass Detached Element.querySelector: ID selector, not matching non-existent descendant: #id #none +Pass Detached Element.querySelectorAll: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass Detached Element.querySelector: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass Detached Element.querySelectorAll: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass Detached Element.querySelector: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass Detached Element.querySelectorAll: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass Detached Element.querySelector: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass Detached Element.querySelectorAll: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass Detached Element.querySelector: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass Detached Element.querySelectorAll: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass Detached Element.querySelector: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass Detached Element.querySelectorAll: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass Detached Element.querySelector: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass Detached Element.querySelectorAll: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass Detached Element.querySelector: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass Detached Element.querySelectorAll: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass Detached Element.querySelector: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass Detached Element.querySelectorAll: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass Detached Element.querySelector: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass Detached Element.querySelectorAll: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass Detached Element.querySelector: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass Detached Element.querySelectorAll: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass Detached Element.querySelector: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass Detached Element.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass Detached Element.querySelector: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass Detached Element.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass Detached Element.querySelector: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass Detached Element.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass Detached Element.querySelector: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass Detached Element.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass Detached Element.querySelector: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass Detached Element.querySelectorAll: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass Detached Element.querySelector: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass Detached Element.querySelectorAll: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass Detached Element.querySelector: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass Detached Element.querySelectorAll: Child combinator, matching element that is a child of an element with id: #child>div +Pass Detached Element.querySelector: Child combinator, matching element that is a child of an element with id: #child>div +Pass Detached Element.querySelectorAll: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass Detached Element.querySelector: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass Detached Element.querySelectorAll: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass Detached Element.querySelector: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass Detached Element.querySelectorAll: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass Detached Element.querySelector: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass Detached Element.querySelectorAll: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass Detached Element.querySelector: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass Detached Element.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass Detached Element.querySelector: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass Detached Element.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass Detached Element.querySelector: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass Detached Element.querySelectorAll: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass Detached Element.querySelector: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass Detached Element.querySelectorAll: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass Detached Element.querySelector: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass Detached Element.querySelectorAll: Child combinator, whitespace after: #child-div1> #child-div2 +Pass Detached Element.querySelector: Child combinator, whitespace after: #child-div1> #child-div2 +Pass Detached Element.querySelectorAll: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass Detached Element.querySelector: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass Detached Element.querySelectorAll: Child combinator, no whitespace: #child-div1>#child-div2 +Pass Detached Element.querySelector: Child combinator, no whitespace: #child-div1>#child-div2 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass Detached Element.querySelector: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass Detached Element.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass Detached Element.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass Detached Element.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass Detached Element.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass Detached Element.querySelector: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass Detached Element.querySelector: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass Detached Element.querySelector: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass Detached Element.querySelector: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass Detached Element.querySelector: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass Detached Element.querySelectorAll: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass Detached Element.querySelector: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass Detached Element.querySelectorAll: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass Detached Element.querySelector: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass Detached Element.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass Detached Element.querySelector: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass Detached Element.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass Detached Element.querySelector: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass Detached Element.querySelectorAll: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass Detached Element.querySelector: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass Detached Element.querySelectorAll: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass Detached Element.querySelector: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass Detached Element.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass Detached Element.querySelector: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass Detached Element.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass Detached Element.querySelector: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass Detached Element.querySelectorAll: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass Detached Element.querySelector: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass Detached Element.querySelectorAll: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass Detached Element.querySelector: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass Detached Element.querySelectorAll: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass Detached Element.querySelector: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass Detached Element.querySelectorAll: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass Detached Element.querySelector: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass Detached Element.querySelectorAll: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass Detached Element.querySelector: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass Detached Element.querySelectorAll: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass Detached Element.querySelector: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass Detached Element.querySelectorAll: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass Detached Element.querySelector: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass Detached Element.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Pass Detached Element.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Fail Detached Element.querySelectorAll: Slotted selector: ::slotted(foo) Failed to parse selector +Fail Detached Element.querySelector: Slotted selector: ::slotted(foo) Failed to parse selector +Fail Detached Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Fail Detached Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Pass Fragment.querySelectorAll: Type selector, matching html element: html +Pass Fragment.querySelector: Type selector, matching html element: html +Pass Fragment.querySelectorAll: Type selector, matching body element: body +Pass Fragment.querySelector: Type selector, matching body element: body +Pass Fragment.querySelectorAll: Universal selector, matching all children of element with specified ID: #universal>* +Pass Fragment.querySelector: Universal selector, matching all children of element with specified ID: #universal>* +Pass Fragment.querySelectorAll: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass Fragment.querySelector: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass Fragment.querySelectorAll: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass Fragment.querySelector: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass Fragment.querySelectorAll: Universal selector, matching all descendants of element with specified ID: #universal * +Pass Fragment.querySelector: Universal selector, matching all descendants of element with specified ID: #universal * +Pass Fragment.querySelectorAll: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass Fragment.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass Fragment.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Pass Fragment.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Fail Fragment.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Fail Fragment.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Fragment.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass Fragment.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass Fragment.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass Fragment.querySelector: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass Fragment.querySelectorAll: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass Fragment.querySelector: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass Fragment.querySelectorAll: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass Fragment.querySelector: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass Fragment.querySelectorAll: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass Fragment.querySelector: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass Fragment.querySelectorAll: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass Fragment.querySelector: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass Fragment.querySelectorAll: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass Fragment.querySelector: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass Fragment.querySelectorAll: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass Fragment.querySelector: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass Fragment.querySelectorAll: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass Fragment.querySelector: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass Fragment.querySelectorAll: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass Fragment.querySelector: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass Fragment.querySelectorAll: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass Fragment.querySelector: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass Fragment.querySelectorAll: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass Fragment.querySelector: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass Fragment.querySelectorAll: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass Fragment.querySelector: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass Fragment.querySelectorAll: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass Fragment.querySelector: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass Fragment.querySelectorAll: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass Fragment.querySelector: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass Fragment.querySelectorAll: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass Fragment.querySelector: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass Fragment.querySelectorAll: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass Fragment.querySelector: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass Fragment.querySelector: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass Fragment.querySelector: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass Fragment.querySelector: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass Fragment.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass Fragment.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass Fragment.querySelector: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass Fragment.querySelector: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass Fragment.querySelector: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass Fragment.querySelector: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass Fragment.querySelectorAll: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass Fragment.querySelector: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass Fragment.querySelectorAll: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass Fragment.querySelector: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass Fragment.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass Fragment.querySelector: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass Fragment.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass Fragment.querySelector: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass Fragment.querySelectorAll: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass Fragment.querySelector: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass Fragment.querySelectorAll: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass Fragment.querySelector: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass Fragment.querySelectorAll: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass Fragment.querySelector: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass Fragment.querySelectorAll: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass Fragment.querySelector: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass Fragment.querySelectorAll: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass Fragment.querySelector: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass Fragment.querySelectorAll: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass Fragment.querySelector: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass Fragment.querySelectorAll: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass Fragment.querySelector: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass Fragment.querySelectorAll: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass Fragment.querySelector: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass Fragment.querySelectorAll: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass Fragment.querySelector: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass Fragment.querySelectorAll: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass Fragment.querySelector: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass Fragment.querySelectorAll: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass Fragment.querySelector: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass Fragment.querySelectorAll: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass Fragment.querySelector: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass Fragment.querySelectorAll: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass Fragment.querySelector: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass Fragment.querySelectorAll: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass Fragment.querySelector: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass Fragment.querySelectorAll: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass Fragment.querySelector: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass Fragment.querySelectorAll: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass Fragment.querySelector: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass Fragment.querySelectorAll: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass Fragment.querySelector: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass Fragment.querySelectorAll: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass Fragment.querySelector: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass Fragment.querySelectorAll: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass Fragment.querySelector: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass Fragment.querySelectorAll: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass Fragment.querySelector: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass Fragment.querySelectorAll: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass Fragment.querySelector: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass Fragment.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass Fragment.querySelector: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass Fragment.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass Fragment.querySelector: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass Fragment.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass Fragment.querySelector: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass Fragment.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass Fragment.querySelector: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass Fragment.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass Fragment.querySelector: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass Fragment.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass Fragment.querySelector: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass Fragment.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass Fragment.querySelector: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass Fragment.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass Fragment.querySelector: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass Fragment.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass Fragment.querySelector: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass Fragment.querySelectorAll: :root pseudo-class selector, not matching document root element: :root +Pass Fragment.querySelector: :root pseudo-class selector, not matching document root element: :root +Pass Fragment.querySelectorAll: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass Fragment.querySelector: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass Fragment.querySelectorAll: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass Fragment.querySelector: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass Fragment.querySelectorAll: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass Fragment.querySelector: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass Fragment.querySelectorAll: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass Fragment.querySelector: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass Fragment.querySelectorAll: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass Fragment.querySelector: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass Fragment.querySelectorAll: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass Fragment.querySelector: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass Fragment.querySelectorAll: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass Fragment.querySelector: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass Fragment.querySelectorAll: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass Fragment.querySelector: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass Fragment.querySelectorAll: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass Fragment.querySelector: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass Fragment.querySelectorAll: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass Fragment.querySelector: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass Fragment.querySelectorAll: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass Fragment.querySelector: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass Fragment.querySelectorAll: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass Fragment.querySelector: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass Fragment.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass Fragment.querySelector: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass Fragment.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass Fragment.querySelector: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass Fragment.querySelectorAll: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass Fragment.querySelector: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass Fragment.querySelectorAll: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass Fragment.querySelector: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass Fragment.querySelectorAll: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass Fragment.querySelector: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass Fragment.querySelectorAll: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass Fragment.querySelector: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass Fragment.querySelectorAll: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass Fragment.querySelector: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass Fragment.querySelectorAll: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass Fragment.querySelector: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass Fragment.querySelectorAll: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass Fragment.querySelector: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass Fragment.querySelectorAll: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass Fragment.querySelector: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass Fragment.querySelectorAll: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass Fragment.querySelector: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass Fragment.querySelectorAll: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass Fragment.querySelector: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass Fragment.querySelectorAll: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass Fragment.querySelector: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass Fragment.querySelectorAll: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass Fragment.querySelector: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass Fragment.querySelectorAll: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass Fragment.querySelector: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass Fragment.querySelectorAll: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass Fragment.querySelector: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass Fragment.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass Fragment.querySelector: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass Fragment.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass Fragment.querySelector: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass Fragment.querySelectorAll: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass Fragment.querySelector: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass Fragment.querySelectorAll: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass Fragment.querySelector: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass Fragment.querySelectorAll: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass Fragment.querySelector: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass Fragment.querySelectorAll: :link and :visited pseudo-class selectors, not matching link elements with href attributes: #head :link, #head :visited +Pass Fragment.querySelector: :link and :visited pseudo-class selectors, not matching link elements with href attributes: #head :link, #head :visited +Pass Fragment.querySelectorAll: :link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing: :link:visited +Pass Fragment.querySelector: :link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing: :link:visited +Pass Fragment.querySelectorAll: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Pass Fragment.querySelector: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Pass Fragment.querySelectorAll: :lang pseudo-class selector, not matching element with no inherited language: #pseudo-lang-div1:lang(en) +Pass Fragment.querySelector: :lang pseudo-class selector, not matching element with no inherited language: #pseudo-lang-div1:lang(en) +Pass Fragment.querySelectorAll: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass Fragment.querySelector: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass Fragment.querySelectorAll: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass Fragment.querySelector: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass Fragment.querySelectorAll: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass Fragment.querySelector: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass Fragment.querySelectorAll: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass Fragment.querySelector: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass Fragment.querySelectorAll: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass Fragment.querySelector: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass Fragment.querySelectorAll: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass Fragment.querySelector: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass Fragment.querySelectorAll: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass Fragment.querySelector: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass Fragment.querySelectorAll: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass Fragment.querySelector: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass Fragment.querySelectorAll: :not pseudo-class selector, matching : #not>:not(div) +Pass Fragment.querySelector: :not pseudo-class selector, matching : #not>:not(div) +Pass Fragment.querySelectorAll: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass Fragment.querySelector: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass Fragment.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*) +Pass Fragment.querySelector: :not pseudo-class selector, matching nothing: :not(*) +Pass Fragment.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*|*) +Pass Fragment.querySelector: :not pseudo-class selector, matching nothing: :not(*|*) +Pass Fragment.querySelectorAll: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass Fragment.querySelector: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass Fragment.querySelectorAll: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass Fragment.querySelector: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass Fragment.querySelectorAll: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass Fragment.querySelector: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass Fragment.querySelectorAll: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass Fragment.querySelector: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass Fragment.querySelectorAll: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass Fragment.querySelector: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass Fragment.querySelectorAll: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass Fragment.querySelector: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass Fragment.querySelectorAll: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass Fragment.querySelector: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass Fragment.querySelectorAll: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass Fragment.querySelector: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass Fragment.querySelectorAll: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass Fragment.querySelector: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass Fragment.querySelectorAll: Class selector, matching element with specified class: .class-p +Pass Fragment.querySelector: Class selector, matching element with specified class: .class-p +Pass Fragment.querySelectorAll: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass Fragment.querySelector: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass Fragment.querySelectorAll: Class Selector, chained, with type selector: div.apple.banana.orange +Pass Fragment.querySelector: Class Selector, chained, with type selector: div.apple.banana.orange +Pass Fragment.querySelectorAll: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass Fragment.querySelector: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass Fragment.querySelectorAll: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass Fragment.querySelector: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass Fragment.querySelectorAll: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass Fragment.querySelector: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass Fragment.querySelectorAll: Class selector, matching element with class with escaped character: .foo\:bar +Pass Fragment.querySelector: Class selector, matching element with class with escaped character: .foo\:bar +Pass Fragment.querySelectorAll: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass Fragment.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass Fragment.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 +Pass Fragment.querySelector: ID selector, matching element with specified id: #id #id-div1 +Fail Fragment.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Fragment.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Fragment.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass Fragment.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass Fragment.querySelectorAll: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass Fragment.querySelector: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass Fragment.querySelectorAll: ID selector, not matching non-existent descendant: #id #none +Pass Fragment.querySelector: ID selector, not matching non-existent descendant: #id #none +Pass Fragment.querySelectorAll: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass Fragment.querySelector: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass Fragment.querySelectorAll: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass Fragment.querySelector: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass Fragment.querySelectorAll: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass Fragment.querySelector: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass Fragment.querySelectorAll: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass Fragment.querySelector: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass Fragment.querySelectorAll: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass Fragment.querySelector: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass Fragment.querySelectorAll: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass Fragment.querySelector: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass Fragment.querySelectorAll: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass Fragment.querySelector: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass Fragment.querySelectorAll: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass Fragment.querySelector: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass Fragment.querySelectorAll: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass Fragment.querySelector: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass Fragment.querySelectorAll: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass Fragment.querySelector: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass Fragment.querySelectorAll: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass Fragment.querySelector: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass Fragment.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass Fragment.querySelector: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass Fragment.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass Fragment.querySelector: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass Fragment.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass Fragment.querySelector: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass Fragment.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass Fragment.querySelector: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass Fragment.querySelectorAll: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass Fragment.querySelector: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass Fragment.querySelectorAll: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass Fragment.querySelector: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass Fragment.querySelectorAll: Child combinator, matching element that is a child of an element with id: #child>div +Pass Fragment.querySelector: Child combinator, matching element that is a child of an element with id: #child>div +Pass Fragment.querySelectorAll: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass Fragment.querySelector: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass Fragment.querySelectorAll: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass Fragment.querySelector: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass Fragment.querySelectorAll: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass Fragment.querySelector: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass Fragment.querySelectorAll: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass Fragment.querySelector: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass Fragment.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass Fragment.querySelector: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass Fragment.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass Fragment.querySelector: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass Fragment.querySelectorAll: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass Fragment.querySelector: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass Fragment.querySelectorAll: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass Fragment.querySelector: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass Fragment.querySelectorAll: Child combinator, whitespace after: #child-div1> #child-div2 +Pass Fragment.querySelector: Child combinator, whitespace after: #child-div1> #child-div2 +Pass Fragment.querySelectorAll: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass Fragment.querySelector: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass Fragment.querySelectorAll: Child combinator, no whitespace: #child-div1>#child-div2 +Pass Fragment.querySelector: Child combinator, no whitespace: #child-div1>#child-div2 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass Fragment.querySelector: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass Fragment.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass Fragment.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass Fragment.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass Fragment.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass Fragment.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass Fragment.querySelector: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass Fragment.querySelectorAll: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass Fragment.querySelector: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass Fragment.querySelector: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass Fragment.querySelector: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass Fragment.querySelector: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass Fragment.querySelectorAll: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass Fragment.querySelector: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass Fragment.querySelectorAll: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass Fragment.querySelector: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass Fragment.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass Fragment.querySelector: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass Fragment.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass Fragment.querySelector: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass Fragment.querySelectorAll: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass Fragment.querySelector: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass Fragment.querySelectorAll: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass Fragment.querySelector: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass Fragment.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass Fragment.querySelector: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass Fragment.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass Fragment.querySelector: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass Fragment.querySelectorAll: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass Fragment.querySelector: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass Fragment.querySelectorAll: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass Fragment.querySelector: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass Fragment.querySelectorAll: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass Fragment.querySelector: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass Fragment.querySelectorAll: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass Fragment.querySelector: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass Fragment.querySelectorAll: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass Fragment.querySelector: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass Fragment.querySelectorAll: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass Fragment.querySelector: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass Fragment.querySelectorAll: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass Fragment.querySelector: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass Fragment.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Pass Fragment.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Fail Fragment.querySelectorAll: Slotted selector: ::slotted(foo) Failed to parse selector +Fail Fragment.querySelector: Slotted selector: ::slotted(foo) Failed to parse selector +Fail Fragment.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Fail Fragment.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Pass In-document Element.querySelectorAll: Type selector, matching html element: html +Pass In-document Element.querySelector: Type selector, matching html element: html +Pass In-document Element.querySelectorAll: Type selector, matching body element: body +Pass In-document Element.querySelector: Type selector, matching body element: body +Pass In-document Element.querySelectorAll: Universal selector, matching all children of element with specified ID: #universal>* +Pass In-document Element.querySelector: Universal selector, matching all children of element with specified ID: #universal>* +Pass In-document Element.querySelectorAll: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass In-document Element.querySelector: Universal selector, matching all grandchildren of element with specified ID: #universal>*>* +Pass In-document Element.querySelectorAll: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass In-document Element.querySelector: Universal selector, matching all children of empty element with specified ID: #empty>* +Pass In-document Element.querySelectorAll: Universal selector, matching all descendants of element with specified ID: #universal * +Pass In-document Element.querySelector: Universal selector, matching all descendants of element with specified ID: #universal * +Pass In-document Element.querySelectorAll: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass In-document Element.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] +Pass In-document Element.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Pass In-document Element.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] +Fail In-document Element.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Fail In-document Element.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass In-document Element.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass In-document Element.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] +Pass In-document Element.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass In-document Element.querySelector: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] +Pass In-document Element.querySelectorAll: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass In-document Element.querySelector: Attribute presence selector, matching attribute with non-ASCII characters: ul[data-中文] +Pass In-document Element.querySelectorAll: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass In-document Element.querySelector: Attribute presence selector, not matching default option without selected attribute: #attr-presence-select1 option[selected] +Pass In-document Element.querySelectorAll: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass In-document Element.querySelector: Attribute presence selector, matching option with selected attribute: #attr-presence-select2 option[selected] +Pass In-document Element.querySelectorAll: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass In-document Element.querySelector: Attribute presence selector, matching multiple options with selected attributes: #attr-presence-select3 option[selected] +Pass In-document Element.querySelectorAll: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass In-document Element.querySelector: Attribute value selector, matching align attribute with value: #attr-value [align="center"] +Pass In-document Element.querySelectorAll: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass In-document Element.querySelector: Attribute value selector, matching align attribute with value, unclosed bracket: #attr-value [align="center" +Pass In-document Element.querySelectorAll: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass In-document Element.querySelector: Attribute value selector, matching align attribute with empty value: #attr-value [align=""] +Pass In-document Element.querySelectorAll: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass In-document Element.querySelector: Attribute value selector, not matching align attribute with partial value: #attr-value [align="c"] +Pass In-document Element.querySelectorAll: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass In-document Element.querySelector: Attribute value selector, not matching align attribute with incorrect value: #attr-value [align="centera"] +Pass In-document Element.querySelectorAll: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass In-document Element.querySelector: Attribute value selector, matching custom data-* attribute with unicode escaped value: [data-attr-value="\e9"] +Pass In-document Element.querySelectorAll: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass In-document Element.querySelector: Attribute value selector, matching custom data-* attribute with escaped character: [data-attr-value_foo="\e9"] +Pass In-document Element.querySelectorAll: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass In-document Element.querySelector: Attribute value selector with single-quoted value, matching multiple inputs with type attributes: #attr-value input[type='hidden'],#attr-value input[type='radio'] +Pass In-document Element.querySelectorAll: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass In-document Element.querySelector: Attribute value selector with double-quoted value, matching multiple inputs with type attributes: #attr-value input[type="hidden"],#attr-value input[type='radio'] +Pass In-document Element.querySelectorAll: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass In-document Element.querySelector: Attribute value selector with unquoted value, matching multiple inputs with type attributes: #attr-value input[type=hidden],#attr-value input[type=radio] +Pass In-document Element.querySelectorAll: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass In-document Element.querySelector: Attribute value selector, matching attribute with value using non-ASCII characters: [data-attr-value=中文] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector, matching class attribute with value: #attr-whitespace [class~="div1"] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector, not matching class attribute with empty value: #attr-whitespace [class~=""] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector, not matching class attribute with partial value: [data-attr-whitespace~="div"] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value: [data-attr-whitespace~="\0000e9"] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character: [data-attr-whitespace_foo~="\e9"] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow'] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~="bookmark"],#attr-whitespace a[rel~='nofollow'] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes: #attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector with double-quoted value, not matching value with space: #attr-whitespace a[rel~="book mark"] +Pass In-document Element.querySelectorAll: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass In-document Element.querySelector: Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters: #attr-whitespace [title~=中文] +Pass In-document Element.querySelectorAll: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass In-document Element.querySelector: Attribute hyphen-separated list selector, not matching unspecified lang attribute: #attr-hyphen-div1[lang|="en"] +Pass In-document Element.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass In-document Element.querySelector: Attribute hyphen-separated list selector, matching lang attribute with exact value: #attr-hyphen-div2[lang|="fr"] +Pass In-document Element.querySelectorAll: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass In-document Element.querySelector: Attribute hyphen-separated list selector, matching lang attribute with partial value: #attr-hyphen-div3[lang|="en"] +Pass In-document Element.querySelectorAll: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass In-document Element.querySelector: Attribute hyphen-separated list selector, not matching incorrect value: #attr-hyphen-div4[lang|="es-AR"] +Pass In-document Element.querySelectorAll: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass In-document Element.querySelector: Attribute begins with selector, matching href attributes beginning with specified substring: #attr-begins a[href^="http://www"] +Pass In-document Element.querySelectorAll: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass In-document Element.querySelector: Attribute begins with selector, matching lang attributes beginning with specified substring, : #attr-begins [lang^="en-"] +Pass In-document Element.querySelectorAll: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass In-document Element.querySelector: Attribute begins with selector, not matching class attribute with empty value: #attr-begins [class^=""] +Pass In-document Element.querySelectorAll: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass In-document Element.querySelector: Attribute begins with selector, not matching class attribute not beginning with specified substring: #attr-begins [class^=apple] +Pass In-document Element.querySelectorAll: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass In-document Element.querySelector: Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=' apple'] +Pass In-document Element.querySelectorAll: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass In-document Element.querySelector: Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring: #attr-begins [class^=" apple"] +Pass In-document Element.querySelectorAll: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass In-document Element.querySelector: Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring: #attr-begins [class^= apple] +Pass In-document Element.querySelectorAll: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass In-document Element.querySelector: Attribute ends with selector, matching href attributes ending with specified substring: #attr-ends a[href$=".org"] +Pass In-document Element.querySelectorAll: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass In-document Element.querySelector: Attribute ends with selector, matching lang attributes ending with specified substring, : #attr-ends [lang$="-CH"] +Pass In-document Element.querySelectorAll: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass In-document Element.querySelector: Attribute ends with selector, not matching class attribute with empty value: #attr-ends [class$=""] +Pass In-document Element.querySelectorAll: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass In-document Element.querySelector: Attribute ends with selector, not matching class attribute not ending with specified substring: #attr-ends [class$=apple] +Pass In-document Element.querySelectorAll: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass In-document Element.querySelector: Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring: #attr-ends [class$='apple '] +Pass In-document Element.querySelectorAll: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass In-document Element.querySelector: Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring: #attr-ends [class$="apple "] +Pass In-document Element.querySelectorAll: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass In-document Element.querySelector: Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring: #attr-ends [class$=apple ] +Pass In-document Element.querySelectorAll: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass In-document Element.querySelector: Attribute contains selector, matching href attributes beginning with specified substring: #attr-contains a[href*="http://www"] +Pass In-document Element.querySelectorAll: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass In-document Element.querySelector: Attribute contains selector, matching href attributes ending with specified substring: #attr-contains a[href*=".org"] +Pass In-document Element.querySelectorAll: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass In-document Element.querySelector: Attribute contains selector, matching href attributes containing specified substring: #attr-contains a[href*=".example."] +Pass In-document Element.querySelectorAll: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass In-document Element.querySelector: Attribute contains selector, matching lang attributes beginning with specified substring, : #attr-contains [lang*="en-"] +Pass In-document Element.querySelectorAll: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass In-document Element.querySelector: Attribute contains selector, matching lang attributes ending with specified substring, : #attr-contains [lang*="-CH"] +Pass In-document Element.querySelectorAll: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass In-document Element.querySelector: Attribute contains selector, not matching class attribute with empty value: #attr-contains [class*=""] +Pass In-document Element.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass In-document Element.querySelector: Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=' apple'] +Pass In-document Element.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass In-document Element.querySelector: Attribute contains selector with single-quoted value, matching class attribute ending with specified substring: #attr-contains [class*='orange '] +Pass In-document Element.querySelectorAll: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass In-document Element.querySelector: Attribute contains selector with single-quoted value, matching class attribute containing specified substring: #attr-contains [class*='ple banana ora'] +Pass In-document Element.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass In-document Element.querySelector: Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring: #attr-contains [class*=" apple"] +Pass In-document Element.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass In-document Element.querySelector: Attribute contains selector with double-quoted value, matching class attribute ending with specified substring: #attr-contains [class*="orange "] +Pass In-document Element.querySelectorAll: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass In-document Element.querySelector: Attribute contains selector with double-quoted value, matching class attribute containing specified substring: #attr-contains [class*="ple banana ora"] +Pass In-document Element.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass In-document Element.querySelector: Attribute contains selector with unquoted value, matching class attribute beginning with specified substring: #attr-contains [class*= apple] +Pass In-document Element.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass In-document Element.querySelector: Attribute contains selector with unquoted value, matching class attribute ending with specified substring: #attr-contains [class*=orange ] +Pass In-document Element.querySelectorAll: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass In-document Element.querySelector: Attribute contains selector with unquoted value, matching class attribute containing specified substring: #attr-contains [class*= banana ] +Pass In-document Element.querySelectorAll: :root pseudo-class selector, not matching document root element: :root +Pass In-document Element.querySelector: :root pseudo-class selector, not matching document root element: :root +Pass In-document Element.querySelectorAll: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass In-document Element.querySelector: :nth-child selector, matching the third child element: #pseudo-nth-table1 :nth-child(3) +Pass In-document Element.querySelectorAll: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass In-document Element.querySelector: :nth-child selector, matching every third child element: #pseudo-nth li:nth-child(3n) +Pass In-document Element.querySelectorAll: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass In-document Element.querySelector: :nth-child selector, matching every second child element, starting from the fourth: #pseudo-nth li:nth-child(2n+4) +Pass In-document Element.querySelectorAll: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass In-document Element.querySelector: :nth-child selector, matching every fourth child element, starting from the third: #pseudo-nth-p1 :nth-child(4n-1) +Pass In-document Element.querySelectorAll: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass In-document Element.querySelector: :nth-last-child selector, matching the third last child element: #pseudo-nth-table1 :nth-last-child(3) +Pass In-document Element.querySelectorAll: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass In-document Element.querySelector: :nth-last-child selector, matching every third child element from the end: #pseudo-nth li:nth-last-child(3n) +Pass In-document Element.querySelectorAll: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass In-document Element.querySelector: :nth-last-child selector, matching every second child element from the end, starting from the fourth last: #pseudo-nth li:nth-last-child(2n+4) +Pass In-document Element.querySelectorAll: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass In-document Element.querySelector: :nth-last-child selector, matching every fourth element from the end, starting from the third last: #pseudo-nth-p1 :nth-last-child(4n-1) +Pass In-document Element.querySelectorAll: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass In-document Element.querySelector: :nth-of-type selector, matching the third em element: #pseudo-nth-p1 em:nth-of-type(3) +Pass In-document Element.querySelectorAll: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass In-document Element.querySelector: :nth-of-type selector, matching every second element of their type: #pseudo-nth-p1 :nth-of-type(2n) +Pass In-document Element.querySelectorAll: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass In-document Element.querySelector: :nth-of-type selector, matching every second elemetn of their type, starting from the first: #pseudo-nth-p1 span:nth-of-type(2n-1) +Pass In-document Element.querySelectorAll: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass In-document Element.querySelector: :nth-last-of-type selector, matching the third last em element: #pseudo-nth-p1 em:nth-last-of-type(3) +Pass In-document Element.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass In-document Element.querySelector: :nth-last-of-type selector, matching every second last element of their type: #pseudo-nth-p1 :nth-last-of-type(2n) +Pass In-document Element.querySelectorAll: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass In-document Element.querySelector: :nth-last-of-type selector, matching every second last element of their type, starting from the last: #pseudo-nth-p1 span:nth-last-of-type(2n-1) +Pass In-document Element.querySelectorAll: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass In-document Element.querySelector: :first-of-type selector, matching the first em element: #pseudo-nth-p1 em:first-of-type +Pass In-document Element.querySelectorAll: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass In-document Element.querySelector: :first-of-type selector, matching the first of every type of element: #pseudo-nth-p1 :first-of-type +Pass In-document Element.querySelectorAll: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass In-document Element.querySelector: :first-of-type selector, matching the first td element in each table row: #pseudo-nth-table1 tr :first-of-type +Pass In-document Element.querySelectorAll: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass In-document Element.querySelector: :last-of-type selector, matching the last em elemnet: #pseudo-nth-p1 em:last-of-type +Pass In-document Element.querySelectorAll: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass In-document Element.querySelector: :last-of-type selector, matching the last of every type of element: #pseudo-nth-p1 :last-of-type +Pass In-document Element.querySelectorAll: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass In-document Element.querySelector: :last-of-type selector, matching the last td element in each table row: #pseudo-nth-table1 tr :last-of-type +Pass In-document Element.querySelectorAll: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass In-document Element.querySelector: :first-child pseudo-class selector, matching first child div element: #pseudo-first-child div:first-child +Pass In-document Element.querySelectorAll: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass In-document Element.querySelector: :first-child pseudo-class selector, doesn't match non-first-child elements: .pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child +Pass In-document Element.querySelectorAll: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass In-document Element.querySelector: :first-child pseudo-class selector, matching first-child of multiple elements: #pseudo-first-child span:first-child +Pass In-document Element.querySelectorAll: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass In-document Element.querySelector: :last-child pseudo-class selector, matching last child div element: #pseudo-last-child div:last-child +Pass In-document Element.querySelectorAll: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass In-document Element.querySelector: :last-child pseudo-class selector, doesn't match non-last-child elements: .pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child +Pass In-document Element.querySelectorAll: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass In-document Element.querySelector: :last-child pseudo-class selector, matching first-child of multiple elements: #pseudo-last-child span:last-child +Pass In-document Element.querySelectorAll: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass In-document Element.querySelector: :pseudo-only-child pseudo-class selector, matching all only-child elements: #pseudo-only :only-child +Pass In-document Element.querySelectorAll: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass In-document Element.querySelector: :pseudo-only-child pseudo-class selector, matching only-child em elements: #pseudo-only em:only-child +Pass In-document Element.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass In-document Element.querySelector: :pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type: #pseudo-only :only-of-type +Pass In-document Element.querySelectorAll: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass In-document Element.querySelector: :pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type: #pseudo-only em:only-of-type +Pass In-document Element.querySelectorAll: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass In-document Element.querySelector: :empty pseudo-class selector, matching empty p elements: #pseudo-empty p:empty +Pass In-document Element.querySelectorAll: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass In-document Element.querySelector: :empty pseudo-class selector, matching all empty elements: #pseudo-empty :empty +Pass In-document Element.querySelectorAll: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass In-document Element.querySelector: :link and :visited pseudo-class selectors, matching a and area elements with href attributes: #pseudo-link :link, #pseudo-link :visited +Pass In-document Element.querySelectorAll: :link and :visited pseudo-class selectors, not matching link elements with href attributes: #head :link, #head :visited +Pass In-document Element.querySelector: :link and :visited pseudo-class selectors, not matching link elements with href attributes: #head :link, #head :visited +Pass In-document Element.querySelectorAll: :link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing: :link:visited +Pass In-document Element.querySelector: :link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing: :link:visited +Fail In-document Element.querySelectorAll: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Fail In-document Element.querySelector: :target pseudo-class selector, matching the element referenced by the URL fragment identifier: :target +Pass In-document Element.querySelectorAll: :lang pseudo-class selector, matching inherited language: #pseudo-lang-div1:lang(en) +Pass In-document Element.querySelector: :lang pseudo-class selector, matching inherited language: #pseudo-lang-div1:lang(en) +Pass In-document Element.querySelectorAll: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass In-document Element.querySelector: :lang pseudo-class selector, matching specified language with exact value: #pseudo-lang-div2:lang(fr) +Pass In-document Element.querySelectorAll: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass In-document Element.querySelector: :lang pseudo-class selector, matching specified language with partial value: #pseudo-lang-div3:lang(en) +Pass In-document Element.querySelectorAll: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass In-document Element.querySelector: :lang pseudo-class selector, not matching incorrect language: #pseudo-lang-div4:lang(es-AR) +Pass In-document Element.querySelectorAll: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass In-document Element.querySelector: :enabled pseudo-class selector, matching all enabled form controls: #pseudo-ui :enabled +Pass In-document Element.querySelectorAll: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass In-document Element.querySelector: :enabled pseudo-class selector, not matching link elements: #pseudo-link :enabled +Pass In-document Element.querySelectorAll: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass In-document Element.querySelector: :disabled pseudo-class selector, matching all disabled form controls: #pseudo-ui :disabled +Pass In-document Element.querySelectorAll: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass In-document Element.querySelector: :disabled pseudo-class selector, not matching link elements: #pseudo-link :disabled +Pass In-document Element.querySelectorAll: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass In-document Element.querySelector: :checked pseudo-class selector, matching checked radio buttons and checkboxes: #pseudo-ui :checked +Pass In-document Element.querySelectorAll: :not pseudo-class selector, matching : #not>:not(div) +Pass In-document Element.querySelector: :not pseudo-class selector, matching : #not>:not(div) +Pass In-document Element.querySelectorAll: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass In-document Element.querySelector: :not pseudo-class selector, matching : #not * :not(:first-child) +Pass In-document Element.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*) +Pass In-document Element.querySelector: :not pseudo-class selector, matching nothing: :not(*) +Pass In-document Element.querySelectorAll: :not pseudo-class selector, matching nothing: :not(*|*) +Pass In-document Element.querySelector: :not pseudo-class selector, matching nothing: :not(*|*) +Pass In-document Element.querySelectorAll: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass In-document Element.querySelector: :not pseudo-class selector argument surrounded by spaces, matching : #not>:not( div ) +Pass In-document Element.querySelectorAll: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass In-document Element.querySelector: :first-line pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-line +Pass In-document Element.querySelectorAll: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass In-document Element.querySelector: ::first-line pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-line +Pass In-document Element.querySelectorAll: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass In-document Element.querySelector: :first-letter pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:first-letter +Pass In-document Element.querySelectorAll: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass In-document Element.querySelector: ::first-letter pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::first-letter +Pass In-document Element.querySelectorAll: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass In-document Element.querySelector: :before pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:before +Pass In-document Element.querySelectorAll: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass In-document Element.querySelector: ::before pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::before +Pass In-document Element.querySelectorAll: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass In-document Element.querySelector: :after pseudo-element (one-colon syntax) selector, not matching any elements: #pseudo-element:after +Pass In-document Element.querySelectorAll: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass In-document Element.querySelector: ::after pseudo-element (two-colon syntax) selector, not matching any elements: #pseudo-element::after +Pass In-document Element.querySelectorAll: Class selector, matching element with specified class: .class-p +Pass In-document Element.querySelector: Class selector, matching element with specified class: .class-p +Pass In-document Element.querySelectorAll: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass In-document Element.querySelector: Class selector, chained, matching only elements with all specified classes: #class .apple.orange.banana +Pass In-document Element.querySelectorAll: Class Selector, chained, with type selector: div.apple.banana.orange +Pass In-document Element.querySelector: Class Selector, chained, with type selector: div.apple.banana.orange +Pass In-document Element.querySelectorAll: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass In-document Element.querySelector: Class selector, matching element with class value using non-ASCII characters (1): .台北Táiběi +Pass In-document Element.querySelectorAll: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass In-document Element.querySelector: Class selector, matching multiple elements with class value using non-ASCII characters: .台北 +Pass In-document Element.querySelectorAll: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass In-document Element.querySelector: Class selector, chained, matching element with multiple class values using non-ASCII characters (1): .台北Táiběi.台北 +Pass In-document Element.querySelectorAll: Class selector, matching element with class with escaped character: .foo\:bar +Pass In-document Element.querySelector: Class selector, matching element with class with escaped character: .foo\:bar +Pass In-document Element.querySelectorAll: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass In-document Element.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar +Pass In-document Element.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 +Pass In-document Element.querySelector: ID selector, matching element with specified id: #id #id-div1 +Fail In-document Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass In-document Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass In-document Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass In-document Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 +Pass In-document Element.querySelectorAll: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass In-document Element.querySelector: ID Selector, chained, with type selector: div#id-div1, div#id-div2 +Pass In-document Element.querySelectorAll: ID selector, not matching non-existent descendant: #id #none +Pass In-document Element.querySelector: ID selector, not matching non-existent descendant: #id #none +Pass In-document Element.querySelectorAll: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass In-document Element.querySelector: ID selector, not matching non-existent ancestor: #none #id-div1 +Pass In-document Element.querySelectorAll: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass In-document Element.querySelector: ID selector, matching multiple elements with duplicate id: #id-li-duplicate +Pass In-document Element.querySelectorAll: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass In-document Element.querySelector: ID selector, matching id value using non-ASCII characters (1): #台北Táiběi +Pass In-document Element.querySelectorAll: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass In-document Element.querySelector: ID selector, matching id value using non-ASCII characters (2): #台北 +Pass In-document Element.querySelectorAll: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass In-document Element.querySelector: ID selector, matching id values using non-ASCII characters (1): #台北Táiběi, #台北 +Pass In-document Element.querySelectorAll: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass In-document Element.querySelector: ID selector, matching element with id with escaped character: #\#foo\:bar +Pass In-document Element.querySelectorAll: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass In-document Element.querySelector: ID selector, matching element with id with escaped character: #test\.foo\[5\]bar +Pass In-document Element.querySelectorAll: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass In-document Element.querySelector: Namespace selector, matching element with any namespace: #any-namespace *|div +Pass In-document Element.querySelectorAll: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass In-document Element.querySelector: Namespace selector, matching div elements in no namespace only: #no-namespace |div +Pass In-document Element.querySelectorAll: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass In-document Element.querySelector: Namespace selector, matching any elements in no namespace only: #no-namespace |* +Pass In-document Element.querySelectorAll: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass In-document Element.querySelector: Descendant combinator, matching element that is a descendant of an element with id: #descendant div +Pass In-document Element.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element: body #descendant-div1 +Pass In-document Element.querySelector: Descendant combinator, matching element with id that is a descendant of an element: body #descendant-div1 +Pass In-document Element.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass In-document Element.querySelector: Descendant combinator, matching element with id that is a descendant of an element: div #descendant-div1 +Pass In-document Element.querySelectorAll: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass In-document Element.querySelector: Descendant combinator, matching element with id that is a descendant of an element with id: #descendant #descendant-div2 +Pass In-document Element.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass In-document Element.querySelector: Descendant combinator, matching element with class that is a descendant of an element with id: #descendant .descendant-div2 +Pass In-document Element.querySelectorAll: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass In-document Element.querySelector: Descendant combinator, matching element with class that is a descendant of an element with class: .descendant-div1 .descendant-div3 +Pass In-document Element.querySelectorAll: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass In-document Element.querySelector: Descendant combinator, not matching element with id that is not a descendant of an element with id: #descendant-div1 #descendant-div4 +Pass In-document Element.querySelectorAll: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass In-document Element.querySelector: Descendant combinator, whitespace characters: #descendant #descendant-div2 +Pass In-document Element.querySelectorAll: Child combinator, matching element that is a child of an element with id: #child>div +Pass In-document Element.querySelector: Child combinator, matching element that is a child of an element with id: #child>div +Pass In-document Element.querySelectorAll: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass In-document Element.querySelector: Child combinator, matching element with id that is a child of an element: div>#child-div1 +Pass In-document Element.querySelectorAll: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass In-document Element.querySelector: Child combinator, matching element with id that is a child of an element with id: #child>#child-div1 +Pass In-document Element.querySelectorAll: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass In-document Element.querySelector: Child combinator, matching element with id that is a child of an element with class: #child-div1>.child-div2 +Pass In-document Element.querySelectorAll: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass In-document Element.querySelector: Child combinator, matching element with class that is a child of an element with class: .child-div1>.child-div2 +Pass In-document Element.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass In-document Element.querySelector: Child combinator, not matching element with id that is not a child of an element with id: #child>#child-div3 +Pass In-document Element.querySelectorAll: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass In-document Element.querySelector: Child combinator, not matching element with id that is not a child of an element with class: #child-div1>.child-div3 +Pass In-document Element.querySelectorAll: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass In-document Element.querySelector: Child combinator, not matching element with class that is not a child of an element with class: .child-div1>.child-div3 +Pass In-document Element.querySelectorAll: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass In-document Element.querySelector: Child combinator, surrounded by whitespace: #child-div1 > #child-div2 +Pass In-document Element.querySelectorAll: Child combinator, whitespace after: #child-div1> #child-div2 +Pass In-document Element.querySelector: Child combinator, whitespace after: #child-div1> #child-div2 +Pass In-document Element.querySelectorAll: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass In-document Element.querySelector: Child combinator, whitespace before: #child-div1 >#child-div2 +Pass In-document Element.querySelectorAll: Child combinator, no whitespace: #child-div1>#child-div2 +Pass In-document Element.querySelector: Child combinator, no whitespace: #child-div1>#child-div2 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass In-document Element.querySelector: Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id: #adjacent-div2+div +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass In-document Element.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element: div+#adjacent-div4 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass In-document Element.querySelector: Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id: #adjacent-div2+#adjacent-div4 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass In-document Element.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id: #adjacent-div2+.adjacent-div4 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass In-document Element.querySelector: Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class: .adjacent-div2+.adjacent-div4 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass In-document Element.querySelector: Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element: #adjacent div+p +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass In-document Element.querySelector: Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id: #adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass In-document Element.querySelector: Adjacent sibling combinator, surrounded by whitespace: #adjacent-p2 + #adjacent-p3 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass In-document Element.querySelector: Adjacent sibling combinator, whitespace after: #adjacent-p2+ #adjacent-p3 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass In-document Element.querySelector: Adjacent sibling combinator, whitespace before: #adjacent-p2 +#adjacent-p3 +Pass In-document Element.querySelectorAll: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass In-document Element.querySelector: Adjacent sibling combinator, no whitespace: #adjacent-p2+#adjacent-p3 +Pass In-document Element.querySelectorAll: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass In-document Element.querySelector: General sibling combinator, matching element that is a sibling of an element with id: #sibling-div2~div +Pass In-document Element.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass In-document Element.querySelector: General sibling combinator, matching element with id that is a sibling of an element: div~#sibling-div4 +Pass In-document Element.querySelectorAll: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass In-document Element.querySelector: General sibling combinator, matching element with id that is a sibling of an element with id: #sibling-div2~#sibling-div4 +Pass In-document Element.querySelectorAll: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass In-document Element.querySelector: General sibling combinator, matching element with class that is a sibling of an element with id: #sibling-div2~.sibling-div +Pass In-document Element.querySelectorAll: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass In-document Element.querySelector: General sibling combinator, matching p element that is a sibling of a div element: #sibling div~p +Pass In-document Element.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass In-document Element.querySelector: General sibling combinator, not matching element with id that is not a sibling after a p element: #sibling>p~div +Pass In-document Element.querySelectorAll: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass In-document Element.querySelector: General sibling combinator, not matching element with id that is not a sibling after an element with id: #sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1 +Pass In-document Element.querySelectorAll: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass In-document Element.querySelector: General sibling combinator, surrounded by whitespace: #sibling-p2 ~ #sibling-p3 +Pass In-document Element.querySelectorAll: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass In-document Element.querySelector: General sibling combinator, whitespace after: #sibling-p2~ #sibling-p3 +Pass In-document Element.querySelectorAll: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass In-document Element.querySelector: General sibling combinator, whitespace before: #sibling-p2 ~#sibling-p3 +Pass In-document Element.querySelectorAll: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass In-document Element.querySelector: General sibling combinator, no whitespace: #sibling-p2~#sibling-p3 +Pass In-document Element.querySelectorAll: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass In-document Element.querySelector: Syntax, group of selectors separator, surrounded by whitespace: #group em , #group strong +Pass In-document Element.querySelectorAll: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass In-document Element.querySelector: Syntax, group of selectors separator, whitespace after: #group em, #group strong +Pass In-document Element.querySelectorAll: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass In-document Element.querySelector: Syntax, group of selectors separator, whitespace before: #group em ,#group strong +Pass In-document Element.querySelectorAll: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Pass In-document Element.querySelector: Syntax, group of selectors separator, no whitespace: #group em,#group strong +Fail In-document Element.querySelectorAll: Slotted selector: ::slotted(foo) Failed to parse selector +Fail In-document Element.querySelector: Slotted selector: ::slotted(foo) Failed to parse selector +Fail In-document Element.querySelectorAll: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector +Fail In-document Element.querySelector: Slotted selector (no matching closing paren): ::slotted(foo Failed to parse selector \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.html new file mode 100644 index 000000000000..4244606205f4 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.html @@ -0,0 +1,120 @@ + + + +Selectors-API Test Suite: HTML + + + + + + +
    This test requires JavaScript.
    + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.js b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.js new file mode 100644 index 000000000000..3c6c50317988 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/ParentNode-querySelector-All.js @@ -0,0 +1,261 @@ +// Require selectors.js to be included before this. + +/* + * Create and append special elements that cannot be created correctly with HTML markup alone. + */ +function setupSpecialElements(doc, parent) { + // Setup null and undefined tests + parent.appendChild(doc.createElement("null")); + parent.appendChild(doc.createElement("undefined")); + + // Setup namespace tests + var anyNS = doc.createElement("div"); + var noNS = doc.createElement("div"); + anyNS.id = "any-namespace"; + noNS.id = "no-namespace"; + + var divs; + div = [doc.createElement("div"), + doc.createElementNS("http://www.w3.org/1999/xhtml", "div"), + doc.createElementNS("", "div"), + doc.createElementNS("http://www.example.org/ns", "div")]; + + div[0].id = "any-namespace-div1"; + div[1].id = "any-namespace-div2"; + div[2].setAttribute("id", "any-namespace-div3"); // Non-HTML elements can't use .id property + div[3].setAttribute("id", "any-namespace-div4"); + + for (var i = 0; i < div.length; i++) { + anyNS.appendChild(div[i]) + } + + div = [doc.createElement("div"), + doc.createElementNS("http://www.w3.org/1999/xhtml", "div"), + doc.createElementNS("", "div"), + doc.createElementNS("http://www.example.org/ns", "div")]; + + div[0].id = "no-namespace-div1"; + div[1].id = "no-namespace-div2"; + div[2].setAttribute("id", "no-namespace-div3"); // Non-HTML elements can't use .id property + div[3].setAttribute("id", "no-namespace-div4"); + + for (i = 0; i < div.length; i++) { + noNS.appendChild(div[i]) + } + + parent.appendChild(anyNS); + parent.appendChild(noNS); + + var span = doc.getElementById("attr-presence-i1"); + span.setAttributeNS("http://www.example.org/ns", "title", ""); +} + +/* + * Check that the querySelector and querySelectorAll methods exist on the given Node + */ +function interfaceCheck(type, obj) { + test(function() { + var q = typeof obj.querySelector === "function"; + assert_true(q, type + " supports querySelector."); + }, type + " supports querySelector") + + test(function() { + var qa = typeof obj.querySelectorAll === "function"; + assert_true( qa, type + " supports querySelectorAll."); + }, type + " supports querySelectorAll") + + test(function() { + var list = obj.querySelectorAll("div"); + if (obj.ownerDocument) { // The object is not a Document + assert_true(list instanceof obj.ownerDocument.defaultView.NodeList, "The result should be an instance of a NodeList") + } else { // The object is a Document + assert_true(list instanceof obj.defaultView.NodeList, "The result should be an instance of a NodeList") + } + }, type + ".querySelectorAll returns NodeList instance") +} + +/* + * Verify that the NodeList returned by querySelectorAll is static and and that a new list is created after + * each call. A static list should not be affected by subsequent changes to the DOM. + */ +function verifyStaticList(type, doc, root) { + var pre, post, preLength; + + test(function() { + pre = root.querySelectorAll("div"); + preLength = pre.length; + + var div = doc.createElement("div"); + (root.body || root).appendChild(div); + + assert_equals(pre.length, preLength, "The length of the NodeList should not change.") + }, type + ": static NodeList") + + test(function() { + post = root.querySelectorAll("div"), + assert_equals(post.length, preLength + 1, "The length of the new NodeList should be 1 more than the previous list.") + }, type + ": new NodeList") +} + +/* + * Verify handling of special values for the selector parameter, including stringification of + * null and undefined, and the handling of the empty string. + */ +function runSpecialSelectorTests(type, root) { + let global = (root.ownerDocument || root).defaultView; + + test(function() { // 1 + assert_equals(root.querySelectorAll(null).length, 1, "This should find one element with the tag name 'NULL'."); + }, type + ".querySelectorAll null") + + test(function() { // 2 + assert_equals(root.querySelectorAll(undefined).length, 1, "This should find one element with the tag name 'UNDEFINED'."); + }, type + ".querySelectorAll undefined") + + test(function() { // 3 + assert_throws_js(global.TypeError, function() { + root.querySelectorAll(); + }, "This should throw a TypeError.") + }, type + ".querySelectorAll no parameter") + + test(function() { // 4 + var elm = root.querySelector(null) + assert_not_equals(elm, null, "This should find an element."); + assert_equals(elm.tagName.toUpperCase(), "NULL", "The tag name should be 'NULL'.") + }, type + ".querySelector null") + + test(function() { // 5 + var elm = root.querySelector(undefined) + assert_not_equals(elm, undefined, "This should find an element."); + assert_equals(elm.tagName.toUpperCase(), "UNDEFINED", "The tag name should be 'UNDEFINED'.") + }, type + ".querySelector undefined") + + test(function() { // 6 + assert_throws_js(global.TypeError, function() { + root.querySelector(); + }, "This should throw a TypeError.") + }, type + ".querySelector no parameter") + + test(function() { // 7 + result = root.querySelectorAll("*"); + var i = 0; + traverse(root, function(elem) { + if (elem !== root) { + assert_equals(elem, result[i], "The result in index " + i + " should be in tree order."); + i++; + } + }) + }, type + ".querySelectorAll tree order"); +} + +/* + * Execute queries with the specified valid selectors for both querySelector() and querySelectorAll() + * Only run these tests when results are expected. Don't run for syntax error tests. + */ +function runValidSelectorTest(type, root, selectors, testType, docType) { + var nodeType = ""; + switch (root.nodeType) { + case Node.DOCUMENT_NODE: + nodeType = "document"; + break; + case Node.ELEMENT_NODE: + nodeType = root.parentNode ? "element" : "detached"; + break; + case Node.DOCUMENT_FRAGMENT_NODE: + nodeType = "fragment"; + break; + default: + assert_unreached(); + nodeType = "unknown"; // This should never happen. + } + + for (var i = 0; i < selectors.length; i++) { + var s = selectors[i]; + var n = s["name"]; + var q = s["selector"]; + var e = s["expect"]; + + if ((!s["exclude"] || (s["exclude"].indexOf(nodeType) === -1 && s["exclude"].indexOf(docType) === -1)) + && (s["testType"] & testType) ) { + var foundall, found; + + test(function() { + foundall = root.querySelectorAll(q); + assert_not_equals(foundall, null, "The method should not return null.") + assert_equals(foundall.length, e.length, "The method should return the expected number of matches.") + + for (var i = 0; i < e.length; i++) { + assert_not_equals(foundall[i], null, "The item in index " + i + " should not be null.") + assert_equals(foundall[i].getAttribute("id"), e[i], "The item in index " + i + " should have the expected ID."); + assert_false(foundall[i].hasAttribute("data-clone"), "This should not be a cloned element."); + } + }, type + ".querySelectorAll: " + n + ": " + q); + + test(function() { + found = root.querySelector(q); + + if (e.length > 0) { + assert_not_equals(found, null, "The method should return a match.") + assert_equals(found.getAttribute("id"), e[0], "The method should return the first match."); + assert_equals(found, foundall[0], "The result should match the first item from querySelectorAll."); + assert_false(found.hasAttribute("data-clone"), "This should not be annotated as a cloned element."); + } else { + assert_equals(found, null, "The method should not match anything."); + } + }, type + ".querySelector: " + n + ": " + q); + } + } +} + +function windowFor(root) { + return root.defaultView || root.ownerDocument.defaultView; +} + +/* + * Execute queries with the specified invalid selectors for both querySelector() and querySelectorAll() + * Only run these tests when errors are expected. Don't run for valid selector tests. + */ +function runInvalidSelectorTest(type, root, selectors) { + for (var i = 0; i < selectors.length; i++) { + var s = selectors[i]; + var n = s["name"]; + var q = s["selector"]; + + test(function() { + assert_throws_dom("SyntaxError", windowFor(root).DOMException, function() { + root.querySelector(q) + }); + }, type + ".querySelector: " + n + ": " + q); + + test(function() { + assert_throws_dom("SyntaxError", windowFor(root).DOMException, function() { + root.querySelectorAll(q) + }); + }, type + ".querySelectorAll: " + n + ": " + q); + } +} + +function traverse(elem, fn) { + if (elem.nodeType === elem.ELEMENT_NODE) { + fn(elem); + } + elem = elem.firstChild; + while (elem) { + traverse(elem, fn); + elem = elem.nextSibling; + } +} + +function getNodeType(node) { + switch (node.nodeType) { + case Node.DOCUMENT_NODE: + return "document"; + case Node.ELEMENT_NODE: + return node.parentNode ? "element" : "detached"; + case Node.DOCUMENT_FRAGMENT_NODE: + return "fragment"; + default: + assert_unreached(); + return "unknown"; // This should never happen. + } +} diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/selectors.js b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/selectors.js new file mode 100644 index 000000000000..5e05547c9acc --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/selectors.js @@ -0,0 +1,755 @@ +// Bit-mapped flags to indicate which tests the selector is suitable for +var TEST_QSA = 0x01; // querySelector() and querySelectorAll() tests +var TEST_FIND = 0x04; // find() and findAll() tests, may be unsuitable for querySelector[All] +var TEST_MATCH = 0x10; // matches() tests + +/* + * All of these invalid selectors should result in a SyntaxError being thrown by the APIs. + * + * name: A descriptive name of the selector being tested + * selector: The selector to test + */ +var invalidSelectors = [ + {name: "Empty String", selector: ""}, + {name: "Invalid character", selector: "["}, + {name: "Invalid character", selector: "]"}, + {name: "Invalid character", selector: "("}, + {name: "Invalid character", selector: ")"}, + {name: "Invalid character", selector: "{"}, + {name: "Invalid character", selector: "}"}, + {name: "Invalid character", selector: "<"}, + {name: "Invalid character", selector: ">"}, + {name: "Invalid ID", selector: "#"}, + {name: "Invalid group of selectors", selector: "div,"}, + {name: "Invalid class", selector: "."}, + {name: "Invalid class", selector: ".5cm"}, + {name: "Invalid class", selector: "..test"}, + {name: "Invalid class", selector: ".foo..quux"}, + {name: "Invalid class", selector: ".bar."}, + {name: "Invalid combinator", selector: "div % address, p"}, + {name: "Invalid combinator", selector: "div ++ address, p"}, + {name: "Invalid combinator", selector: "div ~~ address, p"}, + {name: "Invalid [att=value] selector", selector: "[*=test]"}, + {name: "Invalid [att=value] selector", selector: "[*|*=test]"}, + {name: "Invalid [att=value] selector", selector: "[class= space unquoted ]"}, + {name: "Unknown pseudo-class", selector: "div:example"}, + {name: "Unknown pseudo-class", selector: ":example"}, + {name: "Unknown pseudo-class", selector: "div:linkexample"}, + {name: "Unknown pseudo-element", selector: "div::example"}, + {name: "Unknown pseudo-element", selector: "::example"}, + {name: "Invalid pseudo-element", selector: ":::before"}, + {name: "Invalid pseudo-element", selector: ":: before"}, + {name: "Undeclared namespace", selector: "ns|div"}, + {name: "Undeclared namespace", selector: ":not(ns|div)"}, + {name: "Invalid namespace", selector: "^|div"}, + {name: "Invalid namespace", selector: "$|div"}, + {name: "Relative selector", selector: ">*"}, +]; + +/* + * All of these should be valid selectors, expected to match zero or more elements in the document. + * None should throw any errors. + * + * name: A descriptive name of the selector being tested + * selector: The selector to test + * expect: A list of IDs of the elements expected to be matched. List must be given in tree order. + * exclude: An array of contexts to exclude from testing. The valid values are: + * ["document", "element", "fragment", "detached", "html", "xhtml"] + * The "html" and "xhtml" values represent the type of document being queried. These are useful + * for tests that are affected by differences between HTML and XML, such as case sensitivity. + * level: An integer indicating the CSS or Selectors level in which the selector being tested was introduced. + * testType: A bit-mapped flag indicating the type of test. + * + * Note: Interactive pseudo-classes (:active :hover and :focus) have not been tested in this test suite. + */ +var validSelectors = [ + // Type Selector + {name: "Type selector, matching html element", selector: "html", expect: ["html"], exclude: ["element", "fragment", "detached"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Type selector, matching html element", selector: "html", expect: [] /*no matches*/, exclude: ["document"], level: 1, testType: TEST_QSA}, + {name: "Type selector, matching body element", selector: "body", expect: ["body"], exclude: ["element", "fragment", "detached"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Type selector, matching body element", selector: "body", expect: [] /*no matches*/, exclude: ["document"], level: 1, testType: TEST_QSA}, + + // Universal Selector + {name: "Universal selector, matching all elements", selector: "*", expect: ["universal", "universal-p1", "universal-code1", "universal-hr1", "universal-pre1", "universal-span1", "universal-p2", "universal-a1", "universal-address1", "universal-code2", "universal-a2"], level: 2, testType: TEST_MATCH}, + {name: "Universal selector, matching all children of element with specified ID", selector: "#universal>*", expect: ["universal-p1", "universal-hr1", "universal-pre1", "universal-p2", "universal-address1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Universal selector, matching all grandchildren of element with specified ID", selector: "#universal>*>*", expect: ["universal-code1", "universal-span1", "universal-a1", "universal-code2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Universal selector, matching all children of empty element with specified ID", selector: "#empty>*", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Universal selector, matching all descendants of element with specified ID", selector: "#universal *", expect: ["universal-p1", "universal-code1", "universal-hr1", "universal-pre1", "universal-span1", "universal-p2", "universal-a1", "universal-address1", "universal-code2", "universal-a2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // Attribute Selectors + // - presence [att] + {name: "Attribute presence selector, matching align attribute with value", selector: ".attr-presence-div1[align]", expect: ["attr-presence-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, matching align attribute with empty value", selector: ".attr-presence-div2[align]", expect: ["attr-presence-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, matching title attribute, case insensitivity", selector: "#attr-presence [*|TiTlE]", expect: ["attr-presence-a1", "attr-presence-span1", "attr-presence-i1"], exclude: ["xhtml"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, not matching title attribute, case sensitivity", selector: "#attr-presence [*|TiTlE]", expect: [], exclude: ["html"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, matching custom data-* attribute", selector: "[data-attr-presence]", expect: ["attr-presence-pre1", "attr-presence-blockquote1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, not matching attribute with similar name", selector: ".attr-presence-div3[align], .attr-presence-div4[align]", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Attribute presence selector, matching attribute with non-ASCII characters", selector: "ul[data-中文]", expect: ["attr-presence-ul1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, not matching default option without selected attribute", selector: "#attr-presence-select1 option[selected]", expect: [] /* no matches */, level: 2, testType: TEST_QSA}, + {name: "Attribute presence selector, matching option with selected attribute", selector: "#attr-presence-select2 option[selected]", expect: ["attr-presence-select2-option4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute presence selector, matching multiple options with selected attributes", selector: "#attr-presence-select3 option[selected]", expect: ["attr-presence-select3-option2", "attr-presence-select3-option3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // - value [att=val] + {name: "Attribute value selector, matching align attribute with value", selector: "#attr-value [align=\"center\"]", expect: ["attr-value-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector, matching align attribute with value, unclosed bracket", selector: "#attr-value [align=\"center\"", expect: ["attr-value-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector, matching align attribute with empty value", selector: "#attr-value [align=\"\"]", expect: ["attr-value-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector, not matching align attribute with partial value", selector: "#attr-value [align=\"c\"]", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Attribute value selector, not matching align attribute with incorrect value", selector: "#attr-value [align=\"centera\"]", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Attribute value selector, matching custom data-* attribute with unicode escaped value", selector: "[data-attr-value=\"\\e9\"]", expect: ["attr-value-div3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector, matching custom data-* attribute with escaped character", selector: "[data-attr-value\_foo=\"\\e9\"]", expect: ["attr-value-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector with single-quoted value, matching multiple inputs with type attributes", selector: "#attr-value input[type='hidden'],#attr-value input[type='radio']", expect: ["attr-value-input3", "attr-value-input4", "attr-value-input6", "attr-value-input8", "attr-value-input9"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector with double-quoted value, matching multiple inputs with type attributes", selector: "#attr-value input[type=\"hidden\"],#attr-value input[type='radio']", expect: ["attr-value-input3", "attr-value-input4", "attr-value-input6", "attr-value-input8", "attr-value-input9"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector with unquoted value, matching multiple inputs with type attributes", selector: "#attr-value input[type=hidden],#attr-value input[type=radio]", expect: ["attr-value-input3", "attr-value-input4", "attr-value-input6", "attr-value-input8", "attr-value-input9"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute value selector, matching attribute with value using non-ASCII characters", selector: "[data-attr-value=中文]", expect: ["attr-value-div5"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // - whitespace-separated list [att~=val] + {name: "Attribute whitespace-separated list selector, matching class attribute with value", selector: "#attr-whitespace [class~=\"div1\"]", expect: ["attr-whitespace-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector, not matching class attribute with empty value", selector: "#attr-whitespace [class~=\"\"]", expect: [] /*no matches*/ , level: 2, testType: TEST_QSA}, + {name: "Attribute whitespace-separated list selector, not matching class attribute with partial value", selector: "[data-attr-whitespace~=\"div\"]", expect: [] /*no matches*/ , level: 2, testType: TEST_QSA}, + {name: "Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value", selector: "[data-attr-whitespace~=\"\\0000e9\"]", expect: ["attr-whitespace-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character", selector: "[data-attr-whitespace\_foo~=\"\\e9\"]", expect: ["attr-whitespace-div5"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes", selector: "#attr-whitespace a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow']", expect: ["attr-whitespace-a1", "attr-whitespace-a2", "attr-whitespace-a3", "attr-whitespace-a5", "attr-whitespace-a7"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes", selector: "#attr-whitespace a[rel~=\"bookmark\"],#attr-whitespace a[rel~='nofollow']", expect: ["attr-whitespace-a1", "attr-whitespace-a2", "attr-whitespace-a3", "attr-whitespace-a5", "attr-whitespace-a7"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes", selector: "#attr-whitespace a[rel~=bookmark], #attr-whitespace a[rel~=nofollow]", expect: ["attr-whitespace-a1", "attr-whitespace-a2", "attr-whitespace-a3", "attr-whitespace-a5", "attr-whitespace-a7"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with double-quoted value, not matching value with space", selector: "#attr-whitespace a[rel~=\"book mark\"]", expect: [] /* no matches */, level: 2, testType: TEST_QSA}, + {name: "Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters", selector: "#attr-whitespace [title~=中文]", expect: ["attr-whitespace-p1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // - hyphen-separated list [att|=val] + {name: "Attribute hyphen-separated list selector, not matching unspecified lang attribute", selector: "#attr-hyphen-div1[lang|=\"en\"]", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Attribute hyphen-separated list selector, matching lang attribute with exact value", selector: "#attr-hyphen-div2[lang|=\"fr\"]", expect: ["attr-hyphen-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute hyphen-separated list selector, matching lang attribute with partial value", selector: "#attr-hyphen-div3[lang|=\"en\"]", expect: ["attr-hyphen-div3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute hyphen-separated list selector, not matching incorrect value", selector: "#attr-hyphen-div4[lang|=\"es-AR\"]", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + + // - substring begins-with [att^=val] (Level 3) + {name: "Attribute begins with selector, matching href attributes beginning with specified substring", selector: "#attr-begins a[href^=\"http://www\"]", expect: ["attr-begins-a1", "attr-begins-a3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute begins with selector, matching lang attributes beginning with specified substring, ", selector: "#attr-begins [lang^=\"en-\"]", expect: ["attr-begins-div2", "attr-begins-div4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute begins with selector, not matching class attribute with empty value", selector: "#attr-begins [class^=\"\"]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "Attribute begins with selector, not matching class attribute not beginning with specified substring", selector: "#attr-begins [class^=apple]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring", selector: "#attr-begins [class^=' apple']", expect: ["attr-begins-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring", selector: "#attr-begins [class^=\" apple\"]", expect: ["attr-begins-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring", selector: "#attr-begins [class^= apple]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // - substring ends-with [att$=val] (Level 3) + {name: "Attribute ends with selector, matching href attributes ending with specified substring", selector: "#attr-ends a[href$=\".org\"]", expect: ["attr-ends-a1", "attr-ends-a3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute ends with selector, matching lang attributes ending with specified substring, ", selector: "#attr-ends [lang$=\"-CH\"]", expect: ["attr-ends-div2", "attr-ends-div4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute ends with selector, not matching class attribute with empty value", selector: "#attr-ends [class$=\"\"]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "Attribute ends with selector, not matching class attribute not ending with specified substring", selector: "#attr-ends [class$=apple]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring", selector: "#attr-ends [class$='apple ']", expect: ["attr-ends-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring", selector: "#attr-ends [class$=\"apple \"]", expect: ["attr-ends-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring", selector: "#attr-ends [class$=apple ]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // - substring contains [att*=val] (Level 3) + {name: "Attribute contains selector, matching href attributes beginning with specified substring", selector: "#attr-contains a[href*=\"http://www\"]", expect: ["attr-contains-a1", "attr-contains-a3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector, matching href attributes ending with specified substring", selector: "#attr-contains a[href*=\".org\"]", expect: ["attr-contains-a1", "attr-contains-a2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector, matching href attributes containing specified substring", selector: "#attr-contains a[href*=\".example.\"]", expect: ["attr-contains-a1", "attr-contains-a3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector, matching lang attributes beginning with specified substring, ", selector: "#attr-contains [lang*=\"en-\"]", expect: ["attr-contains-div2", "attr-contains-div6"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector, matching lang attributes ending with specified substring, ", selector: "#attr-contains [lang*=\"-CH\"]", expect: ["attr-contains-div3", "attr-contains-div5"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector, not matching class attribute with empty value", selector: "#attr-contains [class*=\"\"]", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring", selector: "#attr-contains [class*=' apple']", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with single-quoted value, matching class attribute ending with specified substring", selector: "#attr-contains [class*='orange ']", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with single-quoted value, matching class attribute containing specified substring", selector: "#attr-contains [class*='ple banana ora']", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring", selector: "#attr-contains [class*=\" apple\"]", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with double-quoted value, matching class attribute ending with specified substring", selector: "#attr-contains [class*=\"orange \"]", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with double-quoted value, matching class attribute containing specified substring", selector: "#attr-contains [class*=\"ple banana ora\"]", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with unquoted value, matching class attribute beginning with specified substring", selector: "#attr-contains [class*= apple]", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with unquoted value, matching class attribute ending with specified substring", selector: "#attr-contains [class*=orange ]", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "Attribute contains selector with unquoted value, matching class attribute containing specified substring", selector: "#attr-contains [class*= banana ]", expect: ["attr-contains-p1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // Pseudo-classes + // - :root (Level 3) + {name: ":root pseudo-class selector, matching document root element", selector: ":root", expect: ["html"], exclude: ["element", "fragment", "detached"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":root pseudo-class selector, not matching document root element", selector: ":root", expect: [] /*no matches*/, exclude: ["document"], level: 3, testType: TEST_QSA}, + + // - :nth-child(n) (Level 3) + // XXX write descriptions + {name: ":nth-child selector, matching the third child element", selector: "#pseudo-nth-table1 :nth-child(3)", expect: ["pseudo-nth-td3", "pseudo-nth-td9", "pseudo-nth-tr3", "pseudo-nth-td15"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-child selector, matching every third child element", selector: "#pseudo-nth li:nth-child(3n)", expect: ["pseudo-nth-li3", "pseudo-nth-li6", "pseudo-nth-li9", "pseudo-nth-li12"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-child selector, matching every second child element, starting from the fourth", selector: "#pseudo-nth li:nth-child(2n+4)", expect: ["pseudo-nth-li4", "pseudo-nth-li6", "pseudo-nth-li8", "pseudo-nth-li10", "pseudo-nth-li12"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-child selector, matching every fourth child element, starting from the third", selector: "#pseudo-nth-p1 :nth-child(4n-1)", expect: ["pseudo-nth-em2", "pseudo-nth-span3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :nth-last-child (Level 3) + {name: ":nth-last-child selector, matching the third last child element", selector: "#pseudo-nth-table1 :nth-last-child(3)", expect: ["pseudo-nth-tr1", "pseudo-nth-td4", "pseudo-nth-td10", "pseudo-nth-td16"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-last-child selector, matching every third child element from the end", selector: "#pseudo-nth li:nth-last-child(3n)", expect: ["pseudo-nth-li1", "pseudo-nth-li4", "pseudo-nth-li7", "pseudo-nth-li10"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-last-child selector, matching every second child element from the end, starting from the fourth last", selector: "#pseudo-nth li:nth-last-child(2n+4)", expect: ["pseudo-nth-li1", "pseudo-nth-li3", "pseudo-nth-li5", "pseudo-nth-li7", "pseudo-nth-li9"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-last-child selector, matching every fourth element from the end, starting from the third last", selector: "#pseudo-nth-p1 :nth-last-child(4n-1)", expect: ["pseudo-nth-span2", "pseudo-nth-span4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :nth-of-type(n) (Level 3) + {name: ":nth-of-type selector, matching the third em element", selector: "#pseudo-nth-p1 em:nth-of-type(3)", expect: ["pseudo-nth-em3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-of-type selector, matching every second element of their type", selector: "#pseudo-nth-p1 :nth-of-type(2n)", expect: ["pseudo-nth-em2", "pseudo-nth-span2", "pseudo-nth-span4", "pseudo-nth-strong2", "pseudo-nth-em4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-of-type selector, matching every second elemetn of their type, starting from the first", selector: "#pseudo-nth-p1 span:nth-of-type(2n-1)", expect: ["pseudo-nth-span1", "pseudo-nth-span3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :nth-last-of-type(n) (Level 3) + {name: ":nth-last-of-type selector, matching the third last em element", selector: "#pseudo-nth-p1 em:nth-last-of-type(3)", expect: ["pseudo-nth-em2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-last-of-type selector, matching every second last element of their type", selector: "#pseudo-nth-p1 :nth-last-of-type(2n)", expect: ["pseudo-nth-span1", "pseudo-nth-em1", "pseudo-nth-strong1", "pseudo-nth-em3", "pseudo-nth-span3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":nth-last-of-type selector, matching every second last element of their type, starting from the last", selector: "#pseudo-nth-p1 span:nth-last-of-type(2n-1)", expect: ["pseudo-nth-span2", "pseudo-nth-span4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :first-of-type (Level 3) + {name: ":first-of-type selector, matching the first em element", selector: "#pseudo-nth-p1 em:first-of-type", expect: ["pseudo-nth-em1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":first-of-type selector, matching the first of every type of element", selector: "#pseudo-nth-p1 :first-of-type", expect: ["pseudo-nth-span1", "pseudo-nth-em1", "pseudo-nth-strong1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":first-of-type selector, matching the first td element in each table row", selector: "#pseudo-nth-table1 tr :first-of-type", expect: ["pseudo-nth-td1", "pseudo-nth-td7", "pseudo-nth-td13"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :last-of-type (Level 3) + {name: ":last-of-type selector, matching the last em elemnet", selector: "#pseudo-nth-p1 em:last-of-type", expect: ["pseudo-nth-em4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":last-of-type selector, matching the last of every type of element", selector: "#pseudo-nth-p1 :last-of-type", expect: ["pseudo-nth-span4", "pseudo-nth-strong2", "pseudo-nth-em4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":last-of-type selector, matching the last td element in each table row", selector: "#pseudo-nth-table1 tr :last-of-type", expect: ["pseudo-nth-td6", "pseudo-nth-td12", "pseudo-nth-td18"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :first-child + {name: ":first-child pseudo-class selector, matching first child div element", selector: "#pseudo-first-child div:first-child", expect: ["pseudo-first-child-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: ":first-child pseudo-class selector, doesn't match non-first-child elements", selector: ".pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: ":first-child pseudo-class selector, matching first-child of multiple elements", selector: "#pseudo-first-child span:first-child", expect: ["pseudo-first-child-span1", "pseudo-first-child-span3", "pseudo-first-child-span5"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // - :last-child (Level 3) + {name: ":last-child pseudo-class selector, matching last child div element", selector: "#pseudo-last-child div:last-child", expect: ["pseudo-last-child-div3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":last-child pseudo-class selector, doesn't match non-last-child elements", selector: ".pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: ":last-child pseudo-class selector, matching first-child of multiple elements", selector: "#pseudo-last-child span:last-child", expect: ["pseudo-last-child-span2", "pseudo-last-child-span4", "pseudo-last-child-span6"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :only-child (Level 3) + {name: ":pseudo-only-child pseudo-class selector, matching all only-child elements", selector: "#pseudo-only :only-child", expect: ["pseudo-only-span1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":pseudo-only-child pseudo-class selector, matching only-child em elements", selector: "#pseudo-only em:only-child", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // - :only-of-type (Level 3) + {name: ":pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type", selector: "#pseudo-only :only-of-type", expect: ["pseudo-only-span1", "pseudo-only-em1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type", selector: "#pseudo-only em:only-of-type", expect: ["pseudo-only-em1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :empty (Level 3) + {name: ":empty pseudo-class selector, matching empty p elements", selector: "#pseudo-empty p:empty", expect: ["pseudo-empty-p1", "pseudo-empty-p2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":empty pseudo-class selector, matching all empty elements", selector: "#pseudo-empty :empty", expect: ["pseudo-empty-p1", "pseudo-empty-p2", "pseudo-empty-span1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :link and :visited + // Implementations may treat all visited links as unvisited, so these cannot be tested separately. + // The only guarantee is that ":link,:visited" matches the set of all visited and unvisited links and that they are individually mutually exclusive sets. + {name: ":link and :visited pseudo-class selectors, matching a and area elements with href attributes", selector: "#pseudo-link :link, #pseudo-link :visited", expect: ["pseudo-link-a1", "pseudo-link-a2", "pseudo-link-area1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: ":link and :visited pseudo-class selectors, matching no elements", selector: "#head :link, #head :visited", expect: [] /*no matches*/, exclude: ["element", "fragment", "detached"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: ":link and :visited pseudo-class selectors, not matching link elements with href attributes", selector: "#head :link, #head :visited", expect: [] /*no matches*/, exclude: ["document"], level: 1, testType: TEST_QSA}, + {name: ":link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing", selector: ":link:visited", expect: [] /*no matches*/, exclude: ["document"], level: 1, testType: TEST_QSA}, + + // - :target (Level 3) + {name: ":target pseudo-class selector, matching the element referenced by the URL fragment identifier", selector: ":target", expect: [] /*no matches*/, exclude: ["document", "element"], level: 3, testType: TEST_QSA}, + {name: ":target pseudo-class selector, matching the element referenced by the URL fragment identifier", selector: ":target", expect: ["target"], exclude: ["fragment", "detached"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :lang() + {name: ":lang pseudo-class selector, matching inherited language", selector: "#pseudo-lang-div1:lang(en)", expect: ["pseudo-lang-div1"], exclude: ["detached", "fragment"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: ":lang pseudo-class selector, not matching element with no inherited language", selector: "#pseudo-lang-div1:lang(en)", expect: [] /*no matches*/, exclude: ["document", "element"], level: 2, testType: TEST_QSA}, + {name: ":lang pseudo-class selector, matching specified language with exact value", selector: "#pseudo-lang-div2:lang(fr)", expect: ["pseudo-lang-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: ":lang pseudo-class selector, matching specified language with partial value", selector: "#pseudo-lang-div3:lang(en)", expect: ["pseudo-lang-div3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: ":lang pseudo-class selector, not matching incorrect language", selector: "#pseudo-lang-div4:lang(es-AR)", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + + // - :enabled (Level 3) + {name: ":enabled pseudo-class selector, matching all enabled form controls", selector: "#pseudo-ui :enabled", expect: ["pseudo-ui-input1", "pseudo-ui-input2", "pseudo-ui-input3", "pseudo-ui-input4", "pseudo-ui-input5", "pseudo-ui-input6", + "pseudo-ui-input7", "pseudo-ui-input8", "pseudo-ui-input9", "pseudo-ui-textarea1", "pseudo-ui-button1"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":enabled pseudo-class selector, not matching link elements", selector: "#pseudo-link :enabled", expect: [] /*no matches*/, unexpected: ["pseudo-link-a1","pseudo-link-a2","pseudo-link-a3","pseudo-link-map1","pseudo-link-area1","pseudo-link-area2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :disabled (Level 3) + {name: ":disabled pseudo-class selector, matching all disabled form controls", selector: "#pseudo-ui :disabled", expect: ["pseudo-ui-input10", "pseudo-ui-input11", "pseudo-ui-input12", "pseudo-ui-input13", "pseudo-ui-input14", "pseudo-ui-input15", + "pseudo-ui-input16", "pseudo-ui-input17", "pseudo-ui-input18", "pseudo-ui-textarea2", "pseudo-ui-button2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":disabled pseudo-class selector, not matching link elements", selector: "#pseudo-link :disabled", expect: [] /*no matches*/, unexpected: ["pseudo-link-a1","pseudo-link-a2","pseudo-link-a3","pseudo-link-map1","pseudo-link-area1","pseudo-link-area2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :checked (Level 3) + {name: ":checked pseudo-class selector, matching checked radio buttons and checkboxes", selector: "#pseudo-ui :checked", expect: ["pseudo-ui-input4", "pseudo-ui-input6", "pseudo-ui-input13", "pseudo-ui-input15"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :not(s) (Level 3) + {name: ":not pseudo-class selector, matching ", selector: "#not>:not(div)", expect: ["not-p1", "not-p2", "not-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":not pseudo-class selector, matching ", selector: "#not * :not(:first-child)", expect: ["not-em1", "not-em2", "not-em3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: ":not pseudo-class selector, matching nothing", selector: ":not(*)", expect: [] /* no matches */, level: 3, testType: TEST_QSA}, + {name: ":not pseudo-class selector, matching nothing", selector: ":not(*|*)", expect: [] /* no matches */, level: 3, testType: TEST_QSA}, + {name: ":not pseudo-class selector argument surrounded by spaces, matching ", selector: "#not>:not( div )", expect: ["not-p1", "not-p2", "not-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // Pseudo-elements + // - ::first-line + {name: ":first-line pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:first-line", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "::first-line pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::first-line", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // - ::first-letter + {name: ":first-letter pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:first-letter", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "::first-letter pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::first-letter", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // - ::before + {name: ":before pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:before", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "::before pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::before", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // - ::after + {name: ":after pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:after", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "::after pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::after", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + + // Class Selectors + {name: "Class selector, matching element with specified class", selector: ".class-p", expect: ["class-p1","class-p2", "class-p3"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class selector, chained, matching only elements with all specified classes", selector: "#class .apple.orange.banana", expect: ["class-div1", "class-div2", "class-p4", "class-div3", "class-p6", "class-div4"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class Selector, chained, with type selector", selector: "div.apple.banana.orange", expect: ["class-div1", "class-div2", "class-div3", "class-div4"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class selector, matching element with class value using non-ASCII characters (1)", selector: ".\u53F0\u5317Ta\u0301ibe\u030Ci", expect: ["class-span1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class selector, matching multiple elements with class value using non-ASCII characters", selector: ".\u53F0\u5317", expect: ["class-span1","class-span2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class selector, chained, matching element with multiple class values using non-ASCII characters (1)", selector: ".\u53F0\u5317Ta\u0301ibe\u030Ci.\u53F0\u5317", expect: ["class-span1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class selector, matching element with class with escaped character", selector: ".foo\\:bar", expect: ["class-span3"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Class selector, matching element with class with escaped character", selector: ".test\\.foo\\[5\\]bar", expect: ["class-span4"], level: 1, testType: TEST_QSA | TEST_MATCH}, + + // ID Selectors + {name: "ID selector, matching element with specified id", selector: "#id #id-div1", expect: ["id-div1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "ID selector, chained, matching element with specified id", selector: "#id-div1, #id-div1", expect: ["id-div1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "ID selector, chained, matching element with specified id", selector: "#id-div1, #id-div2", expect: ["id-div1", "id-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "ID Selector, chained, with type selector", selector: "div#id-div1, div#id-div2", expect: ["id-div1", "id-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "ID selector, not matching non-existent descendant", selector: "#id #none", expect: [] /*no matches*/, level: 1, testType: TEST_QSA}, + {name: "ID selector, not matching non-existent ancestor", selector: "#none #id-div1", expect: [] /*no matches*/, level: 1, testType: TEST_QSA}, + {name: "ID selector, matching multiple elements with duplicate id", selector: "#id-li-duplicate", expect: ["id-li-duplicate", "id-li-duplicate", "id-li-duplicate", "id-li-duplicate"], level: 1, testType: TEST_QSA | TEST_MATCH}, + + {name: "ID selector, matching id value using non-ASCII characters (1)", selector: "#\u53F0\u5317Ta\u0301ibe\u030Ci", expect: ["\u53F0\u5317Ta\u0301ibe\u030Ci"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "ID selector, matching id value using non-ASCII characters (2)", selector: "#\u53F0\u5317", expect: ["\u53F0\u5317"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "ID selector, matching id values using non-ASCII characters (1)", selector: "#\u53F0\u5317Ta\u0301ibe\u030Ci, #\u53F0\u5317", expect: ["\u53F0\u5317Ta\u0301ibe\u030Ci", "\u53F0\u5317"], level: 1, testType: TEST_QSA | TEST_MATCH}, + + // XXX runMatchesTest() in level2-lib.js can't handle this because obtaining the expected nodes requires escaping characters when generating the selector from 'expect' values + {name: "ID selector, matching element with id with escaped character", selector: "#\\#foo\\:bar", expect: ["#foo:bar"], level: 1, testType: TEST_QSA}, + {name: "ID selector, matching element with id with escaped character", selector: "#test\\.foo\\[5\\]bar", expect: ["test.foo[5]bar"], level: 1, testType: TEST_QSA}, + + // Namespaces + // XXX runMatchesTest() in level2-lib.js can't handle these because non-HTML elements don't have a recognised id + {name: "Namespace selector, matching element with any namespace", selector: "#any-namespace *|div", expect: ["any-namespace-div1", "any-namespace-div2", "any-namespace-div3", "any-namespace-div4"], level: 3, testType: TEST_QSA}, + {name: "Namespace selector, matching div elements in no namespace only", selector: "#no-namespace |div", expect: ["no-namespace-div3"], level: 3, testType: TEST_QSA}, + {name: "Namespace selector, matching any elements in no namespace only", selector: "#no-namespace |*", expect: ["no-namespace-div3"], level: 3, testType: TEST_QSA}, + + // Combinators + // - Descendant combinator ' ' + {name: "Descendant combinator, matching element that is a descendant of an element with id", selector: "#descendant div", expect: ["descendant-div1", "descendant-div2", "descendant-div3", "descendant-div4"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Descendant combinator, matching element with id that is a descendant of an element", selector: "body #descendant-div1", expect: ["descendant-div1"], exclude: ["detached", "fragment"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Descendant combinator, matching element with id that is a descendant of an element", selector: "div #descendant-div1", expect: ["descendant-div1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Descendant combinator, matching element with id that is a descendant of an element with id", selector: "#descendant #descendant-div2", expect: ["descendant-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Descendant combinator, matching element with class that is a descendant of an element with id", selector: "#descendant .descendant-div2", expect: ["descendant-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Descendant combinator, matching element with class that is a descendant of an element with class", selector: ".descendant-div1 .descendant-div3", expect: ["descendant-div3"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Descendant combinator, not matching element with id that is not a descendant of an element with id", selector: "#descendant-div1 #descendant-div4", expect: [] /*no matches*/, level: 1, testType: TEST_QSA}, + {name: "Descendant combinator, whitespace characters", selector: "#descendant\t\r\n#descendant-div2", expect: ["descendant-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + + /* The future of this combinator is uncertain, see + * https://github.com/w3c/csswg-drafts/issues/641 + * These tests are commented out until a final decision is made on whether to + * keep the feature in the spec. + */ + + // // - Descendant combinator '>>' + // {name: "Descendant combinator '>>', matching element that is a descendant of an element with id", selector: "#descendant>>div", expect: ["descendant-div1", "descendant-div2", "descendant-div3", "descendant-div4"], level: 1, testType: TEST_QSA | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with id that is a descendant of an element", selector: "body>>#descendant-div1", expect: ["descendant-div1"], exclude: ["detached", "fragment"], level: 1, testType: TEST_QSA | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with id that is a descendant of an element", selector: "div>>#descendant-div1", expect: ["descendant-div1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with id that is a descendant of an element with id", selector: "#descendant>>#descendant-div2", expect: ["descendant-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with class that is a descendant of an element with id", selector: "#descendant>>.descendant-div2", expect: ["descendant-div2"], level: 1, testType: TEST_QSA | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with class that is a descendant of an element with class", selector: ".descendant-div1>>.descendant-div3", expect: ["descendant-div3"], level: 1, testType: TEST_QSA | TEST_MATCH}, + // {name: "Descendant combinator '>>', not matching element with id that is not a descendant of an element with id", selector: "#descendant-div1>>#descendant-div4", expect: [] /*no matches*/, level: 1, testType: TEST_QSA}, + + // - Child combinator '>' + {name: "Child combinator, matching element that is a child of an element with id", selector: "#child>div", expect: ["child-div1", "child-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, matching element with id that is a child of an element", selector: "div>#child-div1", expect: ["child-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, matching element with id that is a child of an element with id", selector: "#child>#child-div1", expect: ["child-div1"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, matching element with id that is a child of an element with class", selector: "#child-div1>.child-div2", expect: ["child-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, matching element with class that is a child of an element with class", selector: ".child-div1>.child-div2", expect: ["child-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, not matching element with id that is not a child of an element with id", selector: "#child>#child-div3", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Child combinator, not matching element with id that is not a child of an element with class", selector: "#child-div1>.child-div3", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Child combinator, not matching element with class that is not a child of an element with class", selector: ".child-div1>.child-div3", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Child combinator, surrounded by whitespace", selector: "#child-div1\t\r\n>\t\r\n#child-div2", expect: ["child-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, whitespace after", selector: "#child-div1>\t\r\n#child-div2", expect: ["child-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, whitespace before", selector: "#child-div1\t\r\n>#child-div2", expect: ["child-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Child combinator, no whitespace", selector: "#child-div1>#child-div2", expect: ["child-div2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // - Adjacent sibling combinator '+' + {name: "Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id", selector: "#adjacent-div2+div", expect: ["adjacent-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element", selector: "div+#adjacent-div4", expect: ["adjacent-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id", selector: "#adjacent-div2+#adjacent-div4", expect: ["adjacent-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id", selector: "#adjacent-div2+.adjacent-div4", expect: ["adjacent-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class", selector: ".adjacent-div2+.adjacent-div4", expect: ["adjacent-div4"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element", selector: "#adjacent div+p", expect: ["adjacent-p2"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id", selector: "#adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1", expect: [] /*no matches*/, level: 2, testType: TEST_QSA}, + {name: "Adjacent sibling combinator, surrounded by whitespace", selector: "#adjacent-p2\t\r\n+\t\r\n#adjacent-p3", expect: ["adjacent-p3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, whitespace after", selector: "#adjacent-p2+\t\r\n#adjacent-p3", expect: ["adjacent-p3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, whitespace before", selector: "#adjacent-p2\t\r\n+#adjacent-p3", expect: ["adjacent-p3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + {name: "Adjacent sibling combinator, no whitespace", selector: "#adjacent-p2+#adjacent-p3", expect: ["adjacent-p3"], level: 2, testType: TEST_QSA | TEST_MATCH}, + + // - General sibling combinator ~ (Level 3) + {name: "General sibling combinator, matching element that is a sibling of an element with id", selector: "#sibling-div2~div", expect: ["sibling-div4", "sibling-div6"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, matching element with id that is a sibling of an element", selector: "div~#sibling-div4", expect: ["sibling-div4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, matching element with id that is a sibling of an element with id", selector: "#sibling-div2~#sibling-div4", expect: ["sibling-div4"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, matching element with class that is a sibling of an element with id", selector: "#sibling-div2~.sibling-div", expect: ["sibling-div4", "sibling-div6"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, matching p element that is a sibling of a div element", selector: "#sibling div~p", expect: ["sibling-p2", "sibling-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, not matching element with id that is not a sibling after a p element", selector: "#sibling>p~div", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "General sibling combinator, not matching element with id that is not a sibling after an element with id", selector: "#sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1", expect: [] /*no matches*/, level: 3, testType: TEST_QSA}, + {name: "General sibling combinator, surrounded by whitespace", selector: "#sibling-p2\t\r\n~\t\r\n#sibling-p3", expect: ["sibling-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, whitespace after", selector: "#sibling-p2~\t\r\n#sibling-p3", expect: ["sibling-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, whitespace before", selector: "#sibling-p2\t\r\n~#sibling-p3", expect: ["sibling-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + {name: "General sibling combinator, no whitespace", selector: "#sibling-p2~#sibling-p3", expect: ["sibling-p3"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // Group of selectors (comma) + {name: "Syntax, group of selectors separator, surrounded by whitespace", selector: "#group em\t\r \n,\t\r \n#group strong", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Syntax, group of selectors separator, whitespace after", selector: "#group em,\t\r\n#group strong", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Syntax, group of selectors separator, whitespace before", selector: "#group em\t\r\n,#group strong", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + {name: "Syntax, group of selectors separator, no whitespace", selector: "#group em,#group strong", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_QSA | TEST_MATCH}, + + // ::slotted (shouldn't match anything, but is a valid selector) + {name: "Slotted selector", selector: "::slotted(foo)", expect: [], level: 3, testType: TEST_QSA}, + {name: "Slotted selector (no matching closing paren)", selector: "::slotted(foo", expect: [], level: 3, testType: TEST_QSA}, +]; + + +/* + * These selectors are intended to be used with the find(), findAll() and matches() methods. Expected results + * should be determined under the assumption that :scope will be prepended to the selector where appropriate, + * in accordance with the specification. + * + * All of these should be valid relative selectors, expected to match zero or more elements in the document. + * None should throw any errors. + * + * name: A descriptive name of the selector being tested + * + * selector: The selector to test + * + * ctx: A selector to obtain the context object to use for tests invoking context.find(), + * and to use as a single reference node for tests invoking document.find(). + * Note: context = root.querySelector(ctx); + * + * ref: A selector to obtain the reference nodes to be used for the selector. + * Note: If root is the document or an in-document element: + * refNodes = document.querySelectorAll(ref); + * Otherwise, if root is a fragment or detached element: + * refNodes = root.querySelectorAll(ref); + * + * expect: A list of IDs of the elements expected to be matched. List must be given in tree order. + * + * unexpected: A list of IDs of some elements that are not expected to match the given selector. + * This is used to verify that unexpected.matches(selector, refNode) does not match. + * + * exclude: An array of contexts to exclude from testing. The valid values are: + * ["document", "element", "fragment", "detached", "html", "xhtml"] + * The "html" and "xhtml" values represent the type of document being queried. These are useful + * for tests that are affected by differences between HTML and XML, such as case sensitivity. + * + * level: An integer indicating the CSS or Selectors level in which the selector being tested was introduced. + * + * testType: A bit-mapped flag indicating the type of test. + * + * The test function for these tests accepts a specified root node, on which the methods will be invoked during the tests. + * + * Based on whether either 'context' or 'refNodes', or both, are specified the tests will execute the following methods: + * + * Where testType is TEST_FIND: + * + * context.findAll(selector, refNodes) + * context.findAll(selector) // Only if refNodes is not specified + * root.findAll(selector, context) // Only if refNodes is not specified + * root.findAll(selector, refNodes) // Only if context is not specified + * root.findAll(selector) // Only if neither context nor refNodes is specified + * + * Where testType is TEST_QSA + * + * context.querySelectorAll(selector) + * root.querySelectorAll(selector) // Only if neither context nor refNodes is specified + * + * Equivalent tests will be run for .find() as well. + * Note: Do not specify a testType of TEST_QSA where either implied :scope or explicit refNodes + * are required. + * + * Where testType is TEST_MATCH: + * For each expected result given, within the specified root: + * + * expect.matches(selector, context) // Only where refNodes is not specified + * expect.matches(selector, refNodes) + * expect.matches(selector) // Only if neither context nor refNodes is specified + * + * The tests involving refNodes for both find(), findAll() and matches() will each be run by passing the + * collection as a NodeList, an Array and, if there is only a single element, an Element node. + * + * Note: Interactive pseudo-classes (:active :hover and :focus) have not been tested in this test suite. + */ + +var scopedSelectors = [ + //{name: "", selector: "", ctx: "", ref: "", expect: [], level: 1, testType: TEST_FIND | TEST_MATCH}, + + // Attribute Selectors + // - presence [att] + {name: "Attribute presence selector, matching align attribute with value", selector: ".attr-presence-div1[align]", ctx: "#attr-presence", expect: ["attr-presence-div1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, matching align attribute with empty value", selector: ".attr-presence-div2[align]", ctx: "#attr-presence", expect: ["attr-presence-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, matching title attribute, case insensitivity", selector: "[TiTlE]", ctx: "#attr-presence", expect: ["attr-presence-a1", "attr-presence-span1"], exclude: ["xhtml"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, not matching title attribute, case sensitivity", selector: "[TiTlE]", ctx: "#attr-presence", expect: [], exclude: ["html"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, matching custom data-* attribute", selector: "[data-attr-presence]", ctx: "#attr-presence", expect: ["attr-presence-pre1", "attr-presence-blockquote1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, not matching attribute with similar name", selector: ".attr-presence-div3[align], .attr-presence-div4[align]", ctx: "#attr-presence", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Attribute presence selector, matching attribute with non-ASCII characters", selector: "ul[data-中文]", ctx: "#attr-presence", expect: ["attr-presence-ul1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, not matching default option without selected attribute", selector: "#attr-presence-select1 option[selected]", ctx: "#attr-presence", expect: [] /* no matches */, level: 2, testType: TEST_FIND}, + {name: "Attribute presence selector, matching option with selected attribute", selector: "#attr-presence-select2 option[selected]", ctx: "#attr-presence", expect: ["attr-presence-select2-option4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute presence selector, matching multiple options with selected attributes", selector: "#attr-presence-select3 option[selected]", ctx: "#attr-presence", expect: ["attr-presence-select3-option2", "attr-presence-select3-option3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + + // - value [att=val] + {name: "Attribute value selector, matching align attribute with value", selector: "[align=\"center\"]", ctx: "#attr-value", expect: ["attr-value-div1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector, matching align attribute with empty value", selector: "[align=\"\"]", ctx: "#attr-value", expect: ["attr-value-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector, not matching align attribute with partial value", selector: "[align=\"c\"]", ctx: "#attr-value", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Attribute value selector, not matching align attribute with incorrect value", selector: "[align=\"centera\"]", ctx: "#attr-value", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Attribute value selector, matching custom data-* attribute with unicode escaped value", selector: "[data-attr-value=\"\\e9\"]", ctx: "#attr-value", expect: ["attr-value-div3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector, matching custom data-* attribute with escaped character", selector: "[data-attr-value\_foo=\"\\e9\"]", ctx: "#attr-value", expect: ["attr-value-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector with single-quoted value, matching multiple inputs with type attributes", selector: "input[type='hidden'],#attr-value input[type='radio']", ctx: "#attr-value", expect: ["attr-value-input3", "attr-value-input4", "attr-value-input6", "attr-value-input8", "attr-value-input9"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector with double-quoted value, matching multiple inputs with type attributes", selector: "input[type=\"hidden\"],#attr-value input[type='radio']", ctx: "#attr-value", expect: ["attr-value-input3", "attr-value-input4", "attr-value-input6", "attr-value-input8", "attr-value-input9"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector with unquoted value, matching multiple inputs with type attributes", selector: "input[type=hidden],#attr-value input[type=radio]", ctx: "#attr-value", expect: ["attr-value-input3", "attr-value-input4", "attr-value-input6", "attr-value-input8", "attr-value-input9"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute value selector, matching attribute with value using non-ASCII characters", selector: "[data-attr-value=中文]", ctx: "#attr-value", expect: ["attr-value-div5"], level: 2, testType: TEST_FIND | TEST_MATCH}, + + // - whitespace-separated list [att~=val] + {name: "Attribute whitespace-separated list selector, matching class attribute with value", selector: "[class~=\"div1\"]", ctx: "#attr-whitespace", expect: ["attr-whitespace-div1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector, not matching class attribute with empty value", selector: "[class~=\"\"]", ctx: "#attr-whitespace", expect: [] /*no matches*/ , level: 2, testType: TEST_FIND}, + {name: "Attribute whitespace-separated list selector, not matching class attribute with partial value", selector: "[data-attr-whitespace~=\"div\"]", ctx: "#attr-whitespace", expect: [] /*no matches*/ , level: 2, testType: TEST_FIND}, + {name: "Attribute whitespace-separated list selector, matching custom data-* attribute with unicode escaped value", selector: "[data-attr-whitespace~=\"\\0000e9\"]", ctx: "#attr-whitespace", expect: ["attr-whitespace-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector, matching custom data-* attribute with escaped character", selector: "[data-attr-whitespace\_foo~=\"\\e9\"]", ctx: "#attr-whitespace", expect: ["attr-whitespace-div5"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with single-quoted value, matching multiple links with rel attributes", selector: "a[rel~='bookmark'], #attr-whitespace a[rel~='nofollow']", ctx: "#attr-whitespace", expect: ["attr-whitespace-a1", "attr-whitespace-a2", "attr-whitespace-a3", "attr-whitespace-a5", "attr-whitespace-a7"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with double-quoted value, matching multiple links with rel attributes", selector: "a[rel~=\"bookmark\"],#attr-whitespace a[rel~='nofollow']", ctx: "#attr-whitespace", expect: ["attr-whitespace-a1", "attr-whitespace-a2", "attr-whitespace-a3", "attr-whitespace-a5", "attr-whitespace-a7"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with unquoted value, matching multiple links with rel attributes", selector: "a[rel~=bookmark], #attr-whitespace a[rel~=nofollow]", ctx: "#attr-whitespace", expect: ["attr-whitespace-a1", "attr-whitespace-a2", "attr-whitespace-a3", "attr-whitespace-a5", "attr-whitespace-a7"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute whitespace-separated list selector with double-quoted value, not matching value with space", selector: "a[rel~=\"book mark\"]", ctx: "#attr-whitespace", expect: [] /* no matches */, level: 2, testType: TEST_FIND}, + {name: "Attribute whitespace-separated list selector, matching title attribute with value using non-ASCII characters", selector: "[title~=中文]", ctx: "#attr-whitespace", expect: ["attr-whitespace-p1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + + // - hyphen-separated list [att|=val] + {name: "Attribute hyphen-separated list selector, not matching unspecified lang attribute", selector: "#attr-hyphen-div1[lang|=\"en\"]", ctx: "#attr-hyphen", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Attribute hyphen-separated list selector, matching lang attribute with exact value", selector: "#attr-hyphen-div2[lang|=\"fr\"]", ctx: "#attr-hyphen", expect: ["attr-hyphen-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute hyphen-separated list selector, matching lang attribute with partial value", selector: "#attr-hyphen-div3[lang|=\"en\"]", ctx: "#attr-hyphen", expect: ["attr-hyphen-div3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute hyphen-separated list selector, not matching incorrect value", selector: "#attr-hyphen-div4[lang|=\"es-AR\"]", ctx: "#attr-hyphen", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + + // - substring begins-with [att^=val] (Level 3) + {name: "Attribute begins with selector, matching href attributes beginning with specified substring", selector: "a[href^=\"http://www\"]", ctx: "#attr-begins", expect: ["attr-begins-a1", "attr-begins-a3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute begins with selector, matching lang attributes beginning with specified substring, ", selector: "[lang^=\"en-\"]", ctx: "#attr-begins", expect: ["attr-begins-div2", "attr-begins-div4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute begins with selector, not matching class attribute with empty value", selector: "[class^=\"\"]", ctx: "#attr-begins", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "Attribute begins with selector, not matching class attribute not beginning with specified substring", selector: "[class^=apple]", ctx: "#attr-begins", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "Attribute begins with selector with single-quoted value, matching class attribute beginning with specified substring", selector: "[class^=' apple']", ctx: "#attr-begins", expect: ["attr-begins-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute begins with selector with double-quoted value, matching class attribute beginning with specified substring", selector: "[class^=\" apple\"]", ctx: "#attr-begins", expect: ["attr-begins-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute begins with selector with unquoted value, not matching class attribute not beginning with specified substring", selector: "[class^= apple]", ctx: "#attr-begins", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // - substring ends-with [att$=val] (Level 3) + {name: "Attribute ends with selector, matching href attributes ending with specified substring", selector: "a[href$=\".org\"]", ctx: "#attr-ends", expect: ["attr-ends-a1", "attr-ends-a3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute ends with selector, matching lang attributes ending with specified substring, ", selector: "[lang$=\"-CH\"]", ctx: "#attr-ends", expect: ["attr-ends-div2", "attr-ends-div4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute ends with selector, not matching class attribute with empty value", selector: "[class$=\"\"]", ctx: "#attr-ends", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "Attribute ends with selector, not matching class attribute not ending with specified substring", selector: "[class$=apple]", ctx: "#attr-ends", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "Attribute ends with selector with single-quoted value, matching class attribute ending with specified substring", selector: "[class$='apple ']", ctx: "#attr-ends", expect: ["attr-ends-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute ends with selector with double-quoted value, matching class attribute ending with specified substring", selector: "[class$=\"apple \"]", ctx: "#attr-ends", expect: ["attr-ends-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute ends with selector with unquoted value, not matching class attribute not ending with specified substring", selector: "[class$=apple ]", ctx: "#attr-ends", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // - substring contains [att*=val] (Level 3) + {name: "Attribute contains selector, matching href attributes beginning with specified substring", selector: "a[href*=\"http://www\"]", ctx: "#attr-contains", expect: ["attr-contains-a1", "attr-contains-a3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector, matching href attributes ending with specified substring", selector: "a[href*=\".org\"]", ctx: "#attr-contains", expect: ["attr-contains-a1", "attr-contains-a2"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector, matching href attributes containing specified substring", selector: "a[href*=\".example.\"]", ctx: "#attr-contains", expect: ["attr-contains-a1", "attr-contains-a3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector, matching lang attributes beginning with specified substring, ", selector: "[lang*=\"en-\"]", ctx: "#attr-contains", expect: ["attr-contains-div2", "attr-contains-div6"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector, matching lang attributes ending with specified substring, ", selector: "[lang*=\"-CH\"]", ctx: "#attr-contains", expect: ["attr-contains-div3", "attr-contains-div5"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector, not matching class attribute with empty value", selector: "[class*=\"\"]", ctx: "#attr-contains", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "Attribute contains selector with single-quoted value, matching class attribute beginning with specified substring", selector: "[class*=' apple']", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with single-quoted value, matching class attribute ending with specified substring", selector: "[class*='orange ']", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with single-quoted value, matching class attribute containing specified substring", selector: "[class*='ple banana ora']", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with double-quoted value, matching class attribute beginning with specified substring", selector: "[class*=\" apple\"]", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with double-quoted value, matching class attribute ending with specified substring", selector: "[class*=\"orange \"]", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with double-quoted value, matching class attribute containing specified substring", selector: "[class*=\"ple banana ora\"]", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with unquoted value, matching class attribute beginning with specified substring", selector: "[class*= apple]", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with unquoted value, matching class attribute ending with specified substring", selector: "[class*=orange ]", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "Attribute contains selector with unquoted value, matching class attribute containing specified substring", selector: "[class*= banana ]", ctx: "#attr-contains", expect: ["attr-contains-p1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // Pseudo-classes + // - :root (Level 3) + {name: ":root pseudo-class selector, matching document root element", selector: ":root", expect: ["html"], exclude: ["element", "fragment", "detached"], level: 3, testType: TEST_FIND}, + {name: ":root pseudo-class selector, not matching document root element", selector: ":root", expect: [] /*no matches*/, exclude: ["document"], level: 3, testType: TEST_FIND}, + {name: ":root pseudo-class selector, not matching document root element", selector: ":root", ctx: "#html", expect: [] /*no matches*/, exclude: ["fragment", "detached"], level: 3, testType: TEST_FIND}, + + // - :nth-child(n) (Level 3) + {name: ":nth-child selector, matching the third child element", selector: ":nth-child(3)", ctx: "#pseudo-nth-table1", expect: ["pseudo-nth-td3", "pseudo-nth-td9", "pseudo-nth-tr3", "pseudo-nth-td15"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-child selector, matching every third child element", selector: "li:nth-child(3n)", ctx: "#pseudo-nth", expect: ["pseudo-nth-li3", "pseudo-nth-li6", "pseudo-nth-li9", "pseudo-nth-li12"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-child selector, matching every second child element, starting from the fourth", selector: "li:nth-child(2n+4)", ctx: "#pseudo-nth", expect: ["pseudo-nth-li4", "pseudo-nth-li6", "pseudo-nth-li8", "pseudo-nth-li10", "pseudo-nth-li12"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-child selector, matching every second child element, starting from the fourth, with whitespace", selector: "li:nth-child(2n \t\r\n+ \t\r\n4)", ctx: "#pseudo-nth", expect: ["pseudo-nth-li4", "pseudo-nth-li6", "pseudo-nth-li8", "pseudo-nth-li10", "pseudo-nth-li12"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-child selector, matching every fourth child element, starting from the third", selector: ":nth-child(4n-1)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em2", "pseudo-nth-span3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-child selector, matching every fourth child element, starting from the third, with whitespace", selector: ":nth-child(4n \t\r\n- \t\r\n1)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em2", "pseudo-nth-span3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-child selector used twice, matching ", selector: ":nth-child(1) :nth-child(1)", ctx: "#pseudo-nth", expect: ["pseudo-nth-table1", "pseudo-nth-tr1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :nth-last-child (Level 3) + {name: ":nth-last-child selector, matching the third last child element", selector: ":nth-last-child(3)", ctx: "#pseudo-nth-table1", expect: ["pseudo-nth-tr1", "pseudo-nth-td4", "pseudo-nth-td10", "pseudo-nth-td16"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-last-child selector, matching every third child element from the end", selector: "li:nth-last-child(3n)", ctx: "pseudo-nth", expect: ["pseudo-nth-li1", "pseudo-nth-li4", "pseudo-nth-li7", "pseudo-nth-li10"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-last-child selector, matching every second child element from the end, starting from the fourth last", selector: "li:nth-last-child(2n+4)", ctx: "pseudo-nth", expect: ["pseudo-nth-li1", "pseudo-nth-li3", "pseudo-nth-li5", "pseudo-nth-li7", "pseudo-nth-li9"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-last-child selector, matching every fourth element from the end, starting from the third last", selector: ":nth-last-child(4n-1)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-span2", "pseudo-nth-span4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :nth-of-type(n) (Level 3) + {name: ":nth-of-type selector, matching the third em element", selector: "em:nth-of-type(3)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-of-type selector, matching every second element of their type", selector: ":nth-of-type(2n)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em2", "pseudo-nth-span2", "pseudo-nth-span4", "pseudo-nth-strong2", "pseudo-nth-em4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-of-type selector, matching every second elemetn of their type, starting from the first", selector: "span:nth-of-type(2n-1)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-span1", "pseudo-nth-span3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :nth-last-of-type(n) (Level 3) + {name: ":nth-last-of-type selector, matching the third last em element", selector: "em:nth-last-of-type(3)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em2"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-last-of-type selector, matching every second last element of their type", selector: ":nth-last-of-type(2n)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-span1", "pseudo-nth-em1", "pseudo-nth-strong1", "pseudo-nth-em3", "pseudo-nth-span3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":nth-last-of-type selector, matching every second last element of their type, starting from the last", selector: "span:nth-last-of-type(2n-1)", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-span2", "pseudo-nth-span4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :first-of-type (Level 3) + {name: ":first-of-type selector, matching the first em element", selector: "em:first-of-type", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":first-of-type selector, matching the first of every type of element", selector: ":first-of-type", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-span1", "pseudo-nth-em1", "pseudo-nth-strong1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":first-of-type selector, matching the first td element in each table row", selector: "tr :first-of-type", ctx: "#pseudo-nth-table1", expect: ["pseudo-nth-td1", "pseudo-nth-td7", "pseudo-nth-td13"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :last-of-type (Level 3) + {name: ":last-of-type selector, matching the last em elemnet", selector: "em:last-of-type", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-em4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":last-of-type selector, matching the last of every type of element", selector: ":last-of-type", ctx: "#pseudo-nth-p1", expect: ["pseudo-nth-span4", "pseudo-nth-strong2", "pseudo-nth-em4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":last-of-type selector, matching the last td element in each table row", selector: "tr :last-of-type", ctx: "#pseudo-nth-table1", expect: ["pseudo-nth-td6", "pseudo-nth-td12", "pseudo-nth-td18"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :first-child + {name: ":first-child pseudo-class selector, matching first child div element", selector: "div:first-child", ctx: "#pseudo-first-child", expect: ["pseudo-first-child-div1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: ":first-child pseudo-class selector, doesn't match non-first-child elements", selector: ".pseudo-first-child-div2:first-child, .pseudo-first-child-div3:first-child", ctx: "#pseudo-first-child", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: ":first-child pseudo-class selector, matching first-child of multiple elements", selector: "span:first-child", ctx: "#pseudo-first-child", expect: ["pseudo-first-child-span1", "pseudo-first-child-span3", "pseudo-first-child-span5"], level: 2, testType: TEST_FIND | TEST_MATCH}, + + // - :last-child (Level 3) + {name: ":last-child pseudo-class selector, matching last child div element", selector: "div:last-child", ctx: "#pseudo-last-child", expect: ["pseudo-last-child-div3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":last-child pseudo-class selector, doesn't match non-last-child elements", selector: ".pseudo-last-child-div1:last-child, .pseudo-last-child-div2:first-child", ctx: "#pseudo-last-child", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: ":last-child pseudo-class selector, matching first-child of multiple elements", selector: "span:last-child", ctx: "#pseudo-last-child", expect: ["pseudo-last-child-span2", "pseudo-last-child-span4", "pseudo-last-child-span6"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :only-child (Level 3) + {name: ":pseudo-only-child pseudo-class selector, matching all only-child elements", selector: ":only-child", ctx: "#pseudo-only", expect: ["pseudo-only-span1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":pseudo-only-child pseudo-class selector, matching only-child em elements", selector: "em:only-child", ctx: "#pseudo-only", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // - :only-of-type (Level 3) + {name: ":pseudo-only-of-type pseudo-class selector, matching all elements with no siblings of the same type", selector: " :only-of-type", ctx: "#pseudo-only", expect: ["pseudo-only-span1", "pseudo-only-em1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":pseudo-only-of-type pseudo-class selector, matching em elements with no siblings of the same type", selector: " em:only-of-type", ctx: "#pseudo-only", expect: ["pseudo-only-em1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :empty (Level 3) + {name: ":empty pseudo-class selector, matching empty p elements", selector: "p:empty", ctx: "#pseudo-empty", expect: ["pseudo-empty-p1", "pseudo-empty-p2"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":empty pseudo-class selector, matching all empty elements", selector: ":empty", ctx: "#pseudo-empty", expect: ["pseudo-empty-p1", "pseudo-empty-p2", "pseudo-empty-span1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :link and :visited + // Implementations may treat all visited links as unvisited, so these cannot be tested separately. + // The only guarantee is that ":link,:visited" matches the set of all visited and unvisited links and that they are individually mutually exclusive sets. + {name: ":link and :visited pseudo-class selectors, matching a and area elements with href attributes", selector: " :link, #pseudo-link :visited", ctx: "#pseudo-link", expect: ["pseudo-link-a1", "pseudo-link-a2", "pseudo-link-area1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: ":link and :visited pseudo-class selectors, matching no elements", selector: " :link, #head :visited", ctx: "#head", expect: [] /*no matches*/, exclude: ["element", "fragment", "detached"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: ":link and :visited pseudo-class selectors, not matching link elements with href attributes", selector: " :link, #head :visited", ctx: "#head", expect: [] /*no matches*/, exclude: ["document"], level: 1, testType: TEST_FIND}, + {name: ":link and :visited pseudo-class selectors, chained, mutually exclusive pseudo-classes match nothing", selector: ":link:visited", ctx: "#html", expect: [] /*no matches*/, exclude: ["document"], level: 1, testType: TEST_FIND}, + +// XXX Figure out context or refNodes for this + // - :target (Level 3) + {name: ":target pseudo-class selector, matching the element referenced by the URL fragment identifier", selector: ":target", ctx: "", expect: [] /*no matches*/, exclude: ["document", "element"], level: 3, testType: TEST_FIND}, + {name: ":target pseudo-class selector, matching the element referenced by the URL fragment identifier", selector: ":target", ctx: "", expect: ["target"], exclude: ["fragment", "detached"], level: 3, testType: TEST_FIND}, + +// XXX Fix ctx in tests below + + // - :lang() + {name: ":lang pseudo-class selector, matching inherited language (1)", selector: "#pseudo-lang-div1:lang(en)", ctx: "", expect: ["pseudo-lang-div1"], exclude: ["detached", "fragment"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: ":lang pseudo-class selector, not matching element with no inherited language", selector: "#pseudo-lang-div1:lang(en)", ctx: "", expect: [] /*no matches*/, exclude: ["document", "element"], level: 2, testType: TEST_FIND}, + {name: ":lang pseudo-class selector, matching specified language with exact value (1)", selector: "#pseudo-lang-div2:lang(fr)", ctx: "", expect: ["pseudo-lang-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: ":lang pseudo-class selector, matching specified language with partial value (1)", selector: "#pseudo-lang-div3:lang(en)", ctx: "", expect: ["pseudo-lang-div3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: ":lang pseudo-class selector, not matching incorrect language", selector: "#pseudo-lang-div4:lang(es-AR)", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + + // - :enabled (Level 3) + {name: ":enabled pseudo-class selector, matching all enabled form controls (1)", selector: "#pseudo-ui :enabled", ctx: "", expect: ["pseudo-ui-input1", "pseudo-ui-input2", "pseudo-ui-input3", "pseudo-ui-input4", "pseudo-ui-input5", "pseudo-ui-input6", + "pseudo-ui-input7", "pseudo-ui-input8", "pseudo-ui-input9", "pseudo-ui-textarea1", "pseudo-ui-button1"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":enabled pseudo-class selector, not matching link elements (1)", selector: "#pseudo-link :enabled", ctx: "", expect: [] /*no matches*/, unexpected: ["pseudo-link-a1","pseudo-link-a2","pseudo-link-a3","pseudo-link-map1","pseudo-link-area1","pseudo-link-area2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :disabled (Level 3) + {name: ":disabled pseudo-class selector, matching all disabled form controls (1)", selector: "#pseudo-ui :disabled", ctx: "", expect: ["pseudo-ui-input10", "pseudo-ui-input11", "pseudo-ui-input12", "pseudo-ui-input13", "pseudo-ui-input14", "pseudo-ui-input15", + "pseudo-ui-input16", "pseudo-ui-input17", "pseudo-ui-input18", "pseudo-ui-textarea2", "pseudo-ui-button2"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":disabled pseudo-class selector, not matching link elements (1)", selector: "#pseudo-link :disabled", ctx: "", expect: [] /*no matches*/, unexpected: ["pseudo-link-a1","pseudo-link-a2","pseudo-link-a3","pseudo-link-map1","pseudo-link-area1","pseudo-link-area2"], level: 3, testType: TEST_QSA | TEST_MATCH}, + + // - :checked (Level 3) + {name: ":checked pseudo-class selector, matching checked radio buttons and checkboxes (1)", selector: "#pseudo-ui :checked", ctx: "", expect: ["pseudo-ui-input4", "pseudo-ui-input6", "pseudo-ui-input13", "pseudo-ui-input15"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // - :not(s) (Level 3) + {name: ":not pseudo-class selector, matching (1)", selector: "#not>:not(div)", ctx: "", expect: ["not-p1", "not-p2", "not-p3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":not pseudo-class selector, matching (1)", selector: "#not * :not(:first-child)", ctx: "", expect: ["not-em1", "not-em2", "not-em3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: ":not pseudo-class selector, matching nothing", selector: ":not(*)", ctx: "", expect: [] /* no matches */, level: 3, testType: TEST_FIND}, + {name: ":not pseudo-class selector, matching nothing", selector: ":not(*|*)", ctx: "", expect: [] /* no matches */, level: 3, testType: TEST_FIND}, + + // Pseudo-elements + // - ::first-line + {name: ":first-line pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:first-line", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "::first-line pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::first-line", ctx: "", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // - ::first-letter + {name: ":first-letter pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:first-letter", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "::first-letter pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::first-letter", ctx: "", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // - ::before + {name: ":before pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:before", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "::before pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::before", ctx: "", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // - ::after + {name: ":after pseudo-element (one-colon syntax) selector, not matching any elements", selector: "#pseudo-element:after", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "::after pseudo-element (two-colon syntax) selector, not matching any elements", selector: "#pseudo-element::after", ctx: "", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + + // Class Selectors + {name: "Class selector, matching element with specified class (1)", selector: ".class-p", ctx: "", expect: ["class-p1","class-p2", "class-p3"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class selector, chained, matching only elements with all specified classes (1)", selector: "#class .apple.orange.banana", ctx: "", expect: ["class-div1", "class-div2", "class-p4", "class-div3", "class-p6", "class-div4"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class Selector, chained, with type selector (1)", selector: "div.apple.banana.orange", ctx: "", expect: ["class-div1", "class-div2", "class-div3", "class-div4"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class selector, matching element with class value using non-ASCII characters (2)", selector: ".\u53F0\u5317Ta\u0301ibe\u030Ci", ctx: "", expect: ["class-span1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class selector, matching multiple elements with class value using non-ASCII characters (1)", selector: ".\u53F0\u5317", ctx: "", expect: ["class-span1","class-span2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class selector, chained, matching element with multiple class values using non-ASCII characters (2)", selector: ".\u53F0\u5317Ta\u0301ibe\u030Ci.\u53F0\u5317", ctx: "", expect: ["class-span1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class selector, matching element with class with escaped character (1)", selector: ".foo\\:bar", ctx: "", expect: ["class-span3"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Class selector, matching element with class with escaped character (1)", selector: ".test\\.foo\\[5\\]bar", ctx: "", expect: ["class-span4"], level: 1, testType: TEST_FIND | TEST_MATCH}, + + // ID Selectors + {name: "ID selector, matching element with specified id (1)", selector: "#id #id-div1", ctx: "", expect: ["id-div1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "ID selector, chained, matching element with specified id (1)", selector: "#id-div1, #id-div1", ctx: "", expect: ["id-div1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "ID selector, chained, matching element with specified id (1)", selector: "#id-div1, #id-div2", ctx: "", expect: ["id-div1", "id-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "ID Selector, chained, with type selector (1)", selector: "div#id-div1, div#id-div2", ctx: "", expect: ["id-div1", "id-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "ID selector, not matching non-existent descendant", selector: "#id #none", ctx: "", expect: [] /*no matches*/, level: 1, testType: TEST_FIND}, + {name: "ID selector, not matching non-existent ancestor", selector: "#none #id-div1", ctx: "", expect: [] /*no matches*/, level: 1, testType: TEST_FIND}, + {name: "ID selector, matching multiple elements with duplicate id (1)", selector: "#id-li-duplicate", ctx: "", expect: ["id-li-duplicate", "id-li-duplicate", "id-li-duplicate", "id-li-duplicate"], level: 1, testType: TEST_FIND | TEST_MATCH}, + + {name: "ID selector, matching id value using non-ASCII characters (3)", selector: "#\u53F0\u5317Ta\u0301ibe\u030Ci", ctx: "", expect: ["\u53F0\u5317Ta\u0301ibe\u030Ci"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "ID selector, matching id value using non-ASCII characters (4)", selector: "#\u53F0\u5317", ctx: "", expect: ["\u53F0\u5317"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "ID selector, matching id values using non-ASCII characters (2)", selector: "#\u53F0\u5317Ta\u0301ibe\u030Ci, #\u53F0\u5317", ctx: "", expect: ["\u53F0\u5317Ta\u0301ibe\u030Ci", "\u53F0\u5317"], level: 1, testType: TEST_FIND | TEST_MATCH}, + + // XXX runMatchesTest() in level2-lib.js can't handle this because obtaining the expected nodes requires escaping characters when generating the selector from 'expect' values + {name: "ID selector, matching element with id with escaped character", selector: "#\\#foo\\:bar", ctx: "", expect: ["#foo:bar"], level: 1, testType: TEST_FIND}, + {name: "ID selector, matching element with id with escaped character", selector: "#test\\.foo\\[5\\]bar", ctx: "", expect: ["test.foo[5]bar"], level: 1, testType: TEST_FIND}, + + // Namespaces + // XXX runMatchesTest() in level2-lib.js can't handle these because non-HTML elements don't have a recognised id + {name: "Namespace selector, matching element with any namespace", selector: "#any-namespace *|div", ctx: "", expect: ["any-namespace-div1", "any-namespace-div2", "any-namespace-div3", "any-namespace-div4"], level: 3, testType: TEST_FIND}, + {name: "Namespace selector, matching div elements in no namespace only", selector: "#no-namespace |div", ctx: "", expect: ["no-namespace-div3"], level: 3, testType: TEST_FIND}, + {name: "Namespace selector, matching any elements in no namespace only", selector: "#no-namespace |*", ctx: "", expect: ["no-namespace-div3"], level: 3, testType: TEST_FIND}, + + // Combinators + // - Descendant combinator ' ' + {name: "Descendant combinator, matching element that is a descendant of an element with id (1)", selector: "#descendant div", ctx: "", expect: ["descendant-div1", "descendant-div2", "descendant-div3", "descendant-div4"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Descendant combinator, matching element with id that is a descendant of an element (1)", selector: "body #descendant-div1", ctx: "", expect: ["descendant-div1"], exclude: ["detached", "fragment"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Descendant combinator, matching element with id that is a descendant of an element (1)", selector: "div #descendant-div1", ctx: "", expect: ["descendant-div1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Descendant combinator, matching element with id that is a descendant of an element with id (1)", selector: "#descendant #descendant-div2", ctx: "", expect: ["descendant-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Descendant combinator, matching element with class that is a descendant of an element with id (1)", selector: "#descendant .descendant-div2", ctx: "", expect: ["descendant-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Descendant combinator, matching element with class that is a descendant of an element with class (1)", selector: ".descendant-div1 .descendant-div3", ctx: "", expect: ["descendant-div3"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Descendant combinator, not matching element with id that is not a descendant of an element with id", selector: "#descendant-div1 #descendant-div4", ctx: "", expect: [] /*no matches*/, level: 1, testType: TEST_FIND}, + {name: "Descendant combinator, whitespace characters (1)", selector: "#descendant\t\r\n#descendant-div2", ctx: "", expect: ["descendant-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + + // // - Descendant combinator '>>' + // {name: "Descendant combinator '>>', matching element that is a descendant of an element with id (1)", selector: "#descendant>>div", ctx: "", expect: ["descendant-div1", "descendant-div2", "descendant-div3", "descendant-div4"], level: 1, testType: TEST_FIND | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with id that is a descendant of an element (1)", selector: "body>>#descendant-div1", ctx: "", expect: ["descendant-div1"], exclude: ["detached", "fragment"], level: 1, testType: TEST_FIND | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with id that is a descendant of an element (1)", selector: "div>>#descendant-div1", ctx: "", expect: ["descendant-div1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with id that is a descendant of an element with id (1)", selector: "#descendant>>#descendant-div2", ctx: "", expect: ["descendant-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + // {name: "Descendant combinator '>>', matching element with class that is a descendant of an element with id (1)", selector: "#descendant>>.descendant-div2", ctx: "", expect: ["descendant-div2"], level: 1, testType: TEST_FIND | TEST_MATCH}, + // {name: "Descendant combinator, '>>', matching element with class that is a descendant of an element with class (1)", selector: ".descendant-div1>>.descendant-div3", ctx: "", expect: ["descendant-div3"], level: 1, testType: TEST_FIND | TEST_MATCH}, + // {name: "Descendant combinator '>>', not matching element with id that is not a descendant of an element with id", selector: "#descendant-div1>>#descendant-div4", ctx: "", expect: [] /*no matches*/, level: 1, testType: TEST_FIND}, + + // - Child combinator '>' + {name: "Child combinator, matching element that is a child of an element with id (1)", selector: "#child>div", ctx: "", expect: ["child-div1", "child-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, matching element with id that is a child of an element (1)", selector: "div>#child-div1", ctx: "", expect: ["child-div1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, matching element with id that is a child of an element with id (1)", selector: "#child>#child-div1", ctx: "", expect: ["child-div1"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, matching element with id that is a child of an element with class (1)", selector: "#child-div1>.child-div2", ctx: "", expect: ["child-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, matching element with class that is a child of an element with class (1)", selector: ".child-div1>.child-div2", ctx: "", expect: ["child-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, not matching element with id that is not a child of an element with id", selector: "#child>#child-div3", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Child combinator, not matching element with id that is not a child of an element with class", selector: "#child-div1>.child-div3", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Child combinator, not matching element with class that is not a child of an element with class", selector: ".child-div1>.child-div3", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Child combinator, surrounded by whitespace (1)", selector: "#child-div1\t\r\n>\t\r\n#child-div2", ctx: "", expect: ["child-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, whitespace after (1)", selector: "#child-div1>\t\r\n#child-div2", ctx: "", expect: ["child-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, whitespace before (1)", selector: "#child-div1\t\r\n>#child-div2", ctx: "", expect: ["child-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Child combinator, no whitespace (1)", selector: "#child-div1>#child-div2", ctx: "", expect: ["child-div2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + + // - Adjacent sibling combinator '+' + {name: "Adjacent sibling combinator, matching element that is an adjacent sibling of an element with id (1)", selector: "#adjacent-div2+div", ctx: "", expect: ["adjacent-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element (1)", selector: "div+#adjacent-div4", ctx: "", expect: ["adjacent-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with id that is an adjacent sibling of an element with id (1)", selector: "#adjacent-div2+#adjacent-div4", ctx: "", expect: ["adjacent-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with id (1)", selector: "#adjacent-div2+.adjacent-div4", ctx: "", expect: ["adjacent-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching element with class that is an adjacent sibling of an element with class (1)", selector: ".adjacent-div2+.adjacent-div4", ctx: "", expect: ["adjacent-div4"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, matching p element that is an adjacent sibling of a div element (1)", selector: "#adjacent div+p", ctx: "", expect: ["adjacent-p2"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, not matching element with id that is not an adjacent sibling of an element with id", selector: "#adjacent-div2+#adjacent-p2, #adjacent-div2+#adjacent-div1", ctx: "", expect: [] /*no matches*/, level: 2, testType: TEST_FIND}, + {name: "Adjacent sibling combinator, surrounded by whitespace (1)", selector: "#adjacent-p2\t\r\n+\t\r\n#adjacent-p3", ctx: "", expect: ["adjacent-p3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, whitespace after (1)", selector: "#adjacent-p2+\t\r\n#adjacent-p3", ctx: "", expect: ["adjacent-p3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, whitespace before (1)", selector: "#adjacent-p2\t\r\n+#adjacent-p3", ctx: "", expect: ["adjacent-p3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + {name: "Adjacent sibling combinator, no whitespace (1)", selector: "#adjacent-p2+#adjacent-p3", ctx: "", expect: ["adjacent-p3"], level: 2, testType: TEST_FIND | TEST_MATCH}, + + // - General sibling combinator ~ (Level 3) + {name: "General sibling combinator, matching element that is a sibling of an element with id (1)", selector: "#sibling-div2~div", ctx: "", expect: ["sibling-div4", "sibling-div6"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, matching element with id that is a sibling of an element (1)", selector: "div~#sibling-div4", ctx: "", expect: ["sibling-div4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, matching element with id that is a sibling of an element with id (1)", selector: "#sibling-div2~#sibling-div4", ctx: "", expect: ["sibling-div4"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, matching element with class that is a sibling of an element with id (1)", selector: "#sibling-div2~.sibling-div", ctx: "", expect: ["sibling-div4", "sibling-div6"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, matching p element that is a sibling of a div element (1)", selector: "#sibling div~p", ctx: "", expect: ["sibling-p2", "sibling-p3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, not matching element with id that is not a sibling after a p element (1)", selector: "#sibling>p~div", ctx: "", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "General sibling combinator, not matching element with id that is not a sibling after an element with id", selector: "#sibling-div2~#sibling-div3, #sibling-div2~#sibling-div1", ctx: "", expect: [] /*no matches*/, level: 3, testType: TEST_FIND}, + {name: "General sibling combinator, surrounded by whitespace (1)", selector: "#sibling-p2\t\r\n~\t\r\n#sibling-p3", ctx: "", expect: ["sibling-p3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, whitespace after (1)", selector: "#sibling-p2~\t\r\n#sibling-p3", ctx: "", expect: ["sibling-p3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, whitespace before (1)", selector: "#sibling-p2\t\r\n~#sibling-p3", ctx: "", expect: ["sibling-p3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + {name: "General sibling combinator, no whitespace (1)", selector: "#sibling-p2~#sibling-p3", ctx: "", expect: ["sibling-p3"], level: 3, testType: TEST_FIND | TEST_MATCH}, + + // Group of selectors (comma) + {name: "Syntax, group of selectors separator, surrounded by whitespace (1)", selector: "#group em\t\r \n,\t\r \n#group strong", ctx: "", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Syntax, group of selectors separator, whitespace after (1)", selector: "#group em,\t\r\n#group strong", ctx: "", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Syntax, group of selectors separator, whitespace before (1)", selector: "#group em\t\r\n,#group strong", ctx: "", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_FIND | TEST_MATCH}, + {name: "Syntax, group of selectors separator, no whitespace (1)", selector: "#group em,#group strong", ctx: "", expect: ["group-em1", "group-strong1"], level: 1, testType: TEST_FIND | TEST_MATCH}, +]; From a2cf1d17fd5ecab72bd94639bf7c9cc5f2a3991c Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:58:18 +1100 Subject: [PATCH 019/397] LibWeb: Require CSS combinators to be followed by a simple selector --- .../LibWeb/CSS/Parser/SelectorParsing.cpp | 6 ++- .../nodes/ParentNode-querySelector-All.txt | 44 +++++++++---------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp index 724292cb64cf..8ac9ae304edd 100644 --- a/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp @@ -166,8 +166,12 @@ Parser::ParseErrorOr> Parser::parse_compoun simple_selectors.append(component.release_value()); } - if (simple_selectors.is_empty()) + if (simple_selectors.is_empty()) { + if (tokens.has_next_token() || combinator != Selector::Combinator::Descendant) + return ParseError::SyntaxError; + return Optional {}; + } return Selector::CompoundSelector { combinator, move(simple_selectors) }; } diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt index b0d3625ff6a5..9e626d0def98 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt @@ -6,8 +6,8 @@ Rerun Found 1975 tests -1903 Pass -72 Fail +1923 Pass +52 Fail Details Result Test Name MessagePass Selectors-API Test Suite: HTML Pass Document supports querySelector @@ -92,10 +92,10 @@ Pass Document.querySelector: Invalid class: .bar. Pass Document.querySelectorAll: Invalid class: .bar. Pass Document.querySelector: Invalid combinator: div % address, p Pass Document.querySelectorAll: Invalid combinator: div % address, p -Fail Document.querySelector: Invalid combinator: div ++ address, p -Fail Document.querySelectorAll: Invalid combinator: div ++ address, p -Fail Document.querySelector: Invalid combinator: div ~~ address, p -Fail Document.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Document.querySelector: Invalid combinator: div ++ address, p +Pass Document.querySelectorAll: Invalid combinator: div ++ address, p +Pass Document.querySelector: Invalid combinator: div ~~ address, p +Pass Document.querySelectorAll: Invalid combinator: div ~~ address, p Pass Document.querySelector: Invalid [att=value] selector: [*=test] Pass Document.querySelectorAll: Invalid [att=value] selector: [*=test] Pass Document.querySelector: Invalid [att=value] selector: [*|*=test] @@ -160,10 +160,10 @@ Pass Detached Element.querySelector: Invalid class: .bar. Pass Detached Element.querySelectorAll: Invalid class: .bar. Pass Detached Element.querySelector: Invalid combinator: div % address, p Pass Detached Element.querySelectorAll: Invalid combinator: div % address, p -Fail Detached Element.querySelector: Invalid combinator: div ++ address, p -Fail Detached Element.querySelectorAll: Invalid combinator: div ++ address, p -Fail Detached Element.querySelector: Invalid combinator: div ~~ address, p -Fail Detached Element.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Detached Element.querySelector: Invalid combinator: div ++ address, p +Pass Detached Element.querySelectorAll: Invalid combinator: div ++ address, p +Pass Detached Element.querySelector: Invalid combinator: div ~~ address, p +Pass Detached Element.querySelectorAll: Invalid combinator: div ~~ address, p Pass Detached Element.querySelector: Invalid [att=value] selector: [*=test] Pass Detached Element.querySelectorAll: Invalid [att=value] selector: [*=test] Pass Detached Element.querySelector: Invalid [att=value] selector: [*|*=test] @@ -228,10 +228,10 @@ Pass Fragment.querySelector: Invalid class: .bar. Pass Fragment.querySelectorAll: Invalid class: .bar. Pass Fragment.querySelector: Invalid combinator: div % address, p Pass Fragment.querySelectorAll: Invalid combinator: div % address, p -Fail Fragment.querySelector: Invalid combinator: div ++ address, p -Fail Fragment.querySelectorAll: Invalid combinator: div ++ address, p -Fail Fragment.querySelector: Invalid combinator: div ~~ address, p -Fail Fragment.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Fragment.querySelector: Invalid combinator: div ++ address, p +Pass Fragment.querySelectorAll: Invalid combinator: div ++ address, p +Pass Fragment.querySelector: Invalid combinator: div ~~ address, p +Pass Fragment.querySelectorAll: Invalid combinator: div ~~ address, p Pass Fragment.querySelector: Invalid [att=value] selector: [*=test] Pass Fragment.querySelectorAll: Invalid [att=value] selector: [*=test] Pass Fragment.querySelector: Invalid [att=value] selector: [*|*=test] @@ -296,10 +296,10 @@ Pass In-document Element.querySelector: Invalid class: .bar. Pass In-document Element.querySelectorAll: Invalid class: .bar. Pass In-document Element.querySelector: Invalid combinator: div % address, p Pass In-document Element.querySelectorAll: Invalid combinator: div % address, p -Fail In-document Element.querySelector: Invalid combinator: div ++ address, p -Fail In-document Element.querySelectorAll: Invalid combinator: div ++ address, p -Fail In-document Element.querySelector: Invalid combinator: div ~~ address, p -Fail In-document Element.querySelectorAll: Invalid combinator: div ~~ address, p +Pass In-document Element.querySelector: Invalid combinator: div ++ address, p +Pass In-document Element.querySelectorAll: Invalid combinator: div ++ address, p +Pass In-document Element.querySelector: Invalid combinator: div ~~ address, p +Pass In-document Element.querySelectorAll: Invalid combinator: div ~~ address, p Pass In-document Element.querySelector: Invalid [att=value] selector: [*=test] Pass In-document Element.querySelectorAll: Invalid [att=value] selector: [*=test] Pass In-document Element.querySelector: Invalid [att=value] selector: [*|*=test] @@ -364,10 +364,10 @@ Pass Empty Element.querySelector: Invalid class: .bar. Pass Empty Element.querySelectorAll: Invalid class: .bar. Pass Empty Element.querySelector: Invalid combinator: div % address, p Pass Empty Element.querySelectorAll: Invalid combinator: div % address, p -Fail Empty Element.querySelector: Invalid combinator: div ++ address, p -Fail Empty Element.querySelectorAll: Invalid combinator: div ++ address, p -Fail Empty Element.querySelector: Invalid combinator: div ~~ address, p -Fail Empty Element.querySelectorAll: Invalid combinator: div ~~ address, p +Pass Empty Element.querySelector: Invalid combinator: div ++ address, p +Pass Empty Element.querySelectorAll: Invalid combinator: div ++ address, p +Pass Empty Element.querySelector: Invalid combinator: div ~~ address, p +Pass Empty Element.querySelectorAll: Invalid combinator: div ~~ address, p Pass Empty Element.querySelector: Invalid [att=value] selector: [*=test] Pass Empty Element.querySelectorAll: Invalid [att=value] selector: [*=test] Pass Empty Element.querySelector: Invalid [att=value] selector: [*|*=test] From ba0cc7fe4619e563aefe65948d2508c8df6ca26f Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:43:10 +1100 Subject: [PATCH 020/397] LibWeb: Use correct case-sensitivity when matching attribute selectors Also removed get_attribute_with_lowercase_qualified_name because it was buggy, duplicated logic, and now unused. --- Libraries/LibWeb/CSS/SelectorEngine.cpp | 3 +-- Libraries/LibWeb/DOM/NamedNodeMap.cpp | 16 +-------------- Libraries/LibWeb/DOM/NamedNodeMap.h | 2 -- .../nodes/ParentNode-querySelector-All.txt | 20 +++++++++---------- 4 files changed, 12 insertions(+), 29 deletions(-) diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index cda81207f42f..6b1627e2ed39 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -219,8 +219,7 @@ static inline bool matches_attribute(CSS::Selector::SimpleSelector::Attribute co auto const& attribute_name = attribute.qualified_name.name.name; - auto const* attr = element.namespace_uri() == Namespace::HTML ? element.attributes()->get_attribute_with_lowercase_qualified_name(attribute_name) - : element.attributes()->get_attribute(attribute_name); + auto const* attr = element.attributes()->get_attribute(attribute_name); if (attribute.match_type == CSS::Selector::SimpleSelector::Attribute::MatchType::HasAttribute) { // Early way out in case of an attribute existence selector. diff --git a/Libraries/LibWeb/DOM/NamedNodeMap.cpp b/Libraries/LibWeb/DOM/NamedNodeMap.cpp index 11ed46345cf9..237538ca5543 100644 --- a/Libraries/LibWeb/DOM/NamedNodeMap.cpp +++ b/Libraries/LibWeb/DOM/NamedNodeMap.cpp @@ -148,8 +148,7 @@ Attr const* NamedNodeMap::get_attribute(FlyString const& qualified_name, size_t* *item_index = 0; // 1. If element is in the HTML namespace and its node document is an HTML document, then set qualifiedName to qualifiedName in ASCII lowercase. - // FIXME: Handle the second condition, assume it is an HTML document for now. - bool compare_as_lowercase = associated_element().namespace_uri() == Namespace::HTML; + bool compare_as_lowercase = associated_element().namespace_uri() == Namespace::HTML && associated_element().document().is_html_document(); // 2. Return the first attribute in element’s attribute list whose qualified name is qualifiedName; otherwise null. for (auto const& attribute : m_attributes) { @@ -168,19 +167,6 @@ Attr const* NamedNodeMap::get_attribute(FlyString const& qualified_name, size_t* return nullptr; } -Attr const* NamedNodeMap::get_attribute_with_lowercase_qualified_name(FlyString const& lowercase_qualified_name) const -{ - bool compare_as_lowercase = associated_element().namespace_uri() == Namespace::HTML; - VERIFY(compare_as_lowercase); - - for (auto const& attribute : m_attributes) { - if (attribute->lowercase_name() == lowercase_qualified_name) - return attribute; - } - - return nullptr; -} - // https://dom.spec.whatwg.org/#concept-element-attributes-get-by-namespace Attr* NamedNodeMap::get_attribute_ns(Optional const& namespace_, FlyString const& local_name, size_t* item_index) { diff --git a/Libraries/LibWeb/DOM/NamedNodeMap.h b/Libraries/LibWeb/DOM/NamedNodeMap.h index 27052935f525..bf59c3a13db5 100644 --- a/Libraries/LibWeb/DOM/NamedNodeMap.h +++ b/Libraries/LibWeb/DOM/NamedNodeMap.h @@ -53,8 +53,6 @@ class NamedNodeMap : public Bindings::PlatformObject { Attr const* remove_attribute(FlyString const& qualified_name); Attr const* remove_attribute_ns(Optional const& namespace_, FlyString const& local_name); - Attr const* get_attribute_with_lowercase_qualified_name(FlyString const&) const; - WebIDL::ExceptionOr> remove_attribute_node(GC::Ref); private: diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt index 9e626d0def98..29487d7992ae 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt @@ -6,8 +6,8 @@ Rerun Found 1975 tests -1923 Pass -52 Fail +1931 Pass +44 Fail Details Result Test Name MessagePass Selectors-API Test Suite: HTML Pass Document supports querySelector @@ -414,8 +414,8 @@ Pass Document.querySelectorAll: Attribute presence selector, matching align attr Pass Document.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] Pass Document.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] Pass Document.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] -Fail Document.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] -Fail Document.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Document.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Document.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] Pass Document.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass Document.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass Document.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] @@ -810,8 +810,8 @@ Pass Detached Element.querySelectorAll: Attribute presence selector, matching al Pass Detached Element.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] Pass Detached Element.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] Pass Detached Element.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] -Fail Detached Element.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] -Fail Detached Element.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Detached Element.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Detached Element.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] Pass Detached Element.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass Detached Element.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass Detached Element.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] @@ -1206,8 +1206,8 @@ Pass Fragment.querySelectorAll: Attribute presence selector, matching align attr Pass Fragment.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] Pass Fragment.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] Pass Fragment.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] -Fail Fragment.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] -Fail Fragment.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Fragment.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass Fragment.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] Pass Fragment.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass Fragment.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass Fragment.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] @@ -1602,8 +1602,8 @@ Pass In-document Element.querySelectorAll: Attribute presence selector, matching Pass In-document Element.querySelector: Attribute presence selector, matching align attribute with value: .attr-presence-div1[align] Pass In-document Element.querySelectorAll: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] Pass In-document Element.querySelector: Attribute presence selector, matching align attribute with empty value: .attr-presence-div2[align] -Fail In-document Element.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] -Fail In-document Element.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass In-document Element.querySelectorAll: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] +Pass In-document Element.querySelector: Attribute presence selector, matching title attribute, case insensitivity: #attr-presence [*|TiTlE] Pass In-document Element.querySelectorAll: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass In-document Element.querySelector: Attribute presence selector, matching custom data-* attribute: [data-attr-presence] Pass In-document Element.querySelectorAll: Attribute presence selector, not matching attribute with similar name: .attr-presence-div3[align], .attr-presence-div4[align] From bb678e75f9dd04166b4bf893dfb8832e2683c5cf Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:31:39 +1100 Subject: [PATCH 021/397] LibWeb: Reject selectors with named namespaces in querySelectorAll --- Libraries/LibWeb/DOM/ParentNode.cpp | 24 ++++++++++ .../nodes/ParentNode-querySelector-All.txt | 44 +++++++++---------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/Libraries/LibWeb/DOM/ParentNode.cpp b/Libraries/LibWeb/DOM/ParentNode.cpp index 1932c78c9b80..240e12175e11 100644 --- a/Libraries/LibWeb/DOM/ParentNode.cpp +++ b/Libraries/LibWeb/DOM/ParentNode.cpp @@ -23,6 +23,26 @@ namespace Web::DOM { GC_DEFINE_ALLOCATOR(ParentNode); +static bool contains_named_namespace(const CSS::SelectorList& selectors) +{ + for (auto const& selector : selectors) { + for (auto const& compound_selector : selector->compound_selectors()) { + for (auto simple_selector : compound_selector.simple_selectors) { + if (simple_selector.value.has()) { + if (simple_selector.qualified_name().namespace_type == CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named) + return true; + } + + if (simple_selector.value.has()) { + if (contains_named_namespace(simple_selector.pseudo_class().argument_selector_list)) + return true; + } + } + } + } + return false; +} + enum class ReturnMatches { First, All, @@ -40,6 +60,10 @@ static WebIDL::ExceptionOr, GC::Ref>> scope_m auto selectors = maybe_selectors.value(); + // "Note: Support for namespaces within selectors is not planned and will not be added." + if (contains_named_namespace(selectors)) + return WebIDL::SyntaxError::create(node.realm(), "Failed to parse selector"_string); + // 3. Return the result of match a selector against a tree with s and node’s root using scoping root node. GC::Ptr single_result; Vector> results; diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt index 29487d7992ae..34ace2711f5c 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt @@ -6,8 +6,8 @@ Rerun Found 1975 tests -1931 Pass -44 Fail +1951 Pass +24 Fail Details Result Test Name MessagePass Selectors-API Test Suite: HTML Pass Document supports querySelector @@ -116,10 +116,10 @@ Pass Document.querySelector: Invalid pseudo-element: :::before Pass Document.querySelectorAll: Invalid pseudo-element: :::before Pass Document.querySelector: Invalid pseudo-element: :: before Pass Document.querySelectorAll: Invalid pseudo-element: :: before -Fail Document.querySelector: Undeclared namespace: ns|div -Fail Document.querySelectorAll: Undeclared namespace: ns|div -Fail Document.querySelector: Undeclared namespace: :not(ns|div) -Fail Document.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Document.querySelector: Undeclared namespace: ns|div +Pass Document.querySelectorAll: Undeclared namespace: ns|div +Pass Document.querySelector: Undeclared namespace: :not(ns|div) +Pass Document.querySelectorAll: Undeclared namespace: :not(ns|div) Pass Document.querySelector: Invalid namespace: ^|div Pass Document.querySelectorAll: Invalid namespace: ^|div Pass Document.querySelector: Invalid namespace: $|div @@ -184,10 +184,10 @@ Pass Detached Element.querySelector: Invalid pseudo-element: :::before Pass Detached Element.querySelectorAll: Invalid pseudo-element: :::before Pass Detached Element.querySelector: Invalid pseudo-element: :: before Pass Detached Element.querySelectorAll: Invalid pseudo-element: :: before -Fail Detached Element.querySelector: Undeclared namespace: ns|div -Fail Detached Element.querySelectorAll: Undeclared namespace: ns|div -Fail Detached Element.querySelector: Undeclared namespace: :not(ns|div) -Fail Detached Element.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Detached Element.querySelector: Undeclared namespace: ns|div +Pass Detached Element.querySelectorAll: Undeclared namespace: ns|div +Pass Detached Element.querySelector: Undeclared namespace: :not(ns|div) +Pass Detached Element.querySelectorAll: Undeclared namespace: :not(ns|div) Pass Detached Element.querySelector: Invalid namespace: ^|div Pass Detached Element.querySelectorAll: Invalid namespace: ^|div Pass Detached Element.querySelector: Invalid namespace: $|div @@ -252,10 +252,10 @@ Pass Fragment.querySelector: Invalid pseudo-element: :::before Pass Fragment.querySelectorAll: Invalid pseudo-element: :::before Pass Fragment.querySelector: Invalid pseudo-element: :: before Pass Fragment.querySelectorAll: Invalid pseudo-element: :: before -Fail Fragment.querySelector: Undeclared namespace: ns|div -Fail Fragment.querySelectorAll: Undeclared namespace: ns|div -Fail Fragment.querySelector: Undeclared namespace: :not(ns|div) -Fail Fragment.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Fragment.querySelector: Undeclared namespace: ns|div +Pass Fragment.querySelectorAll: Undeclared namespace: ns|div +Pass Fragment.querySelector: Undeclared namespace: :not(ns|div) +Pass Fragment.querySelectorAll: Undeclared namespace: :not(ns|div) Pass Fragment.querySelector: Invalid namespace: ^|div Pass Fragment.querySelectorAll: Invalid namespace: ^|div Pass Fragment.querySelector: Invalid namespace: $|div @@ -320,10 +320,10 @@ Pass In-document Element.querySelector: Invalid pseudo-element: :::before Pass In-document Element.querySelectorAll: Invalid pseudo-element: :::before Pass In-document Element.querySelector: Invalid pseudo-element: :: before Pass In-document Element.querySelectorAll: Invalid pseudo-element: :: before -Fail In-document Element.querySelector: Undeclared namespace: ns|div -Fail In-document Element.querySelectorAll: Undeclared namespace: ns|div -Fail In-document Element.querySelector: Undeclared namespace: :not(ns|div) -Fail In-document Element.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass In-document Element.querySelector: Undeclared namespace: ns|div +Pass In-document Element.querySelectorAll: Undeclared namespace: ns|div +Pass In-document Element.querySelector: Undeclared namespace: :not(ns|div) +Pass In-document Element.querySelectorAll: Undeclared namespace: :not(ns|div) Pass In-document Element.querySelector: Invalid namespace: ^|div Pass In-document Element.querySelectorAll: Invalid namespace: ^|div Pass In-document Element.querySelector: Invalid namespace: $|div @@ -388,10 +388,10 @@ Pass Empty Element.querySelector: Invalid pseudo-element: :::before Pass Empty Element.querySelectorAll: Invalid pseudo-element: :::before Pass Empty Element.querySelector: Invalid pseudo-element: :: before Pass Empty Element.querySelectorAll: Invalid pseudo-element: :: before -Fail Empty Element.querySelector: Undeclared namespace: ns|div -Fail Empty Element.querySelectorAll: Undeclared namespace: ns|div -Fail Empty Element.querySelector: Undeclared namespace: :not(ns|div) -Fail Empty Element.querySelectorAll: Undeclared namespace: :not(ns|div) +Pass Empty Element.querySelector: Undeclared namespace: ns|div +Pass Empty Element.querySelectorAll: Undeclared namespace: ns|div +Pass Empty Element.querySelector: Undeclared namespace: :not(ns|div) +Pass Empty Element.querySelectorAll: Undeclared namespace: :not(ns|div) Pass Empty Element.querySelector: Invalid namespace: ^|div Pass Empty Element.querySelectorAll: Invalid namespace: ^|div Pass Empty Element.querySelector: Invalid namespace: $|div From 7444f76b0dd547b454aee2c220403abd8a1a86d5 Mon Sep 17 00:00:00 2001 From: Gingeh <39150378+Gingeh@users.noreply.github.com> Date: Sat, 23 Nov 2024 16:22:42 +1100 Subject: [PATCH 022/397] LibWeb: Make querySelectorAll match each element at most once --- Libraries/LibWeb/DOM/ParentNode.cpp | 1 + .../dom/nodes/ParentNode-querySelector-All.txt | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Libraries/LibWeb/DOM/ParentNode.cpp b/Libraries/LibWeb/DOM/ParentNode.cpp index 240e12175e11..4f1a23d4f129 100644 --- a/Libraries/LibWeb/DOM/ParentNode.cpp +++ b/Libraries/LibWeb/DOM/ParentNode.cpp @@ -76,6 +76,7 @@ static WebIDL::ExceptionOr, GC::Ref>> scope_m return TraversalDecision::Break; } results.append(element); + break; } } return TraversalDecision::Continue; diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt index 34ace2711f5c..73e79a570766 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/ParentNode-querySelector-All.txt @@ -6,8 +6,8 @@ Rerun Found 1975 tests -1951 Pass -24 Fail +1955 Pass +20 Fail Details Result Test Name MessagePass Selectors-API Test Suite: HTML Pass Document supports querySelector @@ -670,7 +670,7 @@ Pass Document.querySelectorAll: Class selector, matching element with class with Pass Document.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar Pass Document.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 Pass Document.querySelector: ID selector, matching element with specified id: #id #id-div1 -Fail Document.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Document.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass Document.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass Document.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 Pass Document.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 @@ -1068,7 +1068,7 @@ Pass Detached Element.querySelectorAll: Class selector, matching element with cl Pass Detached Element.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar Pass Detached Element.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 Pass Detached Element.querySelector: ID selector, matching element with specified id: #id #id-div1 -Fail Detached Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Detached Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass Detached Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass Detached Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 Pass Detached Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 @@ -1464,7 +1464,7 @@ Pass Fragment.querySelectorAll: Class selector, matching element with class with Pass Fragment.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar Pass Fragment.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 Pass Fragment.querySelector: ID selector, matching element with specified id: #id #id-div1 -Fail Fragment.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass Fragment.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass Fragment.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass Fragment.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 Pass Fragment.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 @@ -1860,7 +1860,7 @@ Pass In-document Element.querySelectorAll: Class selector, matching element with Pass In-document Element.querySelector: Class selector, matching element with class with escaped character: .test\.foo\[5\]bar Pass In-document Element.querySelectorAll: ID selector, matching element with specified id: #id #id-div1 Pass In-document Element.querySelector: ID selector, matching element with specified id: #id #id-div1 -Fail In-document Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 +Pass In-document Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass In-document Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div1 Pass In-document Element.querySelectorAll: ID selector, chained, matching element with specified id: #id-div1, #id-div2 Pass In-document Element.querySelector: ID selector, chained, matching element with specified id: #id-div1, #id-div2 From 3e536a4cd71d2899b32b0e81ecfb48f5fa0cdbc0 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Fri, 22 Nov 2024 12:01:42 +0100 Subject: [PATCH 023/397] LibWeb: Implement more IntersectionObserver attributes --- Libraries/LibWeb/CSS/Parser/Parser.cpp | 5 + Libraries/LibWeb/CSS/Parser/Parser.h | 2 + .../IntersectionObserver.cpp | 183 ++++++++++++++++-- .../IntersectionObserver.h | 23 ++- .../IntersectionObserver.idl | 9 +- .../observer-attributes.txt | 19 ++ .../observer-exceptions.txt | 19 ++ .../observer-attributes.html | 41 ++++ .../observer-exceptions.html | 61 ++++++ 9 files changed, 345 insertions(+), 17 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 7b9a7e28b41d..cc16c88ff32d 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -6039,6 +6039,11 @@ Vector Parser::parse_font_face_src(TokenStream& compo template Vector Parser::parse_font_face_src(TokenStream& component_values); template Vector Parser::parse_font_face_src(TokenStream& component_values); +Vector Parser::parse_as_list_of_component_values() +{ + return parse_a_list_of_component_values(m_token_stream); +} + RefPtr Parser::parse_list_style_value(TokenStream& tokens) { RefPtr list_position; diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index 4698eeb06878..3c9862d67154 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -69,6 +69,8 @@ class Parser { Vector parse_as_font_face_src(); + Vector parse_as_list_of_component_values(); + static NonnullRefPtr resolve_unresolved_style_value(ParsingContext const&, DOM::Element&, Optional, PropertyID, UnresolvedStyleValue const&); [[nodiscard]] LengthOrCalculated parse_as_sizes_attribute(DOM::Element const& element, HTML::HTMLImageElement const* img = nullptr); diff --git a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp index f8fa224e502c..5fa8cdc74840 100644 --- a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp +++ b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -21,7 +23,24 @@ GC_DEFINE_ALLOCATOR(IntersectionObserver); // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-intersectionobserver WebIDL::ExceptionOr> IntersectionObserver::construct_impl(JS::Realm& realm, GC::Ptr callback, IntersectionObserverInit const& options) { - // 4. Let thresholds be a list equal to options.threshold. + // https://w3c.github.io/IntersectionObserver/#initialize-a-new-intersectionobserver + // 1. Let this be a new IntersectionObserver object + // 2. Set this’s internal [[callback]] slot to callback. + // NOTE: Steps 1 and 2 are handled by creating the IntersectionObserver at the very end of this function. + + // 3. Attempt to parse a margin from options.rootMargin. If a list is returned, set this’s internal [[rootMargin]] slot to that. Otherwise, throw a SyntaxError exception. + auto root_margin = parse_a_margin(realm, options.root_margin); + if (!root_margin.has_value()) { + return WebIDL::SyntaxError::create(realm, "IntersectionObserver: Cannot parse root margin as a margin."_string); + } + + // 4. Attempt to parse a margin from options.scrollMargin. If a list is returned, set this’s internal [[scrollMargin]] slot to that. Otherwise, throw a SyntaxError exception. + auto scroll_margin = parse_a_margin(realm, options.scroll_margin); + if (!scroll_margin.has_value()) { + return WebIDL::SyntaxError::create(realm, "IntersectionObserver: Cannot parse scroll margin as a margin."_string); + } + + // 5. Let thresholds be a list equal to options.threshold. Vector thresholds; if (options.threshold.has()) { thresholds.append(options.threshold.get()); @@ -30,28 +49,47 @@ WebIDL::ExceptionOr> IntersectionObserver::constru thresholds = options.threshold.get>(); } - // 5. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception. + // 6. If any value in thresholds is less than 0.0 or greater than 1.0, throw a RangeError exception. for (auto value : thresholds) { if (value < 0.0 || value > 1.0) return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Threshold values must be between 0.0 and 1.0 inclusive"sv }; } - // 6. Sort thresholds in ascending order. + // 7. Sort thresholds in ascending order. quick_sort(thresholds, [](double left, double right) { return left < right; }); - // 1. Let this be a new IntersectionObserver object - // 2. Set this’s internal [[callback]] slot to callback. - // 8. The thresholds attribute getter will return this sorted thresholds list. - // 9. Return this. - return realm.create(realm, callback, options.root, move(thresholds)); + // 8. If thresholds is empty, append 0 to thresholds. + if (thresholds.is_empty()) { + thresholds.append(0); + } + + // 9. The thresholds attribute getter will return this sorted thresholds list. + // NOTE: Handled implicitly by passing it into the constructor at the end of this function + + // 10. Let delay be the value of options.delay. + auto delay = options.delay; + + // 11. If options.trackVisibility is true and delay is less than 100, set delay to 100. + if (options.track_visibility && delay < 100) { + delay = 100; + } + + // 12. Set this’s internal [[delay]] slot to options.delay to delay. + // 13. Set this’s internal [[trackVisibility]] slot to options.trackVisibility. + // 14. Return this. + return realm.create(realm, callback, options.root, move(root_margin.value()), move(scroll_margin.value()), move(thresholds), move(delay), move(options.track_visibility)); } -IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr callback, Optional, GC::Root>> const& root, Vector&& thresholds) +IntersectionObserver::IntersectionObserver(JS::Realm& realm, GC::Ptr callback, Optional, GC::Root>> const& root, Vector root_margin, Vector scroll_margin, Vector&& thresholds, double delay, bool track_visibility) : PlatformObject(realm) , m_callback(callback) + , m_root_margin(root_margin) + , m_scroll_margin(scroll_margin) , m_thresholds(move(thresholds)) + , m_delay(delay) + , m_track_visibility(track_visibility) { m_root = root.has_value() ? root->visit([](auto& value) -> GC::Ptr { return *value; }) : nullptr; intersection_root().visit([this](auto& node) { @@ -161,6 +199,44 @@ Variant, GC::Root, Empty> IntersectionObse VERIFY_NOT_REACHED(); } +// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin +String IntersectionObserver::root_margin() const +{ + // On getting, return the result of serializing the elements of [[rootMargin]] space-separated, where pixel + // lengths serialize as the numeric value followed by "px", and percentages serialize as the numeric value + // followed by "%". Note that this is not guaranteed to be identical to the options.rootMargin passed to the + // IntersectionObserver constructor. If no rootMargin was passed to the IntersectionObserver + // constructor, the value of this attribute is "0px 0px 0px 0px". + StringBuilder builder; + builder.append(m_root_margin[0].to_string()); + builder.append(' '); + builder.append(m_root_margin[1].to_string()); + builder.append(' '); + builder.append(m_root_margin[2].to_string()); + builder.append(' '); + builder.append(m_root_margin[3].to_string()); + return builder.to_string().value(); +} + +// https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-scrollmargin +String IntersectionObserver::scroll_margin() const +{ + // On getting, return the result of serializing the elements of [[scrollMargin]] space-separated, where pixel + // lengths serialize as the numeric value followed by "px", and percentages serialize as the numeric value + // followed by "%". Note that this is not guaranteed to be identical to the options.scrollMargin passed to the + // IntersectionObserver constructor. If no scrollMargin was passed to the IntersectionObserver + // constructor, the value of this attribute is "0px 0px 0px 0px". + StringBuilder builder; + builder.append(m_scroll_margin[0].to_string()); + builder.append(' '); + builder.append(m_scroll_margin[1].to_string()); + builder.append(' '); + builder.append(m_scroll_margin[2].to_string()); + builder.append(' '); + builder.append(m_scroll_margin[3].to_string()); + return builder.to_string().value(); +} + // https://www.w3.org/TR/intersection-observer/#intersectionobserver-intersection-root Variant, GC::Root> IntersectionObserver::intersection_root() const { @@ -211,11 +287,25 @@ CSSPixelRect IntersectionObserver::root_intersection_rectangle() const rect = CSSPixelRect(bounding_client_rect->x(), bounding_client_rect->y(), bounding_client_rect->width(), bounding_client_rect->height()); } - // FIXME: When calculating the root intersection rectangle for a same-origin-domain target, the rectangle is then - // expanded according to the offsets in the IntersectionObserver’s [[rootMargin]] slot in a manner similar - // to CSS’s margin property, with the four values indicating the amount the top, right, bottom, and left - // edges, respectively, are offset by, with positive lengths indicating an outward offset. Percentages - // are resolved relative to the width of the undilated rectangle. + // When calculating the root intersection rectangle for a same-origin-domain target, the rectangle is then + // expanded according to the offsets in the IntersectionObserver’s [[rootMargin]] slot in a manner similar + // to CSS’s margin property, with the four values indicating the amount the top, right, bottom, and left + // edges, respectively, are offset by, with positive lengths indicating an outward offset. Percentages + // are resolved relative to the width of the undilated rectangle. + DOM::Document* document = { nullptr }; + if (intersection_root.has>()) { + document = intersection_root.get>().cell(); + } else { + document = &intersection_root.get>().cell()->document(); + } + if (m_document.has_value() && document->origin().is_same_origin(m_document->origin())) { + auto layout_node = intersection_root.visit([&](auto& elem) { return static_cast>(*elem)->layout_node(); }); + rect.inflate( + m_root_margin[0].to_px(*layout_node, rect.height()), + m_root_margin[1].to_px(*layout_node, rect.width()), + m_root_margin[2].to_px(*layout_node, rect.height()), + m_root_margin[3].to_px(*layout_node, rect.width())); + } return rect; } @@ -225,4 +315,69 @@ void IntersectionObserver::queue_entry(Badge, GC::Ref> IntersectionObserver::parse_a_margin(JS::Realm& realm, String margin_string) +{ + // 1. Parse a list of component values marginString, storing the result as tokens. + auto tokens = CSS::Parser::Parser::create(CSS::Parser::ParsingContext { realm }, margin_string).parse_as_list_of_component_values(); + + // 2. Remove all whitespace tokens from tokens. + tokens.remove_all_matching([](auto componentValue) { return componentValue.is(CSS::Parser::Token::Type::Whitespace); }); + + // 3. If the length of tokens is greater than 4, return failure. + if (tokens.size() > 4) { + return {}; + } + + // 4. If there are zero elements in tokens, set tokens to ["0px"]. + if (tokens.size() == 0) { + tokens.append(CSS::Parser::Token::create_dimension(0, "px"_fly_string)); + } + + // 5. Replace each token in tokens: + // NOTE: In the spec, tokens miraculously changes type from a list of component values + // to a list of pixel lengths or percentages. + Vector tokens_length_percentage; + for (auto const& token : tokens) { + // If token is an absolute length dimension token, replace it with a an equivalent pixel length. + if (token.is(CSS::Parser::Token::Type::Dimension)) { + auto length = CSS::Length(token.token().dimension_value(), CSS::Length::unit_from_name(token.token().dimension_unit()).value()); + if (length.is_absolute()) { + length.absolute_length_to_px(); + tokens_length_percentage.append(length); + continue; + } + } + // If token is a token, replace it with an equivalent percentage. + if (token.is(CSS::Parser::Token::Type::Percentage)) { + tokens_length_percentage.append(CSS::Percentage(token.token().percentage())); + continue; + } + // Otherwise, return failure. + return {}; + } + + // 6. + switch (tokens_length_percentage.size()) { + // If there is one element in tokens, append three duplicates of that element to tokens. + case 1: + tokens_length_percentage.append(tokens_length_percentage.first()); + tokens_length_percentage.append(tokens_length_percentage.first()); + tokens_length_percentage.append(tokens_length_percentage.first()); + break; + // Otherwise, if there are two elements are tokens, append a duplicate of each element to tokens. + case 2: + tokens_length_percentage.append(tokens_length_percentage.at(0)); + tokens_length_percentage.append(tokens_length_percentage.at(1)); + break; + // Otherwise, if there are three elements in tokens, append a duplicate of the second element to tokens. + case 3: + tokens_length_percentage.append(tokens_length_percentage.at(1)); + break; + } + + // 7. Return tokens. + return tokens_length_percentage; +} + } diff --git a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h index 2ef1ae2899bb..1ea66401e6b9 100644 --- a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h +++ b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.h @@ -16,7 +16,10 @@ namespace Web::IntersectionObserver { struct IntersectionObserverInit { Optional, GC::Root>> root; String root_margin { "0px"_string }; + String scroll_margin { "0px"_string }; Variant> threshold { 0 }; + long delay = 0; + bool track_visibility = false; }; // https://www.w3.org/TR/intersection-observer/#intersectionobserverregistration @@ -53,7 +56,11 @@ class IntersectionObserver final : public Bindings::PlatformObject { Vector> const& observation_targets() const { return m_observation_targets; } Variant, GC::Root, Empty> root() const; + String root_margin() const; + String scroll_margin() const; Vector const& thresholds() const { return m_thresholds; } + long delay() const { return m_delay; } + bool track_visibility() const { return m_track_visibility; } Variant, GC::Root> intersection_root() const; CSSPixelRect root_intersection_rectangle() const; @@ -63,21 +70,35 @@ class IntersectionObserver final : public Bindings::PlatformObject { WebIDL::CallbackType& callback() { return *m_callback; } private: - explicit IntersectionObserver(JS::Realm&, GC::Ptr callback, Optional, GC::Root>> const& root, Vector&& thresholds); + explicit IntersectionObserver(JS::Realm&, GC::Ptr callback, Optional, GC::Root>> const& root, Vector root_margin, Vector scroll_margin, Vector&& thresholds, double debug, bool track_visibility); virtual void initialize(JS::Realm&) override; virtual void visit_edges(JS::Cell::Visitor&) override; virtual void finalize() override; + static Optional> parse_a_margin(JS::Realm&, String); + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-callback-slot GC::Ptr m_callback; // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-root GC::Ptr m_root; + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-rootmargin + Vector m_root_margin; + + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-scrollmargin + Vector m_scroll_margin; + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-thresholds Vector m_thresholds; + // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-delay + long m_delay; + + // https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-trackvisibility + bool m_track_visibility; + // https://www.w3.org/TR/intersection-observer/#dom-intersectionobserver-queuedentries-slot Vector> m_queued_entries; diff --git a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl index 86f5e2886245..f17877a1b71d 100644 --- a/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl +++ b/Libraries/LibWeb/IntersectionObserver/IntersectionObserver.idl @@ -11,9 +11,12 @@ callback IntersectionObserverCallback = undefined (sequence` should be `FrozenArray` readonly attribute sequence thresholds; + readonly attribute long delay; + readonly attribute boolean trackVisibility; undefined observe(Element target); undefined unobserve(Element target); undefined disconnect(); @@ -24,6 +27,8 @@ interface IntersectionObserver { dictionary IntersectionObserverInit { (Element or Document)? root = null; DOMString rootMargin = "0px"; - // FIXME: DOMString scrollMargin = "0px"; + DOMString scrollMargin = "0px"; (double or sequence) threshold = 0; + long delay = 0; + boolean trackVisibility = false; }; diff --git a/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt new file mode 100644 index 000000000000..dfd7b88d652c --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-attributes.txt @@ -0,0 +1,19 @@ +Summary + +Harness status: OK + +Rerun + +Found 9 tests + +9 Pass +Details +Result Test Name MessagePass Observer attribute getters. +Pass observer.root +Pass observer.thresholds +Pass observer.rootMargin +Pass empty observer.thresholds +Pass whitespace observer.rootMargin +Pass set observer.root +Pass set observer.thresholds +Pass set observer.rootMargin \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt new file mode 100644 index 000000000000..50c169118dfa --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/intersection-observer/observer-exceptions.txt @@ -0,0 +1,19 @@ +Summary + +Harness status: OK + +Rerun + +Found 9 tests + +9 Pass +Details +Result Test Name MessagePass IntersectionObserver constructor with { threshold: [1.1] } +Pass IntersectionObserver constructor with { threshold: ["foo"] } +Pass IntersectionObserver constructor with { rootMargin: "1" } +Pass IntersectionObserver constructor with { rootMargin: "2em" } +Pass IntersectionObserver constructor with { rootMargin: "auto" } +Pass IntersectionObserver constructor with { rootMargin: "calc(1px + 2px)" } +Pass IntersectionObserver constructor with { rootMargin: "1px !important" } +Pass IntersectionObserver constructor with { rootMargin: "1px 1px 1px 1px 1px" } +Pass IntersectionObserver.observe("foo") \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html new file mode 100644 index 000000000000..2fe66e4d2df0 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-attributes.html @@ -0,0 +1,41 @@ + + + + + +
    + + diff --git a/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html new file mode 100644 index 000000000000..30e7887ec0bb --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/intersection-observer/observer-exceptions.html @@ -0,0 +1,61 @@ + + + + + + From d389fb440fae3f01d96905e179161a6ea659bd74 Mon Sep 17 00:00:00 2001 From: rmg-x Date: Fri, 22 Nov 2024 09:17:07 -0600 Subject: [PATCH 024/397] CI: Ignore changes to Documentation related files This will prevent CI from running if: - the only changes included are under the Documentation folder, or - the only changes included are *.md files in the root See: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-excluding-paths --- .github/workflows/ci.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79aa499fe021..f7969e1df185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,14 @@ name: CI -on: [push, pull_request] +on: + push: + paths-ignore: + - 'Documentation/**' + - '*.md' + pull_request: + paths-ignore: + - 'Documentation/**' + - '*.md' concurrency: group: ${{ github.workflow }}-${{ github.head_ref || format('{0}-{1}', github.ref, github.run_number) }} From 59c8e0cd5ad06c3da41b82319c827b1fb092a5f0 Mon Sep 17 00:00:00 2001 From: rmg-x Date: Fri, 22 Nov 2024 09:33:33 -0600 Subject: [PATCH 025/397] Documentation: Remove SerenityOS references where appropriate --- Documentation/AddNewIDLFile.md | 2 +- Documentation/AdvancedBuildInstructions.md | 2 +- Documentation/EditorConfiguration/CLionConfiguration.md | 2 +- Documentation/EditorConfiguration/QtCreatorConfiguration.md | 2 +- Documentation/EditorConfiguration/VSCodeConfiguration.md | 4 ++-- Documentation/HumanInterfaceGuidelines/Text.md | 4 ++-- Documentation/ProcessArchitecture.md | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Documentation/AddNewIDLFile.md b/Documentation/AddNewIDLFile.md index bc3c18bb36cb..7cf758e00a5d 100644 --- a/Documentation/AddNewIDLFile.md +++ b/Documentation/AddNewIDLFile.md @@ -1,6 +1,6 @@ # Adding a new IDL file -Serenity's build system does a lot of work of turning the IDL from a Web spec into code, but there are a few things you'll need to do yourself. +Ladybird's build system does a lot of work of turning the IDL from a Web spec into code, but there are a few things you'll need to do yourself. For the sake of example, let's say you're wanting to add the `HTMLDetailsElement`. diff --git a/Documentation/AdvancedBuildInstructions.md b/Documentation/AdvancedBuildInstructions.md index abe130bddd38..ddb7f416cdf3 100644 --- a/Documentation/AdvancedBuildInstructions.md +++ b/Documentation/AdvancedBuildInstructions.md @@ -17,7 +17,7 @@ directory to `Build/release` and then running `ninja `: There are some optional features that can be enabled during compilation that are intended to help with specific types of development work or introduce experimental features. Currently, the following build options are available: - `ENABLE_ADDRESS_SANITIZER`: builds in runtime checks for memory corruption bugs (like buffer overflows and memory leaks) in Lagom test cases. - `ENABLE_MEMORY_SANITIZER`: enables runtime checks for uninitialized memory accesses in Lagom test cases. -- `ENABLE_UNDEFINED_SANITIZER`: builds in runtime checks for [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior) (like null pointer dereferences and signed integer overflows) in Lagom and the SerenityOS userland. +- `ENABLE_UNDEFINED_SANITIZER`: builds in runtime checks for [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior) (like null pointer dereferences and signed integer overflows) in Lagom and Ladybird. - `UNDEFINED_BEHAVIOR_IS_FATAL`: makes all undefined behavior sanitizer errors non-recoverable. This option reduces the performance overhead of `ENABLE_UNDEFINED_SANITIZER`. - `ENABLE_COMPILER_EXPLORER_BUILD`: Skip building non-library entities in Lagom (this only applies to Lagom). - `ENABLE_FUZZERS`: builds [fuzzers](../Meta/Lagom/ReadMe.md#fuzzing) for various parts of the system. diff --git a/Documentation/EditorConfiguration/CLionConfiguration.md b/Documentation/EditorConfiguration/CLionConfiguration.md index 03cbfcf5bc9b..58f2ea077fc0 100644 --- a/Documentation/EditorConfiguration/CLionConfiguration.md +++ b/Documentation/EditorConfiguration/CLionConfiguration.md @@ -25,7 +25,7 @@ A symptom of this not being configured correctly is CLion giving a warning for e ## Code Generation Settings -To make code generated by CLion match the SerenityOS coding style, import the `CLionCodeStyleSettings.xml` from this directory as code style scheme via +To make code generated by CLion match the Ladybird coding style, import the `CLionCodeStyleSettings.xml` from this directory as code style scheme via `Settings -> Editor -> Code Style -> C/C++ -> Scheme -> Cog icon -> Import Scheme...` ## CMake Error Messages diff --git a/Documentation/EditorConfiguration/QtCreatorConfiguration.md b/Documentation/EditorConfiguration/QtCreatorConfiguration.md index 9503775f3ac0..226e4d5d8327 100644 --- a/Documentation/EditorConfiguration/QtCreatorConfiguration.md +++ b/Documentation/EditorConfiguration/QtCreatorConfiguration.md @@ -70,7 +70,7 @@ In order to so, create a new file anywhere, for example `license-template.creato ``` /* - * Copyright (c) 2024, the SerenityOS developers. + * Copyright (c) 2024, the Ladybird developers. * * SPDX-License-Identifier: BSD-2-Clause */ diff --git a/Documentation/EditorConfiguration/VSCodeConfiguration.md b/Documentation/EditorConfiguration/VSCodeConfiguration.md index c6b4016882b0..0a77e00afa27 100644 --- a/Documentation/EditorConfiguration/VSCodeConfiguration.md +++ b/Documentation/EditorConfiguration/VSCodeConfiguration.md @@ -38,8 +38,8 @@ Run ``./Meta/ladybird.sh run ladybird`` at least once to generate the ``compile_ ### DSL syntax highlighting -There's a syntax highlighter extension for SerenityOS DSLs called "SerenityOS DSL Syntax Highlight", available [here](https://marketplace.visualstudio.com/items?itemName=kleinesfilmroellchen.serenity-dsl-syntaxhighlight) or [here](https://open-vsx.org/extension/kleinesfilmroellchen/serenity-dsl-syntaxhighlight). -The extension provides syntax highlighting for LibIPC's IPC files, [Web IDL](https://webidl.spec.whatwg.org/), and LibJS's +There's a syntax highlighter extension for domain specific language files (.idl, .ipc) called "SerenityOS DSL Syntax Highlight", available [here](https://marketplace.visualstudio.com/items?itemName=kleinesfilmroellchen.serenity-dsl-syntaxhighlight) or [here](https://open-vsx.org/extension/kleinesfilmroellchen/serenity-dsl-syntaxhighlight). +The extension provides syntax highlighting for these files, [Web IDL](https://webidl.spec.whatwg.org/), and LibJS's serialization format (no extension) as output by js with the -d option. ### Microsoft C/C++ tools diff --git a/Documentation/HumanInterfaceGuidelines/Text.md b/Documentation/HumanInterfaceGuidelines/Text.md index 5592317ff5f9..b070dac9dab3 100644 --- a/Documentation/HumanInterfaceGuidelines/Text.md +++ b/Documentation/HumanInterfaceGuidelines/Text.md @@ -1,8 +1,8 @@ -# Guidelines for user interface text in SerenityOS +# Guidelines for user interface text in Ladybird ## Capitalization -SerenityOS employs two capitalization styles: +Ladybird employs two capitalization styles: - Book title capitalization - Sentence-style capitalization diff --git a/Documentation/ProcessArchitecture.md b/Documentation/ProcessArchitecture.md index 9becaabd76f4..fcf7de6f7f85 100644 --- a/Documentation/ProcessArchitecture.md +++ b/Documentation/ProcessArchitecture.md @@ -1,8 +1,8 @@ -# SerenityOS Browser process architecture +# Ladybird Browser process architecture *NOTE: This document is partly aspirational, in that the state of the code does not yet fully reflect what's described here. Implementation is underway.* -The SerenityOS web browser (**"Browser"**) uses a multi-process architecture to improve stability and security in the face of arbitrary (and possibly hostile) web content. +The Ladybird web browser uses a multi-process architecture to improve stability and security in the face of arbitrary (and possibly hostile) web content. ## Process overview From 7ea7352bf37f197e96d42a7c89a3cd4addff0384 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 23 Nov 2024 10:52:36 +0100 Subject: [PATCH 026/397] LibWeb: Bail on applying the history step when no active window This is an ad-hoc hack papering over the fact that we can apparently end up in these places without an active window, and proceeding without one leads to assertions on WPT. --- Libraries/LibWeb/HTML/TraversableNavigable.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.cpp b/Libraries/LibWeb/HTML/TraversableNavigable.cpp index bd3e2cb4e972..3ea734cf0318 100644 --- a/Libraries/LibWeb/HTML/TraversableNavigable.cpp +++ b/Libraries/LibWeb/HTML/TraversableNavigable.cpp @@ -815,15 +815,17 @@ TraversableNavigable::HistoryStepResult TraversableNavigable::apply_the_history_ // 18. For each navigable of nonchangingNavigablesThatStillNeedUpdates, queue a global task on the navigation and traversal task source given navigable's active window to run the steps: for (auto& navigable : non_changing_navigables_that_still_need_updates) { - if (navigable->has_been_destroyed()) { + // AD-HOC: This check is not in the spec but we should not continue navigation if navigable has been destroyed, + // or if there's no active window. + if (navigable->has_been_destroyed() || !navigable->active_window()) { ++completed_non_changing_jobs; continue; } - VERIFY(navigable->active_window()); queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), GC::create_function(heap(), [&] { - // NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed. - if (navigable->has_been_destroyed()) { + // AD-HOC: This check is not in the spec but we should not continue navigation if navigable has been destroyed, + // or if there's no active window. + if (navigable->has_been_destroyed() || !navigable->active_window()) { ++completed_non_changing_jobs; return; } From 97aa608c13efacd972a16b04735602d78dde6b35 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 23 Nov 2024 10:58:32 +0100 Subject: [PATCH 027/397] Tests: Import WPT test for pixel length attributes --- .../rendering/pixel-length-attributes.txt | 599 ++++++++++++++++++ .../rendering/pixel-length-attributes.html | 173 +++++ 2 files changed, 772 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/rendering/pixel-length-attributes.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/rendering/pixel-length-attributes.html diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/rendering/pixel-length-attributes.txt b/Tests/LibWeb/Text/expected/wpt-import/html/rendering/pixel-length-attributes.txt new file mode 100644 index 000000000000..67777b3e85c7 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/rendering/pixel-length-attributes.txt @@ -0,0 +1,599 @@ +Summary + +Harness status: OK + +Rerun + +Found 588 tests + +244 Pass +344 Fail +Details +Result Test Name MessageFail + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.html new file mode 100644 index 000000000000..3272d14302f8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.html @@ -0,0 +1,22 @@ + +document.write into iframe + + + +
    \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_003.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_003.html new file mode 100644 index 000000000000..91fc11f6bb87 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_003.html @@ -0,0 +1,23 @@ + +document.write script into iframe + + + +
    \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_004.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_004.html new file mode 100644 index 000000000000..4263696f3071 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_004.html @@ -0,0 +1,22 @@ + +document.write script into iframe write back into parent + + + +
    \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_005.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_005.html new file mode 100644 index 000000000000..b139094d1b66 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_005.html @@ -0,0 +1,25 @@ + +document.write external script into iframe write back into parent + + + +
    \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_006.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_006.html new file mode 100644 index 000000000000..27332038704e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_006.html @@ -0,0 +1,19 @@ + +document.write external script into iframe write back into parent + + + +
    \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_007.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_007.html new file mode 100644 index 000000000000..a6073de1297b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_007.html @@ -0,0 +1,17 @@ + +document.write comment into iframe + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.html new file mode 100644 index 000000000000..e44336b660da --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.html @@ -0,0 +1,18 @@ + +document.write plaintext into iframe + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.html new file mode 100644 index 000000000000..59940102c20b --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.html @@ -0,0 +1,21 @@ + +document.write plaintext into iframe + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_010.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_010.html new file mode 100644 index 000000000000..7d2deb8b80a0 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_010.html @@ -0,0 +1,23 @@ + +document.write plaintext + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_001.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_001.html new file mode 100644 index 000000000000..5563c7901cbf --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_001.html @@ -0,0 +1,10 @@ + +document.write script + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_003.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_003.html new file mode 100644 index 000000000000..f8c9ba84ce01 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_003.html @@ -0,0 +1,10 @@ + +document.write script writing a further script + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_005.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_005.html new file mode 100644 index 000000000000..dd7bb2e448a5 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_005.html @@ -0,0 +1,20 @@ + +document.write external script + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_006.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_006.html new file mode 100644 index 000000000000..54efcb37cd08 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_006.html @@ -0,0 +1,20 @@ + +document.write external script followed by internal script + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_007.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_007.html new file mode 100644 index 000000000000..9f3a42d938c9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_007.html @@ -0,0 +1,19 @@ + +document.write external script that document.writes inline script + + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_008.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_008.html new file mode 100644 index 000000000000..b1389bb449c9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_008.html @@ -0,0 +1,19 @@ + +document.write external script that document.writes external script + + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_010.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_010.html new file mode 100644 index 000000000000..34ff90a0574d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_010.html @@ -0,0 +1,22 @@ + +document.write external script tokenizer order + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_011.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_011.html new file mode 100644 index 000000000000..46df311f2204 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_011.html @@ -0,0 +1,22 @@ + +document.write external script that document.writes external script + + + +
    diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_012.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_012.html new file mode 100644 index 000000000000..0bd69bb32fb2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/script_012.html @@ -0,0 +1,22 @@ + +document.write external script tokenizer order + + + +
    From 7309f2a6f9344501da5dd1914ad426a290420a6d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 23 Nov 2024 16:26:52 +0100 Subject: [PATCH 056/397] LibWeb: Add spec comments to HTML parser's "initial" insertion mode --- Libraries/LibWeb/HTML/Parser/HTMLParser.cpp | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp index 7dcc965e4baf..3c376a9a2161 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp @@ -519,25 +519,51 @@ DOM::QuirksMode HTMLParser::which_quirks_mode(HTMLToken const& doctype_token) co return DOM::QuirksMode::No; } +// https://html.spec.whatwg.org/multipage/parsing.html#the-initial-insertion-mode void HTMLParser::handle_initial(HTMLToken& token) { + // -> A character token that is one of U+0009 CHARACTER TABULATION, U+000A LINE FEED (LF), + // U+000C FORM FEED (FF), U+000D CARRIAGE RETURN (CR), or U+0020 SPACE if (token.is_character() && token.is_parser_whitespace()) { return; } + // -> A comment token if (token.is_comment()) { + // Insert a comment as the last child of the Document object. auto comment = realm().create(document(), token.comment()); MUST(document().append_child(*comment)); return; } + // -> A DOCTYPE token if (token.is_doctype()) { + // If the DOCTYPE token's name is not "html", or the token's public identifier is not missing, + // or the token's system identifier is neither missing nor "about:legacy-compat", then there is a parse error. + if (token.doctype_data().name != "html" || !token.doctype_data().missing_public_identifier || (!token.doctype_data().missing_system_identifier && token.doctype_data().system_identifier != "about:legacy-compat")) { + log_parse_error(); + } + + // Append a DocumentType node to the Document node, with its name set to the name given in the DOCTYPE token, + // or the empty string if the name was missing; its public ID set to the public identifier given in the DOCTYPE token, + // or the empty string if the public identifier was missing; and its system ID set to the system identifier + // given in the DOCTYPE token, or the empty string if the system identifier was missing. + auto doctype = realm().create(document()); doctype->set_name(token.doctype_data().name); doctype->set_public_id(token.doctype_data().public_identifier); doctype->set_system_id(token.doctype_data().system_identifier); MUST(document().append_child(*doctype)); + + // Then, if the document is not an iframe srcdoc document, and the parser cannot change the mode flag is false, + // and the DOCTYPE token matches one of the conditions in the following list, then set the Document to quirks mode: + // [...] + // Otherwise, if the document is not an iframe srcdoc document, and the parser cannot change the mode flag is false, + // and the DOCTYPE token matches one of the conditions in the following list, then set the Document to limited-quirks mode: + // [...] document().set_quirks_mode(which_quirks_mode(token)); + + // Then, switch the insertion mode to "before html". m_insertion_mode = InsertionMode::BeforeHTML; return; } From 1d554f97de341008ef60ed241169365538154f82 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 23 Nov 2024 16:28:35 +0100 Subject: [PATCH 057/397] LibWeb: Reset the "stop parsing" flag when entering HTML parser Otherwise we'll always bail after processing one token, which is not what we want. --- Libraries/LibWeb/HTML/Parser/HTMLParser.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp index 3c376a9a2161..7bb582d5fe9e 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLParser.cpp @@ -188,6 +188,8 @@ void HTMLParser::visit_edges(Cell::Visitor& visitor) void HTMLParser::run(HTMLTokenizer::StopAtInsertionPoint stop_at_insertion_point) { + m_stop_parsing = false; + for (;;) { // FIXME: Find a better way to say that we come from Document::close() and want to process EOF. if (!m_tokenizer.is_eof_inserted() && m_tokenizer.is_insertion_point_reached()) From 69367194a6d04bf124dd0b1697c69a75489122a7 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 23 Nov 2024 18:30:01 +0100 Subject: [PATCH 058/397] LibWeb: Make HTML tokenizer stop at insertion point after state switch If we reach the insertion point at the same time as we switch to another tokenizer state, we have to bail immediately without proceeding with the next code point. Otherwise we'd fetch the next token, get an EOF marker, and then proceed as if we're at the end of the input stream, even though more data may be coming (with more calls to document.write()..) --- Libraries/LibWeb/HTML/Parser/HTMLTokenizer.cpp | 14 ++++++++------ .../html/syntax/parsing/ambiguous-ampersand.txt | 8 ++++---- .../document-write/003.txt | 4 ++-- .../document-write/007.txt | 4 ++-- .../document-write/010.txt | 4 ++-- .../document-write/011.txt | 4 ++-- .../document-write/014.txt | 4 ++-- .../document-write/015.txt | 4 ++-- .../document-write/017.txt | 4 ++-- .../document-write/033.txt | 4 ++-- .../document-write/035.txt | 4 ++-- .../document-write/038.txt | 4 ++-- .../document-write/039.txt | 4 ++-- .../document-write/046.txt | 4 ++-- .../document-write/049.txt | 4 ++-- .../document-write/iframe_002.txt | 4 ++-- .../document-write/iframe_008.txt | 4 ++-- .../document-write/iframe_009.txt | 4 ++-- 18 files changed, 44 insertions(+), 42 deletions(-) diff --git a/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.cpp b/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.cpp index fca8292ec291..c13ce25192ba 100644 --- a/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.cpp +++ b/Libraries/LibWeb/HTML/Parser/HTMLTokenizer.cpp @@ -30,12 +30,14 @@ namespace Web::HTML { SWITCH_TO_WITH_UNCLEAN_BUILDER(new_state); \ } while (0) -#define SWITCH_TO_WITH_UNCLEAN_BUILDER(new_state) \ - do { \ - will_switch_to(State::new_state); \ - m_state = State::new_state; \ - CONSUME_NEXT_INPUT_CHARACTER; \ - goto new_state; \ +#define SWITCH_TO_WITH_UNCLEAN_BUILDER(new_state) \ + do { \ + will_switch_to(State::new_state); \ + m_state = State::new_state; \ + if (stop_at_insertion_point == StopAtInsertionPoint::Yes && is_insertion_point_reached()) \ + return {}; \ + CONSUME_NEXT_INPUT_CHARACTER; \ + goto new_state; \ } while (0) #define RECONSUME_IN(new_state) \ diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/syntax/parsing/ambiguous-ampersand.txt b/Tests/LibWeb/Text/expected/wpt-import/html/syntax/parsing/ambiguous-ampersand.txt index 4dcb3893808c..8879e4e2e1b6 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/syntax/parsing/ambiguous-ampersand.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/syntax/parsing/ambiguous-ampersand.txt @@ -6,9 +6,9 @@ Rerun Found 3 tests -1 Pass -2 Fail +2 Pass +1 Fail Details -Result Test Name MessageFail Check number of divs +Result Test Name MessagePass Check number of divs Pass Check div structure: network -Fail Check div structure: document.write Cannot access property "childNodes" on undefined object "div" \ No newline at end of file +Fail Check div structure: document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/003.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/003.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/003.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/003.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/007.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/007.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/007.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/007.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/010.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/010.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/010.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/010.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/011.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/011.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/011.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/011.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/014.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/014.txt index 9e0ab144835e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/014.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/014.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write Cannot access property "firstChild" on null object "document.body" \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/015.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/015.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/015.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/015.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/017.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/017.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/017.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/017.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/033.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/033.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/033.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/033.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/035.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/035.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/035.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/035.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/038.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/038.txt index a57d8db53603..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/038.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/038.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write Cannot access property "childNodes" on null object "document.body" \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/039.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/039.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/039.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/039.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/046.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/046.txt index 400fa5782b1e..49a300c2d275 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/046.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/046.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write \ No newline at end of file +Result Test Name MessagePass document.write \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/049.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/049.txt index d3ff29811340..577ea5ff09a3 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/049.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/049.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write plaintext \ No newline at end of file +Result Test Name MessagePass document.write plaintext \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.txt index b2f8a2410858..38d3b78bfbe8 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_002.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write into iframe \ No newline at end of file +Result Test Name MessagePass document.write into iframe \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.txt index 71b917823416..e649c315d7dd 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_008.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write plaintext into iframe \ No newline at end of file +Result Test Name MessagePass document.write plaintext into iframe \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.txt index 71b917823416..e649c315d7dd 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/dynamic-markup-insertion/document-write/iframe_009.txt @@ -6,6 +6,6 @@ Rerun Found 1 tests -1 Fail +1 Pass Details -Result Test Name MessageFail document.write plaintext into iframe \ No newline at end of file +Result Test Name MessagePass document.write plaintext into iframe \ No newline at end of file From 442629064bd91cd5fe119b344dce4b21a89f4ede Mon Sep 17 00:00:00 2001 From: Pavel Shliak Date: Sat, 23 Nov 2024 19:22:31 +0400 Subject: [PATCH 059/397] LibCore: Remove deprecated ElapsedTimer::elapsed() This follows FIXME in the code. --- Libraries/LibCore/ElapsedTimer.h | 6 ------ Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp | 4 ++-- Services/RequestServer/ConnectionFromClient.cpp | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Libraries/LibCore/ElapsedTimer.h b/Libraries/LibCore/ElapsedTimer.h index f3b7cd3d4613..4baff0de3c99 100644 --- a/Libraries/LibCore/ElapsedTimer.h +++ b/Libraries/LibCore/ElapsedTimer.h @@ -31,12 +31,6 @@ class ElapsedTimer { i64 elapsed_milliseconds() const; AK::Duration elapsed_time() const; - // FIXME: Move callers to elapsed_milliseconds(), remove this. - i64 elapsed() const // milliseconds - { - return elapsed_milliseconds(); - } - MonotonicTime const& origin_time() const { return m_origin_time; } private: diff --git a/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp b/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp index b2d987125755..438629ce6730 100644 --- a/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp +++ b/Libraries/LibWeb/HTML/Scripting/ClassicScript.cpp @@ -51,7 +51,7 @@ GC::Ref ClassicScript::create(ByteString filename, StringView sou // 10. Let result be ParseScript(source, realm, script). auto parse_timer = Core::ElapsedTimer::start_new(); auto result = JS::Script::parse(source, realm, script->filename(), script, source_line_number); - dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Parsed {} in {}ms", script->filename(), parse_timer.elapsed()); + dbgln_if(HTML_SCRIPT_DEBUG, "ClassicScript: Parsed {} in {}ms", script->filename(), parse_timer.elapsed_milliseconds()); // 11. If result is a list of errors, then: if (result.is_error()) { @@ -101,7 +101,7 @@ JS::Completion ClassicScript::run(RethrowErrors rethrow_errors, GC::Ptrwhen_resolved([url, timer](auto const& results) -> ErrorOr { - dbgln("ensure_connection::ResolveOnly({}) OK {} entrie(s) in {}ms", url, results->cached_addresses().size(), timer.elapsed()); + dbgln("ensure_connection::ResolveOnly({}) OK {} entrie(s) in {}ms", url, results->cached_addresses().size(), timer.elapsed_milliseconds()); return {}; }); promise->when_rejected([url](auto const&) { dbgln("ensure_connection::ResolveOnly({}) rejected", url); }); From 628e1d98537c0dec03dfda5d1752c2b49d70d143 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Sat, 23 Nov 2024 19:13:09 +0100 Subject: [PATCH 060/397] LibWeb: Fix incorrect spec link for the @supports rule --- Libraries/LibWeb/CSS/Supports.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/Supports.h b/Libraries/LibWeb/CSS/Supports.h index 78463827a7c2..2bd3cb8e0986 100644 --- a/Libraries/LibWeb/CSS/Supports.h +++ b/Libraries/LibWeb/CSS/Supports.h @@ -15,7 +15,7 @@ namespace Web::CSS { -// https://www.w3.org/TR/css-conditional-4/#at-supports +// https://www.w3.org/TR/css-conditional-3/#at-supports class Supports final : public RefCounted { friend class Parser::Parser; From 8562b0e33bd057d9aa5be1b6ce66db039bc99576 Mon Sep 17 00:00:00 2001 From: Saksham Mittal Date: Fri, 22 Nov 2024 19:53:59 +0530 Subject: [PATCH 061/397] LibWeb: Migrate CSS filter application to new ApplyFilters command This helps reuse this code in other areas, such as for filters for SVGs --- Libraries/LibWeb/Painting/Command.h | 8 ++- Libraries/LibWeb/Painting/DisplayList.cpp | 1 + Libraries/LibWeb/Painting/DisplayList.h | 1 + .../LibWeb/Painting/DisplayListPlayerSkia.cpp | 63 +++++++++++-------- .../LibWeb/Painting/DisplayListPlayerSkia.h | 1 + .../LibWeb/Painting/DisplayListRecorder.cpp | 6 +- .../LibWeb/Painting/DisplayListRecorder.h | 2 +- Libraries/LibWeb/Painting/StackingContext.cpp | 2 +- 8 files changed, 53 insertions(+), 31 deletions(-) diff --git a/Libraries/LibWeb/Painting/Command.h b/Libraries/LibWeb/Painting/Command.h index 47c643e052c6..cb395bbd9222 100644 --- a/Libraries/LibWeb/Painting/Command.h +++ b/Libraries/LibWeb/Painting/Command.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -118,7 +119,6 @@ struct StackingContextTransform { struct PushStackingContext { float opacity; - CSS::ResolvedFilter filter; // The bounding box of the source paintable (pre-transform). Gfx::IntRect source_paintable_rect; // A translation to be applied after the stacking context has been transformed. @@ -408,6 +408,11 @@ struct ApplyOpacity { float opacity; }; +struct ApplyFilters { + float opacity; + CSS::ResolvedFilter filter; +}; + struct ApplyTransform { Gfx::FloatPoint origin; Gfx::FloatMatrix4x4 matrix; @@ -463,6 +468,7 @@ using Command = Variant< PaintNestedDisplayList, PaintScrollBar, ApplyOpacity, + ApplyFilters, ApplyTransform, ApplyMaskBitmap>; diff --git a/Libraries/LibWeb/Painting/DisplayList.cpp b/Libraries/LibWeb/Painting/DisplayList.cpp index b7b0f89173d1..ec7ad75e93b6 100644 --- a/Libraries/LibWeb/Painting/DisplayList.cpp +++ b/Libraries/LibWeb/Painting/DisplayList.cpp @@ -122,6 +122,7 @@ void DisplayListPlayer::execute(DisplayList& display_list) else HANDLE_COMMAND(PaintScrollBar, paint_scrollbar) else HANDLE_COMMAND(PaintNestedDisplayList, paint_nested_display_list) else HANDLE_COMMAND(ApplyOpacity, apply_opacity) + else HANDLE_COMMAND(ApplyFilters, apply_filters) else HANDLE_COMMAND(ApplyTransform, apply_transform) else HANDLE_COMMAND(ApplyMaskBitmap, apply_mask_bitmap) else VERIFY_NOT_REACHED(); diff --git a/Libraries/LibWeb/Painting/DisplayList.h b/Libraries/LibWeb/Painting/DisplayList.h index 386743ec5b0b..4b9c9ca4709d 100644 --- a/Libraries/LibWeb/Painting/DisplayList.h +++ b/Libraries/LibWeb/Painting/DisplayList.h @@ -76,6 +76,7 @@ class DisplayListPlayer { virtual void paint_nested_display_list(PaintNestedDisplayList const&) = 0; virtual void paint_scrollbar(PaintScrollBar const&) = 0; virtual void apply_opacity(ApplyOpacity const&) = 0; + virtual void apply_filters(ApplyFilters const&) = 0; virtual void apply_transform(ApplyTransform const&) = 0; virtual void apply_mask_bitmap(ApplyMaskBitmap const&) = 0; virtual bool would_be_fully_clipped_by_painter(Gfx::IntRect) const = 0; diff --git a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp index b72a2b3b9d97..d3ea84fe993a 100644 --- a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp +++ b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp @@ -396,33 +396,7 @@ void DisplayListPlayerSkia::push_stacking_context(PushStackingContext const& com .translate(-command.transform.origin); auto matrix = to_skia_matrix(new_transform); - if (!command.filter.is_none()) { - sk_sp image_filter; - auto append_filter = [&image_filter](auto new_filter) { - if (image_filter) - image_filter = SkImageFilters::Compose(new_filter, image_filter); - else - image_filter = new_filter; - }; - - // Apply filters in order - for (auto const& filter_function : command.filter.filters) - append_filter(to_skia_image_filter(filter_function)); - - // We apply opacity as a color filter here so we only need to save and restore a single layer. - if (command.opacity < 1) { - append_filter(to_skia_image_filter(CSS::ResolvedFilter::FilterFunction { - CSS::ResolvedFilter::Color { - CSS::FilterOperation::Color::Type::Opacity, - command.opacity, - }, - })); - } - - SkPaint paint; - paint.setImageFilter(image_filter); - canvas.saveLayer(nullptr, &paint); - } else if (command.opacity < 1) { + if (command.opacity < 1) { auto source_paintable_rect = to_skia_rect(command.source_paintable_rect); SkRect dest; matrix.mapRect(&dest, source_paintable_rect); @@ -1105,6 +1079,41 @@ void DisplayListPlayerSkia::apply_opacity(ApplyOpacity const& command) canvas.saveLayer(nullptr, &paint); } +void DisplayListPlayerSkia::apply_filters(ApplyFilters const& command) +{ + if (command.filter.is_none()) { + return; + } + sk_sp image_filter; + auto append_filter = [&image_filter](auto new_filter) { + if (image_filter) + image_filter = SkImageFilters::Compose(new_filter, image_filter); + else + image_filter = new_filter; + }; + + // Apply filters in order + for (auto filter : command.filter.filters) { + append_filter(to_skia_image_filter(filter)); + } + + // We apply opacity as a color filter here so we only need to save and restore a single layer. + if (command.opacity < 1) { + append_filter(to_skia_image_filter(CSS::ResolvedFilter::FilterFunction { + CSS::ResolvedFilter::Color { + CSS::FilterOperation::Color::Type::Opacity, + command.opacity, + }, + })); + } + + SkPaint paint; + paint.setImageFilter(image_filter); + auto& canvas = surface().canvas(); + canvas.saveLayer(nullptr, &paint); + return; +} + void DisplayListPlayerSkia::apply_transform(ApplyTransform const& command) { auto affine_transform = Gfx::extract_2d_affine_transform(command.matrix); diff --git a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.h b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.h index 2a1e2b502031..6e1f408cda7e 100644 --- a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.h +++ b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.h @@ -63,6 +63,7 @@ class DisplayListPlayerSkia : public DisplayListPlayer { void paint_scrollbar(PaintScrollBar const&) override; void paint_nested_display_list(PaintNestedDisplayList const&) override; void apply_opacity(ApplyOpacity const&) override; + void apply_filters(ApplyFilters const&) override; void apply_transform(ApplyTransform const&) override; void apply_mask_bitmap(ApplyMaskBitmap const&) override; diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index 2b1584cee646..3d3f6b479d1c 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp @@ -295,7 +295,6 @@ void DisplayListRecorder::push_stacking_context(PushStackingContextParams params { append(PushStackingContext { .opacity = params.opacity, - .filter = params.filter, .source_paintable_rect = params.source_paintable_rect, .transform = { .origin = params.transform.origin, @@ -408,6 +407,11 @@ void DisplayListRecorder::apply_opacity(float opacity) append(ApplyOpacity { .opacity = opacity }); } +void DisplayListRecorder::apply_filters(float opacity, CSS::ResolvedFilter filter) +{ + append(ApplyFilters { .opacity = opacity, .filter = filter }); +} + void DisplayListRecorder::apply_transform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4 matrix) { append(ApplyTransform { diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.h b/Libraries/LibWeb/Painting/DisplayListRecorder.h index 581622c1cc08..300d2be7e076 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.h +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.h @@ -121,7 +121,6 @@ class DisplayListRecorder { struct PushStackingContextParams { float opacity; - CSS::ResolvedFilter filter; bool is_fixed_position; Gfx::IntRect source_paintable_rect; StackingContextTransform transform; @@ -150,6 +149,7 @@ class DisplayListRecorder { void paint_scrollbar(int scroll_frame_id, Gfx::IntRect, CSSPixelFraction scroll_size, bool vertical); void apply_opacity(float opacity); + void apply_filters(float opacity, CSS::ResolvedFilter filter); void apply_transform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4); void apply_mask_bitmap(Gfx::IntPoint origin, Gfx::ImmutableBitmap const&, Gfx::Bitmap::MaskKind); diff --git a/Libraries/LibWeb/Painting/StackingContext.cpp b/Libraries/LibWeb/Painting/StackingContext.cpp index 41d630e35c89..6938e8488263 100644 --- a/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Libraries/LibWeb/Painting/StackingContext.cpp @@ -301,7 +301,6 @@ void StackingContext::paint(PaintContext& context) const DisplayListRecorder::PushStackingContextParams push_stacking_context_params { .opacity = opacity, - .filter = paintable_box().computed_values().filter(), .is_fixed_position = paintable_box().is_fixed_position(), .source_paintable_rect = source_paintable_rect, .transform = { @@ -328,6 +327,7 @@ void StackingContext::paint(PaintContext& context) const context.display_list_recorder().push_scroll_frame_id(*paintable_box().scroll_frame_id()); } context.display_list_recorder().push_stacking_context(push_stacking_context_params); + context.display_list_recorder().apply_filters(opacity, paintable_box().computed_values().filter()); if (auto mask_image = computed_values.mask_image()) { auto mask_display_list = DisplayList::create(); From ab1cf8f89bcf0c717663d5c64ecf4c851c94ecf1 Mon Sep 17 00:00:00 2001 From: Saksham Mittal Date: Fri, 22 Nov 2024 19:55:07 +0530 Subject: [PATCH 062/397] LibWeb: Use ApplyFilters command for SVGs --- Libraries/LibWeb/Painting/SVGSVGPaintable.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp index 7d782e4aa0b3..9384eb107a6c 100644 --- a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp @@ -70,6 +70,8 @@ void SVGSVGPaintable::paint_descendants(PaintContext& context, PaintableBox cons context.display_list_recorder().apply_opacity(computed_values.opacity()); } + context.display_list_recorder().apply_filters(paintable.computed_values().opacity(), paintable.computed_values().filter()); + if (svg_box.has_css_transform()) { auto transform_matrix = svg_box.transform(); Gfx::FloatPoint transform_origin = svg_box.transform_origin().template to_type(); From ecdb53cca6f385199f656f1f766dc181698b8b33 Mon Sep 17 00:00:00 2001 From: Saksham Mittal Date: Sat, 23 Nov 2024 23:08:51 +0530 Subject: [PATCH 063/397] LibWeb: Deduplicate opacity code in ApplyFilters The opacity is still being set separately by using ApplyOpacity for both CSS and SVG --- Libraries/LibWeb/Painting/Command.h | 1 - Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp | 10 ---------- Libraries/LibWeb/Painting/DisplayListRecorder.cpp | 4 ++-- Libraries/LibWeb/Painting/DisplayListRecorder.h | 2 +- Libraries/LibWeb/Painting/SVGSVGPaintable.cpp | 2 +- Libraries/LibWeb/Painting/StackingContext.cpp | 2 +- 6 files changed, 5 insertions(+), 16 deletions(-) diff --git a/Libraries/LibWeb/Painting/Command.h b/Libraries/LibWeb/Painting/Command.h index cb395bbd9222..fa1deaec99e1 100644 --- a/Libraries/LibWeb/Painting/Command.h +++ b/Libraries/LibWeb/Painting/Command.h @@ -409,7 +409,6 @@ struct ApplyOpacity { }; struct ApplyFilters { - float opacity; CSS::ResolvedFilter filter; }; diff --git a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp index d3ea84fe993a..9460fdab76c8 100644 --- a/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp +++ b/Libraries/LibWeb/Painting/DisplayListPlayerSkia.cpp @@ -1097,16 +1097,6 @@ void DisplayListPlayerSkia::apply_filters(ApplyFilters const& command) append_filter(to_skia_image_filter(filter)); } - // We apply opacity as a color filter here so we only need to save and restore a single layer. - if (command.opacity < 1) { - append_filter(to_skia_image_filter(CSS::ResolvedFilter::FilterFunction { - CSS::ResolvedFilter::Color { - CSS::FilterOperation::Color::Type::Opacity, - command.opacity, - }, - })); - } - SkPaint paint; paint.setImageFilter(image_filter); auto& canvas = surface().canvas(); diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp index 3d3f6b479d1c..0ba89e6e9d9a 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.cpp +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.cpp @@ -407,9 +407,9 @@ void DisplayListRecorder::apply_opacity(float opacity) append(ApplyOpacity { .opacity = opacity }); } -void DisplayListRecorder::apply_filters(float opacity, CSS::ResolvedFilter filter) +void DisplayListRecorder::apply_filters(CSS::ResolvedFilter filter) { - append(ApplyFilters { .opacity = opacity, .filter = filter }); + append(ApplyFilters { .filter = filter }); } void DisplayListRecorder::apply_transform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4 matrix) diff --git a/Libraries/LibWeb/Painting/DisplayListRecorder.h b/Libraries/LibWeb/Painting/DisplayListRecorder.h index 300d2be7e076..0e7a560cc2e3 100644 --- a/Libraries/LibWeb/Painting/DisplayListRecorder.h +++ b/Libraries/LibWeb/Painting/DisplayListRecorder.h @@ -149,7 +149,7 @@ class DisplayListRecorder { void paint_scrollbar(int scroll_frame_id, Gfx::IntRect, CSSPixelFraction scroll_size, bool vertical); void apply_opacity(float opacity); - void apply_filters(float opacity, CSS::ResolvedFilter filter); + void apply_filters(CSS::ResolvedFilter filter); void apply_transform(Gfx::FloatPoint origin, Gfx::FloatMatrix4x4); void apply_mask_bitmap(Gfx::IntPoint origin, Gfx::ImmutableBitmap const&, Gfx::Bitmap::MaskKind); diff --git a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp index 9384eb107a6c..39d45b092059 100644 --- a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp @@ -70,7 +70,7 @@ void SVGSVGPaintable::paint_descendants(PaintContext& context, PaintableBox cons context.display_list_recorder().apply_opacity(computed_values.opacity()); } - context.display_list_recorder().apply_filters(paintable.computed_values().opacity(), paintable.computed_values().filter()); + context.display_list_recorder().apply_filters(paintable.computed_values().filter()); if (svg_box.has_css_transform()) { auto transform_matrix = svg_box.transform(); diff --git a/Libraries/LibWeb/Painting/StackingContext.cpp b/Libraries/LibWeb/Painting/StackingContext.cpp index 6938e8488263..8d00a0303078 100644 --- a/Libraries/LibWeb/Painting/StackingContext.cpp +++ b/Libraries/LibWeb/Painting/StackingContext.cpp @@ -327,7 +327,7 @@ void StackingContext::paint(PaintContext& context) const context.display_list_recorder().push_scroll_frame_id(*paintable_box().scroll_frame_id()); } context.display_list_recorder().push_stacking_context(push_stacking_context_params); - context.display_list_recorder().apply_filters(opacity, paintable_box().computed_values().filter()); + context.display_list_recorder().apply_filters(paintable_box().computed_values().filter()); if (auto mask_image = computed_values.mask_image()) { auto mask_display_list = DisplayList::create(); From 6ad93d2b213023bc47245bef26e7393d348c8a88 Mon Sep 17 00:00:00 2001 From: Pavel Shliak Date: Sat, 23 Nov 2024 21:29:18 +0400 Subject: [PATCH 064/397] UI/Qt: Use Qt-reported dark theme For Qt >= 6.5, the system theme can be determined reliably, so no guesswork is needed. A fallback remains for Qt < 6.5, but it is hacky and less reliable. --- UI/Qt/main.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/UI/Qt/main.cpp b/UI/Qt/main.cpp index af3c312b1f43..cee83fed1d91 100644 --- a/UI/Qt/main.cpp +++ b/UI/Qt/main.cpp @@ -22,6 +22,10 @@ #include #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) +# include +#endif + #if defined(AK_OS_MACOS) # include #endif @@ -32,15 +36,20 @@ namespace Ladybird { bool is_using_dark_system_theme(QWidget&); bool is_using_dark_system_theme(QWidget& widget) { - // FIXME: Qt does not provide any method to query if the system is using a dark theme. We will have to implement - // platform-specific methods if we wish to have better detection. For now, this inspects if Qt is using a - // dark color for widget backgrounds using Rec. 709 luma coefficients. - // https://en.wikipedia.org/wiki/Rec._709#Luma_coefficients - +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + // We use that only for fallback mode + Q_UNUSED(widget); + // Use the new Qt API available from version 6.5.0 + auto color_scheme = QGuiApplication::styleHints()->colorScheme(); + return color_scheme == Qt::ColorScheme::Dark; +#else + // Fallback for older Qt versions + // Calculate luma based on Rec. 709 coefficients + // https://en.wikipedia.org/wiki/Rec._709#Luma_coefficients auto color = widget.palette().color(widget.backgroundRole()); auto luma = 0.2126f * color.redF() + 0.7152f * color.greenF() + 0.0722f * color.blueF(); - return luma <= 0.5f; +#endif } } From d2ca522540788e96f3942598f40b04ba21ed44c1 Mon Sep 17 00:00:00 2001 From: Jonne Ransijn Date: Sat, 23 Nov 2024 20:28:58 +0100 Subject: [PATCH 065/397] LibWeb: Use `is_viewport` helper This method exists precicely to simplify the check being done here, so lets use it! --- Libraries/LibWeb/Painting/PaintableBox.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index cffeb0110317..4abec2825ebc 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -865,7 +865,7 @@ TraversalDecision PaintableBox::hit_test(CSSPixelPoint position, HitTestType typ if (hit_test_scrollbars(position_adjusted_by_scroll_offset, callback) == TraversalDecision::Break) return TraversalDecision::Break; - if (layout_node_with_style_and_box_metrics().is_viewport()) { + if (is_viewport()) { auto& viewport_paintable = const_cast(static_cast(*this)); viewport_paintable.build_stacking_context_tree_if_needed(); viewport_paintable.document().update_paint_and_hit_testing_properties_if_needed(); From a0fb092d94240d2b71672a1225241f47a696ef8e Mon Sep 17 00:00:00 2001 From: Jonne Ransijn Date: Sat, 23 Nov 2024 20:32:07 +0100 Subject: [PATCH 066/397] LibWeb: Transform `PaintableBox::hit_test` positions Elements with transforms were tested on their pre-transformed positions, causing incorrect hits. Copy the position transformation done in `StackingContext::hit_test` to ensure that hit tests are done on the _actual_ position. --- Libraries/LibWeb/Painting/PaintableBox.cpp | 31 ++++++++++++------- .../expected/hit_testing/css-transforms.txt | 1 + .../input/hit_testing/css-transforms.html | 22 +++++++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/hit_testing/css-transforms.txt create mode 100644 Tests/LibWeb/Text/input/hit_testing/css-transforms.html diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index 4abec2825ebc..09c1d21dfc31 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -917,6 +917,13 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy auto position_adjusted_by_scroll_offset = position; position_adjusted_by_scroll_offset.translate_by(-cumulative_offset_of_enclosing_scroll_frame()); + // NOTE: This CSSPixels -> Float -> CSSPixels conversion is because we can't AffineTransform::map() a CSSPixelPoint. + Gfx::FloatPoint offset_position { + (position_adjusted_by_scroll_offset.x() - transform_origin().x()).to_float(), + (position_adjusted_by_scroll_offset.y() - transform_origin().y()).to_float() + }; + auto transformed_position_adjusted_by_scroll_offset = combined_css_transform().inverse().value_or({}).map(offset_position).to_type() + transform_origin(); + // TextCursor hit testing mode should be able to place cursor in contenteditable elements even if they are empty auto is_editable = layout_node_with_style_and_box_metrics().dom_node() && layout_node_with_style_and_box_metrics().dom_node()->is_editable(); if (is_editable && m_fragments.is_empty() && !has_children() && type == HitTestType::TextCursor) { @@ -934,7 +941,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy return PaintableBox::hit_test(position, type, callback); } - if (hit_test_scrollbars(position_adjusted_by_scroll_offset, callback) == TraversalDecision::Break) + if (hit_test_scrollbars(transformed_position_adjusted_by_scroll_offset, callback) == TraversalDecision::Break) return TraversalDecision::Break; for (auto const* child = last_child(); child; child = child->previous_sibling()) { @@ -946,10 +953,10 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy if (fragment.paintable().has_stacking_context()) continue; auto fragment_absolute_rect = fragment.absolute_rect(); - if (fragment_absolute_rect.contains(position_adjusted_by_scroll_offset)) { - if (fragment.paintable().hit_test(position, type, callback) == TraversalDecision::Break) + if (fragment_absolute_rect.contains(transformed_position_adjusted_by_scroll_offset)) { + if (fragment.paintable().hit_test(transformed_position_adjusted_by_scroll_offset, type, callback) == TraversalDecision::Break) return TraversalDecision::Break; - HitTestResult hit_test_result { const_cast(fragment.paintable()), fragment.text_index_at(position_adjusted_by_scroll_offset), 0, 0 }; + HitTestResult hit_test_result { const_cast(fragment.paintable()), fragment.text_index_at(transformed_position_adjusted_by_scroll_offset), 0, 0 }; if (callback(hit_test_result) == TraversalDecision::Break) return TraversalDecision::Break; } else if (type == HitTestType::TextCursor) { @@ -972,30 +979,30 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy // the place to place the cursor. To determine the best place, we first find the closest fragment horizontally to // the cursor. If we could not find one, then find for the closest vertically above the cursor. // If we knew the direction of selection, we would look above if selecting upward. - if (fragment_absolute_rect.bottom() - 1 <= position_adjusted_by_scroll_offset.y()) { // fully below the fragment + if (fragment_absolute_rect.bottom() - 1 <= transformed_position_adjusted_by_scroll_offset.y()) { // fully below the fragment HitTestResult hit_test_result { .paintable = const_cast(fragment.paintable()), .index_in_node = fragment.start() + fragment.length(), - .vertical_distance = position_adjusted_by_scroll_offset.y() - fragment_absolute_rect.bottom(), + .vertical_distance = transformed_position_adjusted_by_scroll_offset.y() - fragment_absolute_rect.bottom(), }; if (callback(hit_test_result) == TraversalDecision::Break) return TraversalDecision::Break; - } else if (fragment_absolute_rect.top() <= position_adjusted_by_scroll_offset.y()) { // vertically within the fragment - if (position_adjusted_by_scroll_offset.x() < fragment_absolute_rect.left()) { + } else if (fragment_absolute_rect.top() <= transformed_position_adjusted_by_scroll_offset.y()) { // vertically within the fragment + if (transformed_position_adjusted_by_scroll_offset.x() < fragment_absolute_rect.left()) { HitTestResult hit_test_result { .paintable = const_cast(fragment.paintable()), .index_in_node = fragment.start(), .vertical_distance = 0, - .horizontal_distance = fragment_absolute_rect.left() - position_adjusted_by_scroll_offset.x(), + .horizontal_distance = fragment_absolute_rect.left() - transformed_position_adjusted_by_scroll_offset.x(), }; if (callback(hit_test_result) == TraversalDecision::Break) return TraversalDecision::Break; - } else if (position_adjusted_by_scroll_offset.x() > fragment_absolute_rect.right()) { + } else if (transformed_position_adjusted_by_scroll_offset.x() > fragment_absolute_rect.right()) { HitTestResult hit_test_result { .paintable = const_cast(fragment.paintable()), .index_in_node = fragment.start() + fragment.length(), .vertical_distance = 0, - .horizontal_distance = position_adjusted_by_scroll_offset.x() - fragment_absolute_rect.right(), + .horizontal_distance = transformed_position_adjusted_by_scroll_offset.x() - fragment_absolute_rect.right(), }; if (callback(hit_test_result) == TraversalDecision::Break) return TraversalDecision::Break; @@ -1005,7 +1012,7 @@ TraversalDecision PaintableWithLines::hit_test(CSSPixelPoint position, HitTestTy } } - if (!stacking_context() && is_visible() && absolute_border_box_rect().contains(position_adjusted_by_scroll_offset.x(), position_adjusted_by_scroll_offset.y())) { + if (!stacking_context() && is_visible() && absolute_border_box_rect().contains(transformed_position_adjusted_by_scroll_offset.x(), transformed_position_adjusted_by_scroll_offset.y())) { if (callback(HitTestResult { const_cast(*this) }) == TraversalDecision::Break) return TraversalDecision::Break; } diff --git a/Tests/LibWeb/Text/expected/hit_testing/css-transforms.txt b/Tests/LibWeb/Text/expected/hit_testing/css-transforms.txt new file mode 100644 index 000000000000..94ca43332fef --- /dev/null +++ b/Tests/LibWeb/Text/expected/hit_testing/css-transforms.txt @@ -0,0 +1 @@ +clicked the diff --git a/Tests/LibWeb/Text/input/hit_testing/css-transforms.html b/Tests/LibWeb/Text/input/hit_testing/css-transforms.html new file mode 100644 index 000000000000..33efd8fa4091 --- /dev/null +++ b/Tests/LibWeb/Text/input/hit_testing/css-transforms.html @@ -0,0 +1,22 @@ + +
    + +
    + This text should not be hit when clicking the input. +
    +
    + + From f378f4152612fd7dbefb6fa033f220348e83d24a Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Wed, 20 Nov 2024 22:48:40 +0000 Subject: [PATCH 067/397] LibWeb: Use correct comparison logic in `NamedNodeMap::get_attribute()` Previously, we were doing a case insensitive comparison, which could return the wrong result if the attribute name was uppercase. --- Libraries/LibWeb/DOM/NamedNodeMap.cpp | 2 +- .../wpt-import/dom/nodes/attributes.txt | 81 ++ .../wpt-import/dom/nodes/attributes.html | 883 ++++++++++++++++++ .../input/wpt-import/dom/nodes/attributes.js | 18 + .../input/wpt-import/dom/nodes/productions.js | 3 + 5 files changed, 986 insertions(+), 1 deletion(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/dom/nodes/productions.js diff --git a/Libraries/LibWeb/DOM/NamedNodeMap.cpp b/Libraries/LibWeb/DOM/NamedNodeMap.cpp index 237538ca5543..989cd67521e9 100644 --- a/Libraries/LibWeb/DOM/NamedNodeMap.cpp +++ b/Libraries/LibWeb/DOM/NamedNodeMap.cpp @@ -153,7 +153,7 @@ Attr const* NamedNodeMap::get_attribute(FlyString const& qualified_name, size_t* // 2. Return the first attribute in element’s attribute list whose qualified name is qualifiedName; otherwise null. for (auto const& attribute : m_attributes) { if (compare_as_lowercase) { - if (attribute->name().equals_ignoring_ascii_case(qualified_name)) + if (attribute->name().equals_ignoring_ascii_case(qualified_name) && !AK::any_of(attribute->name().bytes(), is_ascii_upper_alpha)) return attribute; } else { if (attribute->name() == qualified_name) diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt new file mode 100644 index 000000000000..aa28082838bb --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt @@ -0,0 +1,81 @@ +Summary + +Harness status: OK + +Rerun + +Found 70 tests + +69 Pass +1 Fail +Details +Result Test Name MessagePass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown. (toggleAttribute) +Pass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown, even if the attribute is already present. (toggleAttribute) +Pass toggleAttribute should lowercase its name argument (upper case attribute) +Pass toggleAttribute should lowercase its name argument (mixed case attribute) +Pass toggleAttribute should not throw even when qualifiedName starts with 'xmlns' +Pass Basic functionality should be intact. (toggleAttribute) +Pass toggleAttribute should not change the order of previously set attributes. +Pass toggleAttribute should set the first attribute with the given name +Pass toggleAttribute should set the attribute with the given qualified name +Pass Toggling element with inline style should make inline style disappear +Pass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown. (setAttribute) +Pass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown, even if the attribute is already present. (setAttribute) +Pass setAttribute should lowercase its name argument (upper case attribute) +Pass setAttribute should lowercase its name argument (mixed case attribute) +Pass setAttribute should not throw even when qualifiedName starts with 'xmlns' +Pass Basic functionality should be intact. +Pass setAttribute should not change the order of previously set attributes. +Pass setAttribute should set the first attribute with the given name +Pass setAttribute should set the attribute with the given qualified name +Pass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown. (setAttributeNS) +Pass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown, even if the attribute is already present. (setAttributeNS) +Pass When qualifiedName does not match the QName production, an INVALID_CHARACTER_ERR exception is to be thrown. +Pass null and the empty string should result in a null namespace. +Pass A namespace is required to use a prefix. +Pass The xml prefix should not be allowed for arbitrary namespaces +Pass XML-namespaced attributes don't need an xml prefix +Pass The xmlns prefix should not be allowed for arbitrary namespaces +Pass The xmlns qualified name should not be allowed for arbitrary namespaces +Pass xmlns should be allowed as local name +Pass The XMLNS namespace should require xmlns as prefix or qualified name +Pass xmlns should be allowed as prefix in the XMLNS namespace +Pass xmlns should be allowed as qualified name in the XMLNS namespace +Pass Setting the same attribute with another prefix should not change the prefix +Pass setAttribute should not throw even if a load is not allowed +Pass Attributes should work in document fragments. +Pass Attribute values should not be parsed. +Pass Specified attributes should be accessible. +Pass Entities in attributes should have been expanded while parsing. +Pass Unset attributes return null +Pass First set attribute is returned by getAttribute +Pass Style attributes are not normalized +Pass Only lowercase attributes are returned on HTML elements (upper case attribute) +Pass Only lowercase attributes are returned on HTML elements (mixed case attribute) +Pass First set attribute is returned with mapped attribute set first +Pass First set attribute is returned with mapped attribute set later +Pass Non-HTML element with upper-case attribute +Pass Attribute with prefix in local name +Pass Attribute loses its owner when removed +Pass Basic functionality of getAttributeNode/getAttributeNodeNS +Pass Basic functionality of setAttributeNode +Pass setAttributeNode should distinguish attributes with same local name and different namespaces +Pass setAttributeNode doesn't have case-insensitivity even with an HTMLElement 1 +Pass setAttributeNode doesn't have case-insensitivity even with an HTMLElement 2 +Pass setAttributeNode doesn't have case-insensitivity even with an HTMLElement 3 +Pass Basic functionality of setAttributeNodeNS +Pass If attr’s element is neither null nor element, throw an InUseAttributeError. +Pass Replacing an attr by itself +Pass Basic functionality of removeAttributeNode +Pass setAttributeNode on bound attribute should throw InUseAttributeError +Pass setAttributeNode, if it fires mutation events, should fire one with the new node when resetting an existing attribute +Pass setAttributeNode, if it fires mutation events, should fire one with the new node when resetting an existing attribute (outer shell) +Pass setAttributeNode called with an Attr that has the same name as an existing one should not change attribute order +Pass getAttributeNames tests +Pass Own property correctness with basic attributes +Pass Own property correctness with non-namespaced attribute before same-name namespaced one +Pass Own property correctness with namespaced attribute before same-name non-namespaced one +Pass Own property correctness with two namespaced attributes with the same name-with-prefix +Pass Own property names should only include all-lowercase qualified names for an HTML element in an HTML document +Pass Own property names should include all qualified names for a non-HTML element in an HTML document +Fail Own property names should include all qualified names for an HTML element in a non-HTML document \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.html b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.html new file mode 100644 index 000000000000..0a7c1abb77c5 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.html @@ -0,0 +1,883 @@ + + +Attributes tests + + + + + + + +
    + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.js b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.js new file mode 100644 index 000000000000..ef32bf6a6773 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/attributes.js @@ -0,0 +1,18 @@ +function attr_is(attr, v, ln, ns, p, n) { + assert_equals(attr.value, v) + assert_equals(attr.nodeValue, v) + assert_equals(attr.textContent, v) + assert_equals(attr.localName, ln) + assert_equals(attr.namespaceURI, ns) + assert_equals(attr.prefix, p) + assert_equals(attr.name, n) + assert_equals(attr.nodeName, n); + assert_equals(attr.specified, true) +} + +function attributes_are(el, l) { + for (var i = 0, il = l.length; i < il; i++) { + attr_is(el.attributes[i], l[i][1], l[i][0], (l[i].length < 3) ? null : l[i][2], null, l[i][0]) + assert_equals(el.attributes[i].ownerElement, el) + } +} diff --git a/Tests/LibWeb/Text/input/wpt-import/dom/nodes/productions.js b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/productions.js new file mode 100644 index 000000000000..218797fc4595 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/dom/nodes/productions.js @@ -0,0 +1,3 @@ +var invalid_names = ["", "invalid^Name", "\\", "'", '"', "0", "0:a"] // XXX +var valid_names = ["x", "X", ":", "a:0"] +var invalid_qnames = [":a", "b:", "x:y:z"] // XXX From 8aaa9324b2b81bf4983bea1a17da45611a0b12c6 Mon Sep 17 00:00:00 2001 From: Psychpsyo Date: Sat, 23 Nov 2024 22:08:36 +0100 Subject: [PATCH 068/397] LibWeb: Unapply CSS filters after applying them to SVG --- Libraries/LibWeb/Painting/SVGSVGPaintable.cpp | 5 +++-- Tests/LibWeb/Ref/expected/svg-restore-after-filter.html | 5 +++++ Tests/LibWeb/Ref/input/svg-restore-after-filter.html | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 Tests/LibWeb/Ref/expected/svg-restore-after-filter.html create mode 100644 Tests/LibWeb/Ref/input/svg-restore-after-filter.html diff --git a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp index 39d45b092059..90f59a86ecfa 100644 --- a/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp +++ b/Libraries/LibWeb/Painting/SVGSVGPaintable.cpp @@ -59,8 +59,9 @@ void SVGSVGPaintable::paint_descendants(PaintContext& context, PaintableBox cons auto paint_svg_box = [&](auto& svg_box) { auto const& computed_values = svg_box.computed_values(); + auto filters = paintable.computed_values().filter(); auto masking_area = svg_box.get_masking_area(); - auto needs_to_save_state = computed_values.opacity() < 1 || svg_box.has_css_transform() || svg_box.get_masking_area().has_value(); + auto needs_to_save_state = computed_values.opacity() < 1 || svg_box.has_css_transform() || svg_box.get_masking_area().has_value() || !filters.is_none(); if (needs_to_save_state) { context.display_list_recorder().save(); @@ -70,7 +71,7 @@ void SVGSVGPaintable::paint_descendants(PaintContext& context, PaintableBox cons context.display_list_recorder().apply_opacity(computed_values.opacity()); } - context.display_list_recorder().apply_filters(paintable.computed_values().filter()); + context.display_list_recorder().apply_filters(filters); if (svg_box.has_css_transform()) { auto transform_matrix = svg_box.transform(); diff --git a/Tests/LibWeb/Ref/expected/svg-restore-after-filter.html b/Tests/LibWeb/Ref/expected/svg-restore-after-filter.html new file mode 100644 index 000000000000..2ddb49cf93bf --- /dev/null +++ b/Tests/LibWeb/Ref/expected/svg-restore-after-filter.html @@ -0,0 +1,5 @@ + + + + +This should be visible. diff --git a/Tests/LibWeb/Ref/input/svg-restore-after-filter.html b/Tests/LibWeb/Ref/input/svg-restore-after-filter.html new file mode 100644 index 000000000000..a605153608eb --- /dev/null +++ b/Tests/LibWeb/Ref/input/svg-restore-after-filter.html @@ -0,0 +1,6 @@ + + + + + +This should be visible. From 54b0476d70fd66edf11062ab2c684f383188a051 Mon Sep 17 00:00:00 2001 From: Milo van der Tier Date: Sat, 23 Nov 2024 20:55:43 +0100 Subject: [PATCH 069/397] LibWeb: Handle second condition in NamedNodeMap's property names This removes the FIXME and fixes a WPT subtest. --- Libraries/LibWeb/DOM/NamedNodeMap.cpp | 3 +-- .../LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/DOM/NamedNodeMap.cpp b/Libraries/LibWeb/DOM/NamedNodeMap.cpp index 989cd67521e9..cd3e123652eb 100644 --- a/Libraries/LibWeb/DOM/NamedNodeMap.cpp +++ b/Libraries/LibWeb/DOM/NamedNodeMap.cpp @@ -61,8 +61,7 @@ Vector NamedNodeMap::supported_property_names() const } // 2. If this NamedNodeMap object’s element is in the HTML namespace and its node document is an HTML document, then for each name in names: - // FIXME: Handle the second condition, assume it is an HTML document for now. - if (associated_element().namespace_uri() == Namespace::HTML) { + if (associated_element().namespace_uri() == Namespace::HTML && associated_element().document().is_html_document()) { // 1. Let lowercaseName be name, in ASCII lowercase. // 2. If lowercaseName is not equal to name, remove name from names. names.remove_all_matching([](auto const& name) { return name != name.to_ascii_lowercase(); }); diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt index aa28082838bb..16e0db53e3b1 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/nodes/attributes.txt @@ -6,8 +6,7 @@ Rerun Found 70 tests -69 Pass -1 Fail +70 Pass Details Result Test Name MessagePass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown. (toggleAttribute) Pass When qualifiedName does not match the Name production, an INVALID_CHARACTER_ERR exception is to be thrown, even if the attribute is already present. (toggleAttribute) @@ -78,4 +77,4 @@ Pass Own property correctness with namespaced attribute before same-name non-nam Pass Own property correctness with two namespaced attributes with the same name-with-prefix Pass Own property names should only include all-lowercase qualified names for an HTML element in an HTML document Pass Own property names should include all qualified names for a non-HTML element in an HTML document -Fail Own property names should include all qualified names for an HTML element in a non-HTML document \ No newline at end of file +Pass Own property names should include all qualified names for an HTML element in a non-HTML document \ No newline at end of file From 5be482550474a4e61f29a8ebbdb9d38dcd4a1ec0 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Sat, 23 Nov 2024 15:42:01 -0700 Subject: [PATCH 070/397] LibWeb: Report exceptions from custom element upgrades to global object --- Libraries/LibWeb/Bindings/MainThreadVM.cpp | 28 +- .../custom-elements-throw-in-constructor.txt | 1 + .../wpt-import/custom-elements/upgrading.txt | 39 +++ .../custom-elements-throw-in-constructor.html | 2 + .../resources/custom-elements-helpers.js | 276 ++++++++++++++++++ .../wpt-import/custom-elements/upgrading.html | 258 ++++++++++++++++ 6 files changed, 594 insertions(+), 10 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/custom-elements/upgrading.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/custom-elements/resources/custom-elements-helpers.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/custom-elements/upgrading.html diff --git a/Libraries/LibWeb/Bindings/MainThreadVM.cpp b/Libraries/LibWeb/Bindings/MainThreadVM.cpp index 05d7137f5ae4..762536d664fe 100644 --- a/Libraries/LibWeb/Bindings/MainThreadVM.cpp +++ b/Libraries/LibWeb/Bindings/MainThreadVM.cpp @@ -739,24 +739,32 @@ void invoke_custom_element_reactions(Vector>& element_que // 1. Remove the first element of reactions, and let reaction be that element. Switch on reaction's type: auto reaction = reactions->take_first(); - auto maybe_exception = reaction.visit( - [&](DOM::CustomElementUpgradeReaction const& custom_element_upgrade_reaction) -> JS::ThrowCompletionOr { + reaction.visit( + [&](DOM::CustomElementUpgradeReaction const& custom_element_upgrade_reaction) -> void { // -> upgrade reaction // Upgrade element using reaction's custom element definition. - return element->upgrade_element(*custom_element_upgrade_reaction.custom_element_definition); + auto maybe_exception = element->upgrade_element(*custom_element_upgrade_reaction.custom_element_definition); + // If this throws an exception, catch it, and report it for reaction's custom element definition's constructor's corresponding JavaScript object's associated realm's global object. + if (maybe_exception.is_error()) { + // FIXME: Should it be easier to get to report an exception from an IDL callback? + auto& callback = custom_element_upgrade_reaction.custom_element_definition->constructor(); + auto& realm = callback.callback->shape().realm(); + auto& global = realm.global_object(); + + auto* window_or_worker = dynamic_cast(&global); + VERIFY(window_or_worker); + window_or_worker->report_an_exception(maybe_exception.error_value()); + } }, - [&](DOM::CustomElementCallbackReaction& custom_element_callback_reaction) -> JS::ThrowCompletionOr { + [&](DOM::CustomElementCallbackReaction& custom_element_callback_reaction) -> void { // -> callback reaction // Invoke reaction's callback function with reaction's arguments, and with element as the callback this value. auto result = WebIDL::invoke_callback(*custom_element_callback_reaction.callback, element.ptr(), custom_element_callback_reaction.arguments); + // FIXME: The error from CustomElementCallbackReaction is supposed + // to use the new steps for IDL callback error reporting if (result.is_abrupt()) - return result.release_error(); - return {}; + HTML::report_exception(result, element->realm()); }); - - // If this throws an exception, catch it, and report the exception. - if (maybe_exception.is_throw_completion()) - HTML::report_exception(maybe_exception, element->realm()); } } } diff --git a/Tests/LibWeb/Text/expected/HTML/custom-elements-throw-in-constructor.txt b/Tests/LibWeb/Text/expected/HTML/custom-elements-throw-in-constructor.txt index 56cb91d69ec4..45ccb2b1c21f 100644 --- a/Tests/LibWeb/Text/expected/HTML/custom-elements-throw-in-constructor.txt +++ b/Tests/LibWeb/Text/expected/HTML/custom-elements-throw-in-constructor.txt @@ -1,2 +1,3 @@ Entered TestElement constructor, throwing. +Uncaught exception: test PASS! (Didn't crash) diff --git a/Tests/LibWeb/Text/expected/wpt-import/custom-elements/upgrading.txt b/Tests/LibWeb/Text/expected/wpt-import/custom-elements/upgrading.txt new file mode 100644 index 000000000000..46634c20732a --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/custom-elements/upgrading.txt @@ -0,0 +1,39 @@ +Summary + +Harness status: OK + +Rerun + +Found 28 tests + +25 Pass +3 Fail +Details +Result Test Name MessagePass Creating an element in the document of the template elements must not enqueue a custom element upgrade reaction because the document does not have a browsing context +Pass Creating an element in the document of the template elements and inserting into the document must not enqueue a custom element upgrade reaction +Pass Creating an element in the document of the template elements and adopting back to a document with browsing context must enqueue a custom element upgrade reaction +Pass Creating an element in a new document must not enqueue a custom element upgrade reaction because the document does not have a browsing context +Pass Creating an element in a new document and inserting into the document must not enqueue a custom element upgrade reaction +Pass Creating an element in a new document and adopting back to a document with browsing context must enqueue a custom element upgrade reaction +Pass Creating an element in a cloned document must not enqueue a custom element upgrade reaction because the document does not have a browsing context +Pass Creating an element in a cloned document and inserting into the document must not enqueue a custom element upgrade reaction +Pass Creating an element in a cloned document and adopting back to a document with browsing context must enqueue a custom element upgrade reaction +Pass Creating an element in a document created by createHTMLDocument must not enqueue a custom element upgrade reaction because the document does not have a browsing context +Pass Creating an element in a document created by createHTMLDocument and inserting into the document must not enqueue a custom element upgrade reaction +Pass Creating an element in a document created by createHTMLDocument and adopting back to a document with browsing context must enqueue a custom element upgrade reaction +Pass Creating an element in an HTML document created by createDocument must not enqueue a custom element upgrade reaction because the document does not have a browsing context +Pass Creating an element in an HTML document created by createDocument and inserting into the document must not enqueue a custom element upgrade reaction +Pass Creating an element in an HTML document created by createDocument and adopting back to a document with browsing context must enqueue a custom element upgrade reaction +Fail Creating an element in an HTML document fetched by XHR must not enqueue a custom element upgrade reaction because the document does not have a browsing context +Fail Creating an element in an HTML document fetched by XHR and inserting into the document must not enqueue a custom element upgrade reaction +Fail Creating an element in an HTML document fetched by XHR and adopting back to a document with browsing context must enqueue a custom element upgrade reaction +Pass Creating an element in the document of an iframe must not enqueue a custom element upgrade reaction if there is no matching definition +Pass Creating an element in the document of an iframe must enqueue a custom element upgrade reaction if there is a matching definition +Pass "define" in the document of an iframe must not enqueue a custom element upgrade reaction on a disconnected unresolved custom element +Pass Inserting an unresolved custom element into the document of an iframe must enqueue a custom element upgrade reaction +Pass "define" in the document of an iframe must enqueue a custom element upgrade reaction on a connected unresolved custom element +Pass Adopting (and leaving disconnceted) an unresolved custom element into the document of an iframe must not enqueue a custom element upgrade reaction +Pass Adopting and inserting an unresolved custom element into the document of an iframe must enqueue a custom element upgrade reaction +Pass If definition's disable shadow is true and element's shadow root is non-null, then throw a "NotSupportedError" DOMException. +Pass Infinite constructor recursion with upgrade(this) should not be possible +Pass Infinite constructor recursion with appendChild should not be possible \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/HTML/custom-elements-throw-in-constructor.html b/Tests/LibWeb/Text/input/HTML/custom-elements-throw-in-constructor.html index 39481e5093e2..b32c1bffbc1b 100644 --- a/Tests/LibWeb/Text/input/HTML/custom-elements-throw-in-constructor.html +++ b/Tests/LibWeb/Text/input/HTML/custom-elements-throw-in-constructor.html @@ -2,6 +2,8 @@ + + + + + + + +
    + + + From c83a3db542d57236efbfba362f136a7ef9246617 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 23 Nov 2024 09:29:20 -0500 Subject: [PATCH 071/397] LibJS: Handle Temporal.PlainDate in dynamic calendar operations --- Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp | 2 ++ Libraries/LibJS/Runtime/Temporal/Calendar.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 8f01fabd29d8..3223eb696f9b 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -624,6 +624,8 @@ ThrowCompletionOr is_partial_temporal_object(VM& vm, Value value) // [[InitializedTemporalTime]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal // slot, return false. // FIXME: Add the other types as we define them. + if (is(object)) + return false; if (is(object)) return false; if (is(object)) diff --git a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp index fb185cfbeccf..34083508d492 100644 --- a/Libraries/LibJS/Runtime/Temporal/Calendar.cpp +++ b/Libraries/LibJS/Runtime/Temporal/Calendar.cpp @@ -452,6 +452,8 @@ ThrowCompletionOr to_temporal_calendar_identifier(VM& vm, Value temporal // internal slot, then // i. Return temporalCalendarLike.[[Calendar]]. // FIXME: Add the other calendar-holding types as we define them. + if (is(temporal_calendar_object)) + return static_cast(temporal_calendar_object).calendar(); if (is(temporal_calendar_object)) return static_cast(temporal_calendar_object).calendar(); if (is(temporal_calendar_object)) @@ -476,6 +478,8 @@ ThrowCompletionOr get_temporal_calendar_identifier_with_iso_default(VM& // [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then // a. Return item.[[Calendar]]. // FIXME: Add the other calendar-holding types as we define them. + if (is(item)) + return static_cast(item).calendar(); if (is(item)) return static_cast(item).calendar(); if (is(item)) From 0b59971ef9174705f39aea888823dfeec7574e48 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 23 Nov 2024 10:11:55 -0500 Subject: [PATCH 072/397] LibJS: Reject ambiguous annotated time strings This missing rejection will be caught by an upcoming Temporal.PlainTime test. --- .../LibJS/Runtime/Temporal/AbstractOperations.cpp | 4 +++- Libraries/LibJS/Runtime/Temporal/ISO8601.cpp | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 3223eb696f9b..1063561a5658 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -1017,7 +1017,9 @@ ThrowCompletionOr parse_iso_date_time(VM& vm, StringView iso_ // 4. If goal is TemporalMonthDayString and parseResult does not contain a DateYear Parse Node, then if (goal == Production::TemporalMonthDayString && !parse_result->date_year.has_value()) { // a. Assert: goal is the last element of allowedFormats. - VERIFY(goal == allowed_formats.last()); + // FIXME: Spec issue: This assertion is possibly incorrect. + // https://github.com/tc39/proposal-temporal/issues/3045 + // VERIFY(goal == allowed_formats.last()); // b. Set yearAbsent to true. year_absent = true; diff --git a/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp b/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp index 60cbd93b55de..852d35fdc530 100644 --- a/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp +++ b/Libraries/LibJS/Runtime/Temporal/ISO8601.cpp @@ -199,7 +199,16 @@ class ISO8601Parser { // AnnotatedTime ::: // TimeDesignator Time DateTimeUTCOffset[~Z][opt] TimeZoneAnnotation[opt] Annotations[opt] // Time DateTimeUTCOffset[~Z][opt] TimeZoneAnnotation[opt] Annotations[opt] - (void)parse_time_designator(); + auto has_time_designator = parse_time_designator(); + + if (!has_time_designator) { + StateTransaction transaction { *this }; + + // It is a Syntax Error if ParseText(Time DateTimeUTCOffset[~Z], DateSpecMonthDay) is a Parse Node. + // It is a Syntax Error if ParseText(Time DateTimeUTCOffset[~Z], DateSpecYearMonth) is a Parse Node. + if (parse_date_spec_month_day() || parse_date_spec_year_month()) + return false; + } if (!parse_time()) return false; From 7f3de3be9c5daa8b72c6050e34975286179d4b58 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 23 Nov 2024 11:03:07 -0500 Subject: [PATCH 073/397] LibJS: Capture sub-second nanoseconds as a u64 in FormatTimeString This is not a plain nanosecond value (which has a range of [0, 999]). Rather, it is all of the sub-second components added together. --- Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp | 2 +- Libraries/LibJS/Runtime/Temporal/AbstractOperations.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp index 1063561a5658..0d5e34d85c5c 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp @@ -684,7 +684,7 @@ String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision } // 13.25 FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formattimestring -String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision precision, Optional style) +String format_time_string(u8 hour, u8 minute, u8 second, u64 sub_second_nanoseconds, SecondsStringPrecision::Precision precision, Optional style) { // 1. If style is present and style is UNSEPARATED, let separator be the empty String; otherwise, let separator be ":". auto separator = style == TimeStyle::Unseparated ? ""sv : ":"sv; diff --git a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h index 7f3e2b21c9bd..7b171cd66ea7 100644 --- a/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h +++ b/Libraries/LibJS/Runtime/Temporal/AbstractOperations.h @@ -158,7 +158,7 @@ RoundingIncrement maximum_temporal_duration_rounding_increment(Unit); Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit); ThrowCompletionOr is_partial_temporal_object(VM&, Value); String format_fractional_seconds(u64, Precision); -String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional = {}); +String format_time_string(u8 hour, u8 minute, u8 second, u64 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional = {}); UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign); double apply_unsigned_rounding_mode(double, double r1, double r2, UnsignedRoundingMode); Crypto::SignedBigInteger apply_unsigned_rounding_mode(Crypto::SignedDivisionResult const&, Crypto::SignedBigInteger const& r1, Crypto::SignedBigInteger const& r2, UnsignedRoundingMode, Crypto::UnsignedBigInteger const& increment); From 971f794127c9a34a98a2f96b71bd8b6da8cadec8 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 23 Nov 2024 13:26:36 -0500 Subject: [PATCH 074/397] LibJS: Add and use a BigInt-aware override of BalanceTime With Temporal.PlainTime, we will need precise results for extremely large values. --- .../LibJS/Runtime/Temporal/PlainTime.cpp | 59 ++++++++++++++++++- Libraries/LibJS/Runtime/Temporal/PlainTime.h | 1 + 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp index 334383f41b73..3dcf5e3c0102 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -15,6 +15,20 @@ namespace JS::Temporal { +// FIXME: We should add a generic floor() method to our BigInt classes. But for now, since we know we are only dividing +// by powers of 10, we can implement a very situationally specific method to compute the floor of a division. +static TimeDuration big_floor(TimeDuration const& numerator, Crypto::UnsignedBigInteger const& denominator) +{ + auto result = numerator.divided_by(denominator); + + if (result.remainder.is_zero()) + return result.quotient; + if (!result.quotient.is_negative() && result.remainder.is_positive()) + return result.quotient; + + return result.quotient.minus(TimeDuration { 1 }); +} + // 4.5.2 CreateTimeRecord ( hour, minute, second, millisecond, microsecond, nanosecond [ , deltaDays ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtimerecord Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days) { @@ -205,6 +219,49 @@ Time balance_time(double hour, double minute, double second, double millisecond, return create_time_record(hour, minute, second, millisecond, microsecond, nanosecond, delta_days); } +// 4.5.10 BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-balancetime +Time balance_time(double hour, double minute, double second, double millisecond, double microsecond, TimeDuration const& nanosecond_value) +{ + // 1. Set microsecond to microsecond + floor(nanosecond / 1000). + auto microsecond_value = TimeDuration { microsecond }.plus(big_floor(nanosecond_value, NANOSECONDS_PER_MICROSECOND)); + + // 2. Set nanosecond to nanosecond modulo 1000. + auto nanosecond = modulo(nanosecond_value, NANOSECONDS_PER_MICROSECOND).to_double(); + + // 3. Set millisecond to millisecond + floor(microsecond / 1000). + auto millisecond_value = TimeDuration { millisecond }.plus(big_floor(microsecond_value, MICROSECONDS_PER_MILLISECOND)); + + // 4. Set microsecond to microsecond modulo 1000. + microsecond = modulo(microsecond_value, MICROSECONDS_PER_MILLISECOND).to_double(); + + // 5. Set second to second + floor(millisecond / 1000). + auto second_value = TimeDuration { second }.plus(big_floor(millisecond_value, MILLISECONDS_PER_SECOND)); + + // 6. Set millisecond to millisecond modulo 1000. + millisecond = modulo(millisecond_value, MILLISECONDS_PER_SECOND).to_double(); + + // 7. Set minute to minute + floor(second / 60). + auto minute_value = TimeDuration { minute }.plus(big_floor(second_value, SECONDS_PER_MINUTE)); + + // 8. Set second to second modulo 60. + second = modulo(second_value, SECONDS_PER_MINUTE).to_double(); + + // 9. Set hour to hour + floor(minute / 60). + auto hour_value = TimeDuration { hour }.plus(big_floor(minute_value, MINUTES_PER_HOUR)); + + // 10. Set minute to minute modulo 60. + minute = modulo(minute_value, MINUTES_PER_HOUR).to_double(); + + // 11. Let deltaDays be floor(hour / 24). + auto delta_days = big_floor(hour_value, HOURS_PER_DAY).to_double(); + + // 12. Set hour to hour modulo 24. + hour = modulo(hour_value, HOURS_PER_DAY).to_double(); + + // 13. Return CreateTimeRecord(hour, minute, second, millisecond, microsecond, nanosecond, deltaDays). + return create_time_record(hour, minute, second, millisecond, microsecond, nanosecond, delta_days); +} + // 4.5.14 CompareTimeRecord ( time1, time2 ), https://tc39.es/proposal-temporal/#sec-temporal-comparetimerecord i8 compare_time_record(Time const& time1, Time const& time2) { @@ -260,7 +317,7 @@ Time add_time(Time const& time, TimeDuration const& time_duration) auto nanoseconds = time_duration.plus(TimeDuration { static_cast(time.nanosecond) }); // 1. Return BalanceTime(time.[[Hour]], time.[[Minute]], time.[[Second]], time.[[Millisecond]], time.[[Microsecond]], time.[[Nanosecond]] + timeDuration). - return balance_time(time.hour, time.minute, time.second, time.millisecond, time.microsecond, nanoseconds.to_double()); + return balance_time(time.hour, time.minute, time.second, time.millisecond, time.microsecond, nanoseconds); } } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Libraries/LibJS/Runtime/Temporal/PlainTime.h index a3cec83ba5b7..84e149ab7f50 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.h +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.h @@ -20,6 +20,7 @@ TimeDuration difference_time(Time const&, Time const&); ThrowCompletionOr