LibWeb: Support autocomplete attribute on form elements

Implement proper support for the `autocomplete` attribute in `input`,
`select` and `textarea` elements.
This commit is contained in:
devgianlu 2025-02-09 15:56:54 +01:00 committed by Tim Ledbetter
commit b8f234719d
Notes: github-actions[bot] 2025-02-26 07:02:12 +00:00
11 changed files with 644 additions and 9 deletions

View file

@ -298,6 +298,7 @@ set(SOURCES
HTML/AttributeNames.cpp HTML/AttributeNames.cpp
HTML/AudioTrack.cpp HTML/AudioTrack.cpp
HTML/AudioTrackList.cpp HTML/AudioTrackList.cpp
HTML/AutocompleteElement.cpp
HTML/BeforeUnloadEvent.cpp HTML/BeforeUnloadEvent.cpp
HTML/BroadcastChannel.cpp HTML/BroadcastChannel.cpp
HTML/BrowsingContext.cpp HTML/BrowsingContext.cpp

View file

@ -0,0 +1,369 @@
/*
* Copyright (c) 2025, Altomani Gianluca <altomanigianluca@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/HTML/AttributeNames.h>
#include <LibWeb/HTML/AutocompleteElement.h>
#include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/HTMLFormElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/Infra/CharacterTypes.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-expectation-mantle
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-anchor-mantle
AutocompleteElement::AutofillMantle AutocompleteElement::get_autofill_mantle() const
{
auto const& element = autocomplete_element_to_html_element();
// On an input element whose type attribute is in the Hidden state, the autocomplete attribute wears the autofill anchor mantle.
if (is<HTMLInputElement>(element)) {
auto const& input_element = as<HTMLInputElement>(element);
if (input_element.type_state() == HTMLInputElement::TypeAttributeState::Hidden)
return AutofillMantle::Anchor;
}
// In all other cases, it wears the autofill expectation mantle.
return AutofillMantle::Expectation;
}
Vector<String> AutocompleteElement::autocomplete_tokens() const
{
auto autocomplete_value = autocomplete_element_to_html_element().attribute(AttributeNames::autocomplete).value_or({});
Vector<String> autocomplete_tokens;
for (auto& token : autocomplete_value.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace))
autocomplete_tokens.append(MUST(String::from_utf8(token)));
return autocomplete_tokens;
}
String AutocompleteElement::autocomplete() const
{
// The autocomplete IDL attribute, on getting, must return the element's IDL-exposed autofill value.
auto details = parse_autocomplete_attribute();
return details.value;
}
WebIDL::ExceptionOr<void> AutocompleteElement::set_autocomplete(String const& value)
{
// The autocomplete IDL attribute [...] on setting, must reflect the content attribute of the same name.
TRY(autocomplete_element_to_html_element().set_attribute(AttributeNames::autocomplete, value));
return {};
}
enum class Category {
Off,
Automatic,
Normal,
Contact,
Credential,
};
struct CategoryAndMaximumTokens {
Optional<Category> category;
Optional<size_t> maximum_tokens;
};
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#determine-a-field's-category
static CategoryAndMaximumTokens determine_a_field_category(StringView const& field)
{
#define CASE_CATEGORY(token, maximum_number_of_tokens, category) \
if (field.equals_ignoring_ascii_case(token)) \
return CategoryAndMaximumTokens { Category::category, maximum_number_of_tokens };
// 1. If the field is not an ASCII case-insensitive match for one of the tokens given
// in the first column of the following table, return the pair (null, null).
// 2. Otherwise, let maximum tokens and category be the values of the cells in the second
// and third columns of that row respectively.
// 3. Return the pair (category, maximum tokens).
CASE_CATEGORY("off"sv, 1, Off);
CASE_CATEGORY("on"sv, 1, Automatic);
CASE_CATEGORY("name"sv, 3, Normal);
CASE_CATEGORY("honorific-prefix"sv, 3, Normal);
CASE_CATEGORY("given-name"sv, 3, Normal);
CASE_CATEGORY("additional-name"sv, 3, Normal);
CASE_CATEGORY("family-name"sv, 3, Normal);
CASE_CATEGORY("honorific-suffix"sv, 3, Normal);
CASE_CATEGORY("nickname"sv, 3, Normal);
CASE_CATEGORY("organization-title"sv, 3, Normal);
CASE_CATEGORY("username"sv, 3, Normal);
CASE_CATEGORY("new-password"sv, 3, Normal);
CASE_CATEGORY("current-password"sv, 3, Normal);
CASE_CATEGORY("one-time-code"sv, 3, Normal);
CASE_CATEGORY("organization"sv, 3, Normal);
CASE_CATEGORY("street-address"sv, 3, Normal);
CASE_CATEGORY("address-line1"sv, 3, Normal);
CASE_CATEGORY("address-line2"sv, 3, Normal);
CASE_CATEGORY("address-line3"sv, 3, Normal);
CASE_CATEGORY("address-level4"sv, 3, Normal);
CASE_CATEGORY("address-level3"sv, 3, Normal);
CASE_CATEGORY("address-level2"sv, 3, Normal);
CASE_CATEGORY("address-level1"sv, 3, Normal);
CASE_CATEGORY("country"sv, 3, Normal);
CASE_CATEGORY("country-name"sv, 3, Normal);
CASE_CATEGORY("postal-code"sv, 3, Normal);
CASE_CATEGORY("cc-name"sv, 3, Normal);
CASE_CATEGORY("cc-given-name"sv, 3, Normal);
CASE_CATEGORY("cc-additional-name"sv, 3, Normal);
CASE_CATEGORY("cc-family-name"sv, 3, Normal);
CASE_CATEGORY("cc-number"sv, 3, Normal);
CASE_CATEGORY("cc-exp"sv, 3, Normal);
CASE_CATEGORY("cc-exp-month"sv, 3, Normal);
CASE_CATEGORY("cc-exp-year"sv, 3, Normal);
CASE_CATEGORY("cc-csc"sv, 3, Normal);
CASE_CATEGORY("cc-type"sv, 3, Normal);
CASE_CATEGORY("transaction-currency"sv, 3, Normal);
CASE_CATEGORY("transaction-amount"sv, 3, Normal);
CASE_CATEGORY("language"sv, 3, Normal);
CASE_CATEGORY("bday"sv, 3, Normal);
CASE_CATEGORY("bday-day"sv, 3, Normal);
CASE_CATEGORY("bday-month"sv, 3, Normal);
CASE_CATEGORY("bday-year"sv, 3, Normal);
CASE_CATEGORY("sex"sv, 3, Normal);
CASE_CATEGORY("url"sv, 3, Normal);
CASE_CATEGORY("photo"sv, 3, Normal);
CASE_CATEGORY("tel"sv, 4, Contact);
CASE_CATEGORY("tel-country-code"sv, 4, Contact);
CASE_CATEGORY("tel-national"sv, 4, Contact);
CASE_CATEGORY("tel-area-code"sv, 4, Contact);
CASE_CATEGORY("tel-local"sv, 4, Contact);
CASE_CATEGORY("tel-local-prefix"sv, 4, Contact);
CASE_CATEGORY("tel-local-suffix"sv, 4, Contact);
CASE_CATEGORY("tel-extension"sv, 4, Contact);
CASE_CATEGORY("email"sv, 4, Contact);
CASE_CATEGORY("impp"sv, 4, Contact);
CASE_CATEGORY("webauthn"sv, 5, Credential);
#undef CASE_CATEGORY
return CategoryAndMaximumTokens {};
}
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-processing-model
AutocompleteElement::AttributeDetails AutocompleteElement::parse_autocomplete_attribute() const
{
AttributeDetails attr_details {};
auto step_default = [&] {
// 32. Default: Let the element's IDL-exposed autofill value be the empty string, and its autofill hint set and autofill scope be empty.
attr_details.value = {};
attr_details.hint_set = {};
attr_details.scope = {};
// 33. If the element's autocomplete attribute is wearing the autofill anchor mantle,
// then let the element's autofill field name be the empty string and return.
if (get_autofill_mantle() == AutofillMantle::Anchor) {
attr_details.field_name = {};
return attr_details;
}
// 34. Let form be the element's form owner, if any, or null otherwise.
auto const* form = as<FormAssociatedElement const>(autocomplete_element_to_html_element()).form();
// 35. If form is not null and form's autocomplete attribute is in the off state, then let the element's autofill field name be "off".
if (form && form->attribute(AttributeNames::autocomplete) == idl_enum_to_string(Bindings::Autocomplete::Off)) {
attr_details.field_name = "off"_string;
}
// Otherwise, let the element's autofill field name be "on".
else {
attr_details.field_name = "on"_string;
}
return attr_details;
};
// 1. If the element has no autocomplete attribute, then jump to the step labeled default.
if (!autocomplete_element_to_html_element().has_attribute(AttributeNames::autocomplete))
return step_default();
// 2. Let tokens be the result of splitting the attribute's value on ASCII whitespace.
auto tokens = autocomplete_tokens();
// 3. If tokens is empty, then jump to the step labeled default.
if (tokens.is_empty())
return step_default();
// 4. Let index be the index of the last token in tokens.
auto index = tokens.size() - 1;
// 5. Let field be the indexth token in tokens.
auto const& field = tokens[index];
// 6. Set the category, maximum tokens pair to the result of determining a field's category given field.
auto [category, maximum_tokens] = determine_a_field_category(field);
// 7. If category is null, then jump to the step labeled default.
if (!category.has_value())
return step_default();
// 8. If the number of tokens in tokens is greater than maximum tokens, then jump to the step labeled default.
if (tokens.size() > maximum_tokens.value())
return step_default();
// 9. If category is Off or Automatic but the element's autocomplete attribute is wearing the autofill anchor mantle,
// then jump to the step labeled default.
if ((category == Category::Off || category == Category::Automatic)
&& get_autofill_mantle() == AutofillMantle::Anchor)
return step_default();
// 10. If category is Off, let the element's autofill field name be the string "off", let its autofill hint set be empty,
// and let its IDL-exposed autofill value be the string "off". Then, return.
if (category == Category::Off) {
attr_details.field_name = "off"_string;
attr_details.hint_set = {};
attr_details.value = "off"_string;
return attr_details;
}
// 11. If category is Automatic, let the element's autofill field name be the string "on", let its autofill hint set be empty,
// and let its IDL-exposed autofill value be the string "on". Then, return.
if (category == Category::Automatic) {
attr_details.field_name = "on"_string;
attr_details.hint_set = {};
attr_details.value = "on"_string;
return attr_details;
}
// 12. Let scope tokens be an empty list.
Vector<String> scope_tokens;
// 13. Let hint tokens be an empty set.
HashTable<String> hint_tokens;
// 14. Let credential type be null.
Optional<String> credential_type;
// 15. Let IDL value have the same value as field.
// NOTE: lowercasing is not mentioned in the spec, but required to pass all WPT tests.
auto idl_value = field.to_ascii_lowercase();
auto step_done = [&] {
// 26. Done: Let the element's autofill hint set be hint tokens.
attr_details.hint_set = hint_tokens.values();
// 27. Let the element's non-autofill credential type be credential type.
attr_details.credential_type = credential_type;
// 28. Let the element's autofill scope be scope tokens.
attr_details.scope = scope_tokens;
// 29. Let the element's autofill field name be field.
attr_details.field_name = field;
// 30. Let the element's IDL-exposed autofill value be IDL value.
attr_details.value = idl_value;
// 31. Return.
return attr_details;
};
// 16. If category is Credential and the indexth token in tokens is an ASCII case-insensitive match for "webauthn",
// then run the substeps that follow:
if (category == Category::Credential && tokens[index].equals_ignoring_ascii_case("webauthn"sv)) {
// 1. Set credential type to "webauthn".
credential_type = "webauthn"_string;
// 2. If the indexth token in tokens is the first entry, then skip to the step labeled done.
if (index == 0)
return step_done();
// 3. Decrement index by one.
--index;
// 4. Set the category, maximum tokens pair to the result of determining a field's category given the indexth token in tokens.
auto category_and_maximum_tokens = determine_a_field_category(tokens[index]);
category = category_and_maximum_tokens.category;
maximum_tokens = category_and_maximum_tokens.maximum_tokens;
// 5. If category is not Normal and category is not Contact, then jump to the step labeled default.
if (category != Category::Normal && category != Category::Contact)
return step_default();
// 6. If index is greater than maximum tokens minus one (i.e. if the number of remaining tokens is greater than maximum tokens),
// then jump to the step labeled default.
if (index > maximum_tokens.value() - 1)
return step_default();
// 7. Set IDL value to the concatenation of the indexth token in tokens, a U+0020 SPACE character, and the previous value of IDL value.
idl_value = MUST(String::formatted("{} {}", tokens[index], idl_value));
}
// 17. If the indexth token in tokens is the first entry, then skip to the step labeled done.
if (index == 0)
return step_done();
// 18. Decrement index by one.
--index;
// 19. If category is Contact and the indexth token in tokens is an ASCII case-insensitive match for one of the strings in the following list,
// then run the substeps that follow:
if (category == Category::Contact && tokens[index].to_ascii_lowercase().is_one_of("home", "work", "mobile", "fax", "pager")) {
// 1. Let contact be the matching string from the list above.
auto contact = tokens[index].to_ascii_lowercase();
// 2. Insert contact at the start of scope tokens.
scope_tokens.prepend(contact);
// 3. Add contact to hint tokens.
hint_tokens.set(contact);
// 4. Let IDL value be the concatenation of contact, a U+0020 SPACE character, and the previous value of IDL value.
idl_value = MUST(String::formatted("{} {}", contact, idl_value));
// 5. If the indexth entry in tokens is the first entry, then skip to the step labeled done.
if (index == 0)
return step_done();
// 6. Decrement index by one.
--index;
}
// 20. If the indexth token in tokens is an ASCII case-insensitive match for one of the strings in the following list,
// then run the substeps that follow:
if (tokens[index].to_ascii_lowercase().is_one_of("shipping", "billing")) {
// 1. Let mode be the matching string from the list above.
auto mode = tokens[index].to_ascii_lowercase();
// 2. Insert mode at the start of scope tokens.
scope_tokens.prepend(mode);
// 3. Add mode to hint tokens.
hint_tokens.set(mode);
// 4. Let IDL value be the concatenation of mode, a U+0020 SPACE character, and the previous value of IDL value.
idl_value = MUST(String::formatted("{} {}", mode, idl_value));
// 5. If the indexth entry in tokens is the first entry, then skip to the step labeled done.
if (index == 0)
return step_done();
// 6. Decrement index by one.
--index;
}
// 21. If the indexth entry in tokens is not the first entry, then jump to the step labeled default.
if (index != 0)
return step_default();
// 22. If the first eight characters of the indexth token in tokens are not an ASCII case-insensitive match for the string "section-",
// then jump to the step labeled default.
if (!tokens[index].to_ascii_lowercase().starts_with_bytes("section-"sv))
return step_default();
// 23. Let section be the indexth token in tokens, converted to ASCII lowercase.
auto section = tokens[index].to_ascii_lowercase();
// 24. Insert section at the start of scope tokens.
scope_tokens.prepend(section);
// 25. Let IDL value be the concatenation of section, a U+0020 SPACE character, and the previous value of IDL value.
idl_value = MUST(String::formatted("{} {}", section, idl_value));
return step_done();
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2025, Altomani Gianluca <altomanigianluca@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/HTML/FormAssociatedElement.h>
namespace Web::HTML {
#define AUTOCOMPLETE_ELEMENT(ElementBaseClass, ElementClass) \
private: \
virtual HTMLElement& autocomplete_element_to_html_element() override \
{ \
static_assert(IsBaseOf<HTMLElement, ElementClass>); \
static_assert(IsBaseOf<FormAssociatedElement, ElementClass>); \
return *this; \
}
class AutocompleteElement {
public:
enum class AutofillMantle {
Anchor,
Expectation,
};
AutofillMantle get_autofill_mantle() const;
Vector<String> autocomplete_tokens() const;
String autocomplete() const;
WebIDL::ExceptionOr<void> set_autocomplete(String const&);
// Each input element to which the autocomplete attribute applies [...] has
// an autofill hint set, an autofill scope, an autofill field name,
// a non-autofill credential type, and an IDL-exposed autofill value.
struct AttributeDetails {
Vector<String> hint_set;
Vector<String> scope;
String field_name;
Optional<String> credential_type;
String value;
};
AttributeDetails parse_autocomplete_attribute() const;
virtual HTMLElement& autocomplete_element_to_html_element() = 0;
HTMLElement const& autocomplete_element_to_html_element() const { return const_cast<AutocompleteElement&>(*this).autocomplete_element_to_html_element(); }
protected:
AutocompleteElement() = default;
virtual ~AutocompleteElement() = default;
};
}

View file

@ -12,6 +12,7 @@
#include <LibWeb/DOM/DocumentLoadEventDelayer.h> #include <LibWeb/DOM/DocumentLoadEventDelayer.h>
#include <LibWeb/DOM/Text.h> #include <LibWeb/DOM/Text.h>
#include <LibWeb/FileAPI/FileList.h> #include <LibWeb/FileAPI/FileList.h>
#include <LibWeb/HTML/AutocompleteElement.h>
#include <LibWeb/HTML/ColorPickerUpdateState.h> #include <LibWeb/HTML/ColorPickerUpdateState.h>
#include <LibWeb/HTML/FileFilter.h> #include <LibWeb/HTML/FileFilter.h>
#include <LibWeb/HTML/FormAssociatedElement.h> #include <LibWeb/HTML/FormAssociatedElement.h>
@ -52,10 +53,12 @@ class HTMLInputElement final
: public HTMLElement : public HTMLElement
, public FormAssociatedTextControlElement , public FormAssociatedTextControlElement
, public Layout::ImageProvider , public Layout::ImageProvider
, public PopoverInvokerElement { , public PopoverInvokerElement
, public AutocompleteElement {
WEB_PLATFORM_OBJECT(HTMLInputElement, HTMLElement); WEB_PLATFORM_OBJECT(HTMLInputElement, HTMLElement);
GC_DECLARE_ALLOCATOR(HTMLInputElement); GC_DECLARE_ALLOCATOR(HTMLInputElement);
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLInputElement) FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLInputElement);
AUTOCOMPLETE_ELEMENT(HTMLElement, HTMLInputElement);
public: public:
virtual ~HTMLInputElement() override; virtual ~HTMLInputElement() override;

View file

@ -11,7 +11,7 @@ interface HTMLInputElement : HTMLElement {
[CEReactions, Reflect] attribute DOMString accept; [CEReactions, Reflect] attribute DOMString accept;
[CEReactions, Reflect] attribute DOMString alt; [CEReactions, Reflect] attribute DOMString alt;
[CEReactions, Enumerated=Autocomplete, Reflect] attribute DOMString autocomplete; [CEReactions] attribute DOMString autocomplete;
[CEReactions, Reflect=checked] attribute boolean defaultChecked; [CEReactions, Reflect=checked] attribute boolean defaultChecked;
[ImplementedAs=checked_binding] attribute boolean checked; [ImplementedAs=checked_binding] attribute boolean checked;
[CEReactions, Reflect=dirname] attribute DOMString dirName; [CEReactions, Reflect=dirname] attribute DOMString dirName;

View file

@ -9,6 +9,7 @@
#pragma once #pragma once
#include <LibWeb/HTML/AutocompleteElement.h>
#include <LibWeb/HTML/FormAssociatedElement.h> #include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h> #include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/HTML/HTMLOptionsCollection.h> #include <LibWeb/HTML/HTMLOptionsCollection.h>
@ -19,10 +20,12 @@ namespace Web::HTML {
class HTMLSelectElement final class HTMLSelectElement final
: public HTMLElement : public HTMLElement
, public FormAssociatedElement { , public FormAssociatedElement
, public AutocompleteElement {
WEB_PLATFORM_OBJECT(HTMLSelectElement, HTMLElement); WEB_PLATFORM_OBJECT(HTMLSelectElement, HTMLElement);
GC_DECLARE_ALLOCATOR(HTMLSelectElement); GC_DECLARE_ALLOCATOR(HTMLSelectElement);
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLSelectElement) FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLSelectElement);
AUTOCOMPLETE_ELEMENT(HTMLElement, HTMLSelectElement);
public: public:
virtual ~HTMLSelectElement() override; virtual ~HTMLSelectElement() override;

View file

@ -8,7 +8,7 @@
interface HTMLSelectElement : HTMLElement { interface HTMLSelectElement : HTMLElement {
[HTMLConstructor] constructor(); [HTMLConstructor] constructor();
[CEReactions, Enumerated=Autocomplete, Reflect] attribute DOMString autocomplete; [CEReactions] attribute DOMString autocomplete;
[CEReactions, Reflect] attribute boolean disabled; [CEReactions, Reflect] attribute boolean disabled;
readonly attribute HTMLFormElement? form; readonly attribute HTMLFormElement? form;
[CEReactions, Reflect] attribute boolean multiple; [CEReactions, Reflect] attribute boolean multiple;

View file

@ -12,6 +12,7 @@
#include <LibCore/Timer.h> #include <LibCore/Timer.h>
#include <LibWeb/ARIA/Roles.h> #include <LibWeb/ARIA/Roles.h>
#include <LibWeb/DOM/Text.h> #include <LibWeb/DOM/Text.h>
#include <LibWeb/HTML/AutocompleteElement.h>
#include <LibWeb/HTML/FormAssociatedElement.h> #include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h> #include <LibWeb/HTML/HTMLElement.h>
#include <LibWeb/WebIDL/Types.h> #include <LibWeb/WebIDL/Types.h>
@ -20,10 +21,12 @@ namespace Web::HTML {
class HTMLTextAreaElement final class HTMLTextAreaElement final
: public HTMLElement : public HTMLElement
, public FormAssociatedTextControlElement { , public FormAssociatedTextControlElement
, public AutocompleteElement {
WEB_PLATFORM_OBJECT(HTMLTextAreaElement, HTMLElement); WEB_PLATFORM_OBJECT(HTMLTextAreaElement, HTMLElement);
GC_DECLARE_ALLOCATOR(HTMLTextAreaElement); GC_DECLARE_ALLOCATOR(HTMLTextAreaElement);
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLTextAreaElement) FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLTextAreaElement);
AUTOCOMPLETE_ELEMENT(HTMLElement, HTMLTextAreaElement);
public: public:
virtual ~HTMLTextAreaElement() override; virtual ~HTMLTextAreaElement() override;

View file

@ -7,7 +7,7 @@
interface HTMLTextAreaElement : HTMLElement { interface HTMLTextAreaElement : HTMLElement {
[HTMLConstructor] constructor(); [HTMLConstructor] constructor();
[CEReactions, Enumerated=Autocomplete, Reflect] attribute DOMString autocomplete; [CEReactions] attribute DOMString autocomplete;
[CEReactions] attribute unsigned long cols; [CEReactions] attribute unsigned long cols;
[CEReactions, Reflect=dirname] attribute DOMString dirName; [CEReactions, Reflect=dirname] attribute DOMString dirName;
[CEReactions, Reflect] attribute boolean disabled; [CEReactions, Reflect] attribute boolean disabled;

View file

@ -0,0 +1,72 @@
Harness status: OK
Found 67 tests
67 Pass
Pass form autocomplete attribute missing
Pass form autocomplete attribute on
Pass form autocomplete attribute off
Pass form autocomplete attribute invalid
Pass on is an allowed autocomplete field name
Pass off is an allowed autocomplete field name
Pass name is an allowed autocomplete field name
Pass honorific-prefix is an allowed autocomplete field name
Pass given-name is an allowed autocomplete field name
Pass additional-name is an allowed autocomplete field name
Pass family-name is an allowed autocomplete field name
Pass honorific-suffix is an allowed autocomplete field name
Pass nickname is an allowed autocomplete field name
Pass username is an allowed autocomplete field name
Pass new-password is an allowed autocomplete field name
Pass current-password is an allowed autocomplete field name
Pass one-time-code is an allowed autocomplete field name
Pass organization-title is an allowed autocomplete field name
Pass organization is an allowed autocomplete field name
Pass street-address is an allowed autocomplete field name
Pass address-line1 is an allowed autocomplete field name
Pass address-line2 is an allowed autocomplete field name
Pass address-line3 is an allowed autocomplete field name
Pass address-level4 is an allowed autocomplete field name
Pass address-level3 is an allowed autocomplete field name
Pass address-level2 is an allowed autocomplete field name
Pass address-level1 is an allowed autocomplete field name
Pass country is an allowed autocomplete field name
Pass country-name is an allowed autocomplete field name
Pass postal-code is an allowed autocomplete field name
Pass cc-name is an allowed autocomplete field name
Pass cc-given-name is an allowed autocomplete field name
Pass cc-additional-name is an allowed autocomplete field name
Pass cc-family-name is an allowed autocomplete field name
Pass cc-number is an allowed autocomplete field name
Pass cc-exp is an allowed autocomplete field name
Pass cc-exp-month is an allowed autocomplete field name
Pass cc-exp-year is an allowed autocomplete field name
Pass cc-csc is an allowed autocomplete field name
Pass cc-type is an allowed autocomplete field name
Pass transaction-currency is an allowed autocomplete field name
Pass transaction-amount is an allowed autocomplete field name
Pass language is an allowed autocomplete field name
Pass bday is an allowed autocomplete field name
Pass bday-day is an allowed autocomplete field name
Pass bday-month is an allowed autocomplete field name
Pass bday-year is an allowed autocomplete field name
Pass sex is an allowed autocomplete field name
Pass url is an allowed autocomplete field name
Pass photo is an allowed autocomplete field name
Pass tel is an allowed autocomplete field name
Pass tel-country-code is an allowed autocomplete field name
Pass tel-national is an allowed autocomplete field name
Pass tel-area-code is an allowed autocomplete field name
Pass tel-local is an allowed autocomplete field name
Pass tel-local-prefix is an allowed autocomplete field name
Pass tel-local-suffix is an allowed autocomplete field name
Pass tel-extension is an allowed autocomplete field name
Pass email is an allowed autocomplete field name
Pass impp is an allowed autocomplete field name
Pass webauthn is an allowed autocomplete field name
Pass Test whitespace-only attribute value
Pass Test maximum number of tokens
Pass Unknown field
Pass Test 'wearing the autofill anchor mantle' with off/on
Pass Serialize combinations of section, mode, contact, and field
Pass Serialize combinations of section, mode, contact, field, and credential

View file

@ -0,0 +1,130 @@
<!DOCTYPE html>
<meta charset=utf-8>
<title>form autocomplete attribute</title>
<link rel="author" title="Denis Ah-Kang" href="mailto:denis@w3.org">
<link rel=help href="https://html.spec.whatwg.org/multipage/#the-form-element">
<link rel=help href="https://html.spec.whatwg.org/multipage/#attr-fe-autocomplete">
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<div id="log"></div>
<form name="missing_attribute">
<input>
<input autocomplete="on">
<input autocomplete="off">
<input autocomplete="foobar">
</form>
<form name="autocomplete_on" autocomplete="on">
<input>
<input autocomplete="on">
<input autocomplete="off">
<input autocomplete="foobar">
</form>
<form name="autocomplete_off" autocomplete="off">
<input>
<input autocomplete="on">
<input autocomplete="off">
<input autocomplete="foobar">
</form>
<form name="autocomplete_invalid" autocomplete="foobar">
<input>
<input autocomplete="on">
<input autocomplete="off">
<input autocomplete="foobar">
</form>
<script>
function autocompletetest(form, expectedValues, desc) {
test(function(){
assert_equals(form.autocomplete, expectedValues[0]);
assert_equals(form.elements[0].autocomplete, expectedValues[1]);
assert_equals(form.elements[1].autocomplete, expectedValues[2]);
assert_equals(form.elements[2].autocomplete, expectedValues[3]);
assert_equals(form.elements[3].autocomplete, expectedValues[4]);
}, desc);
}
autocompletetest(document.forms.missing_attribute, ["on", "", "on", "off", ""], "form autocomplete attribute missing");
autocompletetest(document.forms.autocomplete_on, ["on", "", "on", "off", ""], "form autocomplete attribute on");
autocompletetest(document.forms.autocomplete_off, ["off", "", "on", "off", ""], "form autocomplete attribute off");
autocompletetest(document.forms.autocomplete_invalid, ["on", "", "on", "off", ""], "form autocomplete attribute invalid");
var keywords = [ "on", "off", "name", "honorific-prefix", "given-name", "additional-name", "family-name", "honorific-suffix", "nickname", "username", "new-password", "current-password", "one-time-code", "organization-title", "organization", "street-address", "address-line1", "address-line2", "address-line3", "address-level4", "address-level3", "address-level2", "address-level1", "country", "country-name", "postal-code", "cc-name", "cc-given-name", "cc-additional-name", "cc-family-name", "cc-number", "cc-exp", "cc-exp-month", "cc-exp-year", "cc-csc", "cc-type", "transaction-currency", "transaction-amount", "language", "bday", "bday-day", "bday-month", "bday-year", "sex", "url", "photo", "tel", "tel-country-code", "tel-national", "tel-area-code", "tel-local", "tel-local-prefix", "tel-local-suffix", "tel-extension", "email", "impp", "webauthn" ];
keywords.forEach(function(keyword) {
test(function(){
var input = document.createElement("input");
// Include whitespace to test splitting tokens on whitespace.
// Convert to uppercase to ensure that the tokens are normalized to lowercase.
input.setAttribute("autocomplete", " " + keyword.toUpperCase() + "\t");
assert_equals(input.autocomplete, keyword);
}, keyword + " is an allowed autocomplete field name");
});
test(() => {
const select = document.createElement("select");
select.setAttribute("autocomplete", " \n");
assert_equals(select.autocomplete, "");
}, "Test whitespace-only attribute value");
test(() => {
const select = document.createElement("select");
select.setAttribute("autocomplete", "foo off");
assert_equals(select.autocomplete, "");
// Normal category; max=3
select.setAttribute("autocomplete", "foo section-foo billing name");
assert_equals(select.autocomplete, "");
// Contact category; max=4
select.setAttribute("autocomplete", "foo section-bar billing work tel");
assert_equals(select.autocomplete, "");
// Credential category; max=5
select.setAttribute("autocomplete", "foo section-bar billing work tel webauthn");
assert_equals(select.autocomplete, "");
}, "Test maximum number of tokens");
test(() => {
const textarea = document.createElement("textarea");
textarea.setAttribute("autocomplete", "call-sign");
assert_equals(textarea.autocomplete, "");
}, "Unknown field");
test(() => {
const hidden = document.createElement("input");
hidden.type = "hidden";
hidden.setAttribute("autocomplete", "on");
assert_equals(hidden.autocomplete, "");
hidden.setAttribute("autocomplete", "off");
assert_equals(hidden.autocomplete, "");
}, "Test 'wearing the autofill anchor mantle' with off/on");
test(() => {
const textarea = document.createElement("textarea");
textarea.setAttribute("autocomplete", " HOME\ntel");
assert_equals(textarea.autocomplete, "home tel");
textarea.setAttribute("autocomplete", "shipping country");
assert_equals(textarea.autocomplete, "shipping country");
textarea.setAttribute("autocomplete", "billing work email");
assert_equals(textarea.autocomplete, "billing work email");
textarea.setAttribute("autocomplete", " section-FOO bday");
assert_equals(textarea.autocomplete, "section-foo bday");
}, "Serialize combinations of section, mode, contact, and field");
test(() => {
const textarea = document.createElement("textarea");
textarea.setAttribute("autocomplete", "\tusername webauthn");
assert_equals(textarea.autocomplete, "username webauthn");
textarea.setAttribute("autocomplete", " section-LOGIN shipping work tel webauthn ");
assert_equals(textarea.autocomplete, "section-login shipping work tel webauthn");
}, "Serialize combinations of section, mode, contact, field, and credential");
</script>