A set of Capybara selectors that allow you to find common UI elements by labels and using screen-reader compatible mark-up.
All feature tests should interact with the browser in the same way a screen-reader user would. This both tests the feature, and ensures the application is accessible.
To be accessible to a screen-reader, a page should be built from the native html elements with the semantics and behaviour required for each feature. For example if the page contains a button it should use <button>
element rather than adding a onClick
handler to a <span>
.
Where a feature does not exist in HTML, such as tabs, then ARIA roles and states can be used to convey the meaning to a screen-reader.
For a better overview see Using aria.
As a result all tests should be built from the visible labels on the page, and the semantic meaning of elements, and ARIA roles and attribute.
CSS and XPATH selectors based on classes, ids and nesting elements with no semantic meaning, should not be used.
This gem contains a set of selectors and filters for common UI elements and element states that are not already included in Capybara. These selectors follow the guidelines in ARIA Authoring Practices.
Examples:
# Bad selectors
# - fragile and does not check the control is correctly labelled
page.find(:css, "#widget > div > .field").set("Bob")
fill_in "field_name_1", with: "Bob"
page.find(:css, "#tab_1").click
within(:css, "#tabs > div > div.panel:first-child") do
expect(page).to have_text "Client name Bob"
end
# Good selectors
# - based on how a screen reader would hear and navigate a page
within_fieldset "User details" do
fill_in "First name", with: "Bob"
end
select_tab "Client details"
expect(page).to have_tab_panel "Client details", text: "Client name Bob"
within_modal "Are you sure?" do
click_button "OK"
end
Include in your Gemfile:
group :test do
# It is recommended you use a tag as the main branch may contain breaking changes
gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", tag: "v0.12.0"
end
See the Capybara cheatsheet for an overview of built-in Capybara selectors and actions.
accessible_name
[String, Regexp]Added to all selectors.
Currently only supported for Selenium drivers.
Filters for an element's computed accessible name. A string will do a partial match, unless the exact
option is set.
This uses the Selenium driver accessible_name
method which uses the accessible name calculated by the browser. Support
is reasonably good in modern browsers for common use cases, but browsers do not produce the same results for
all cases and some screen-readers will perform their own calculations.
This method must request the accessible name from the driver for each node found by the selector individually. Therefore, using this with a selector that returns a large number of elements will be inefficient.
<button id="id1" aria-labelledby="id1 id2">Delete</button>
<a id="id2" href="https://github.com/citizensadvice/capybara_accessible_selectors/blob/main/files/Documentation.pdf">Documentation.pdf</a>
click_button accessible_name: "Delete Documentation.pdf"
aria
[Hash]Added to all selectors.
Filters for an element that declares ARIA attributes
<button aria-controls="some-state" aria-pressed="true">A pressed button</button>
expect(page).to have_selector :button, "A pressed button", aria: { controls: "some-state", pressed: true }
current
[String, Symbol]Added to: link
, link_or_button
.
Is the element the current item within a container or set of related elements using aria-current
.
For example:
<ul>
<li>
<a href="https://github.com/citizensadvice/capybara_accessible_selectors/blob/main/">Home</a>
</li>
<li>
<a href="https://github.com/citizensadvice/capybara_accessible_selectors/blob/main/about-us" aria-current="page">About us</a>
</li>
</ul>
expect(page).to have_link "Home", current: nil
expect(page).to have_link "About us", current: "page"
Note: The [aria-current]
attribute supports both "true"
and "false"
values. A current: true
will match against [aria-current="true"]
, and a
current: false
will match against [aria-current="false"]
. To match an
element without any [aria-current]
attribute, pass current: nil
.
described_by
[String, Regexp]Added to all selectors.
Is the field described by some text using aria-describedby
.
For example:
<label>
My field
<input aria-describedby="id1 id2" />
</label>
<span id="id1">My</span>
<span id="id2">description</span>
expect(page).to have_field "My field", described_by: "My description"
fieldset
[String, Symbol, Array]Added to: button
, link
, link_or_button
, field
, fillable_field
, radio_button
, checkbox
, select
, file_field
, combo_box
and rich_text
.
Filter for controls within a <fieldset>
by <legend>
text. This can also take an array of fieldsets for multiple nested fieldsets.
For example:
<fieldset>
<legend>My question</legend>
<label>
<input type="radio" name="radios" />
Answer 1
</label>
<label>
<input type="radio" name="radios" />
Answer 2
</label>
</fieldset>
find :radio_button, "Answer 1", fieldset: "My question"
choose "Answer 1", fieldset: "My question"
Also see ↓ Locating fields
required
[Boolean]Added to: button
, link
, link_or_button
, field
, fillable_field
, radio_button
, checkbox
, select
, file_field
, combo_box
and rich_text
.
Filter for controls with a required
or aria-required
attribute.
For example:
<label>
<input requied />
Text
</label>
find :field, required: true
role
[String, Symbol, nil]Added to all selectors
Filters for an element with a matching calculated role, or with no role if nil
is supplied.
The roles "none", "presentation" and "generic" are returned as nil
as they are implementation details not
exposed to users.
This uses the Selenium driver aria_role
method which uses the role calculated by the browser taking
into account implicit role mappings. The results for implicit roles are
not consistent across all browsers, but are good for more common use cases.
Rack test will currently only use the value of the role
attribute and does not return implicit role values.
This method must request the role from the driver for each node found by the selector individually. Therefore, using this with a selector that returns a large number of elements will be inefficient.
<label for="switch-input">A switch input</label>
<input id="switch-input" type="checkbox" role="switch" />
expect(page).to have_field "A switch input", role: "switch"
validation_error
[String, Regexp, true, false]Added to: field
, fillable_field
, datalist_input
, radio_button
, checkbox
, select
, file_field
, combo_box
and rich_text
.
Filters for an element being both invalid, and has a description or label containing the error message.
This differs from the Capybara valid
and validation_message
filters which only consider the HTML validation API.
To be invalid, the element must
willValidate === false
validity.valid === false
or or have aria-invalid="true"
validity.valid === false
and `aria-invalid="false"For the error description, this can be contained in the accessible description, or the accessible name.
<label>
My field
<input required aria-describedby="error-id" />
</label>
<span id="error-id">This is required</span>
expect(page).to have_field "My field", validation_error: "This is required"
# Just check the field is invalid
expect(page).to have_field "My field", validation_error: true
# Just check the field is not invalid
expect(page).to have_field "My field", validation_error: false
Also see:
The following selectors have been extended so you can use an array as the locator to select within a fieldset. The last element of the array is the field label, and the other elements are fieldsets.
Extended selectors: button
, link
, link_or_button
, field
, fillable_field
, datalist_input
, radio_button
, checkbox
, select
, file_field
, combo_box
, rich_text
.
<fieldset>
<legend>My question</legend>
<label>
<input type="radio" name="radios" />
Answer 1
</label>
<label>
<input type="radio" name="radios" />
Answer 2
</label>
</fieldset>
find :radio_button, ["My question", "Answer 1"]
choose ["My question", "Answer 1"]
Also see ↑ fieldset
filter
alert
Selects an element with the role of alert
.
<div role="alert">Important message</div>
expect(page).to have_selector :alert, text: "Successfully saved"
expect(page).to have_alert, text: "Successfully saved"
Also see ↓ Expectation shortcuts
article
Finds an article structural role. The selector will match either an <article>
element or an element with role="article".
locator
[String, Symbol] The article's [aria-label]
attribute or contents
of the element referenced by its [aria-labelledby]
attributeAlso see:
banner
Finds a banner landmark.
locator
[String, Symbol] The landmark's [aria-label]
attribute or contents
of the element referenced by its [aria-labelledby]
attributeAlso see:
columnheader
Finds a columnheader cell that's either a <th>
element descendant of a <table>
, or a [role="columnheader"]
element.
locator
[String, Symbol] The text contents of the elementcolindex
[Integer, String] Filters elements based on their position amongst their siblings, or elements with a matching aria-colindexAlso see:
Example:
<table role="grid">
<tr>
<th>A columnheader</th>
</tr>
</table>
<div role="grid">
<div role="row">
<div role="columnheader">A columnheader</div>
</div>
</div>
expect(page).to have_selector :columnheader, "A columnheader", count: 2
combo_box
Finds a combo box. This will find ARIA 1.0 and ARIA 1.1 combo boxes. A combo box is an input with a popup list of options.
This also finds select based on Twitter typeahead classes, but this behaviour is deprecated and will be removed in a future release.
Locator and options are the same as the field selector with the following additional filters:
expanded
[Boolean] - Is the combo box expandedoptions
[Array\<String, Regexp>] - Has exactly these options in order. This, and other other filters, will match if the option includes the stringwith_options
[Array\<String, Regexp>] - Includes these optionsenabled_options
[Array\<String, Regexp>] - Has exactly these enabled options in orderwith_enabled_options
[Array\<String, Regexp>] - Includes these enabled optionsdisabled_options
[Array\<String, Regexp>] - Has exactly these disabled options in orderwith_disabled_options
[Array\<String, Regexp>] - Includes these disabled optionsOption text is normalised to single white spaces.
Note that the built-in Capybara selector datalist_input
will find a native html list
attribute based combo-box.
Also see:
contentinfo
Finds a contentinfo landmark.
locator
[String, Symbol] The landmark's [aria-label]
attribute or contents
of the element referenced by its [aria-labelledby]
attributeAlso see:
dialog
Finds a dialog.
This checks for either
an element with the role dialog
or alertdialog
or, an open <dialog>
element
locator
[String, Symbol] The title of the modal
Filters:
modal
[Boolean] Is dialog a modal. Modals are either opened with showModal()
, or have the aria-modal="true"
attributeAlso see:
disclosure
Finds a disclosure. This will find both a native disclosure (<details>
/<summary>
) and an ARIA disclosure.
locator
[String, Symbol] The text label of the disclosureexpanded
[Boolean] Is the disclosure expandedNote that an ARIA disclosure is typically hidden when closed. Using expanded: false
will only find an element where visible:
is set to false
or :all
.
Also see:
disclosure_button
Finds the open and close button associated with a disclosure. This will be a <summary>
, a <button>
or an element with the role of button.
locator
[String, Symbol] The text label of the disclosureexpanded
[Boolean] Is the disclosure expandedAlso see:
grid
Finds a grid element that declares [role="grid"]
.
locator
[String, Symbol] Either the grid's [aria-label]
value, or the
text contents of the elements referenced by its [aria-labelledby]
attributedescribed_by
[String, Symbol] The text contents of the elements referenced by
its [aria-describedby]
attribute, or the text contents of a <table>
element's
<caption>
elementAlso see:
Example:
<table role="grid" aria-label="A grid"></table>
<div role="grid" aria-label="A grid"></div>
expect(page).to have_selector :grid, "A table grid", count: 2
gridcell
Finds a gridcell element that's either a <td>
descendant of a <table>
or declares [role="gridcell"]
.
locator
[String, Symbol]columnheader
[String, Symbol] Filters elements based on their matching columnheader's text contentrowindex
[Integer, String] Filters elements based on their ancestor row's positing amongst its siblingscolindex
[Integer, String] Filters elements based on their position amongst their siblingsAlso see:
Example:
<table role="grid">
<tr>
<td>A gridcell</td>
</tr>
</table>
<div role="grid">
<div role="row">
<div role="gridcell">A gridcell</div>
</div>
</div>
expect(page).to have_selector :gridcell, "A gridcell", count: 2
heading
Finds a heading. This can be either an element with the role "heading" or h1
to h6
.
locator
[String, Symbol]level
[1..6] Filter for a heading levelAlso see:
Example:
<div role="heading">Heading</div>
<h2>Heading</h2>
expect(page).to have_selector :heading, "Heading", count: 2
item
and item_type
Finds a microdata item.
Microdata isn't exposed to users, including screen-readers. However this can still be a useful way to check a page has the expected information in the expected place.
locator
[String, Symbol] The itemprop
name of the itemtype
[String, Symbol, Array] The itemtype
. Also accepts and array of item types, for selecting nested item typesExample:
<dl itemscope itemtype="application:person">
<dt>First name</dt>
<dd itemprop="first-name">Bob</dd>
<dt>Last name</dt>
<dd itemprop="last-name">Hoskins</dd>
</dl>
expect(page).to have_selector :item, "first-name", type: "application:person", text: "Bob"
expect(page).to have_selector :item_type, "application:person"
Also see ↓ Expectation shortcuts
main
Finds a main landmark.
locator
[String, Symbol] The landmark's [aria-label]
attribute or contents
of the element referenced by its [aria-labelledby]
attributeAlso see:
menu
Finds a menu.
locator
[String, Symbol] Either the menu's [aria-label]
value, the
contents of the [role="menuitem"]
or <button>
referenced by its
[aria-labelledby]
expanded
[Boolean] Is the menu expandedorientation
[String] The menu's orientation, either horizontal
or
vertical
(defaults to vertical
when omitted<div role="menu" aria-label="Actions">
<button type="button" role="menuitem">Share</li>
<button type="button" role="menuitem">Save</li>
<button type="button" role="menuitem">Delete</li>
</div>
expect(page).to have_selector :menu, "Actions"
expect(page).to have_selector :menu, "Actions", expanded: true
menuitem
Finds a menuitem.
locator
[String, Symbol] The menuitem content or thelocator
[String, Symbol] Either the menuitem's contents, its [aria-label]
value, or the contents of the element referenced by its [aria-labelledby]
disabled
[Boolean] Is the menuitem disabled<div role="menu" aria-label="Actions">
<button type="button" role="menuitem">Share</li>
<button type="button" role="menuitem" aria-disabled="true">Save</li>
<button type="button" role="menuitem">Delete</li>
</div>
within :menu, "Actions", expanded: true do
expect(page).to have_selector :menuitem, "Share"
expect(page).to have_selector :menuitem, "Save", disabled: true
expect(page).to have_no_selector :menuitem, "Do something else"
end
modal
Finds a modal dialog.
This checks for either
a modal with the dialog
or alertdialog
role and aria-modal="true"
attribute
or, a <dialog>
element opened with showModal()
locator
[String, Symbol] The title of the modal
Also see:
navigation
Finds a navigation landmark.
locator
[String, Symbol] The landmark's [aria-label]
attribute or contents
of the element referenced by its [aria-labelledby]
attributeAlso see:
row
Finds a row element that's either a <tr>
descendant of a <table>
or declares [role="row"]
.
locator
[String, Symbol] The text contents of the elementrowindex
[Integer, String] Filters elements based on their position amongst their siblings, or elements with a matching aria-rowindexAlso see:
Example:
<table role="grid">
<tr>
<td>Within a row</td>
</tr>
</table>
<div role="grid">
<div role="row">
<div role="gridcell">Within a row</div>
</div>
</div>
expect(page).to have_selector :row, "Within a row", count: 2
region
Finds a region landmark.
locator
[String, Symbol] The landmark's [aria-label]
attribute or contents
of the element referenced by its [aria-labelledby]
attributeAlso see:
rich_text
Finds a rich text editor.
This should be compatible with most browser based rich text editors. It searches for an element with the contenteditable
attribute marked up with the textbox
role. It is also compatible with <iframe>
based editors such as CKEditor 4 and TinyMCE.
locator
[String, Symbol] The label for the editor. This can be an aria-label
or aria-labelledby
. For iframe editors this is the title
attribute.For testing the content of an iframe based editor you need to use within_frame
, or you can use within_rich_text
.
# non-iframe based editors
expect(page).to have_selector :rich_text, "Label", text: "My content"
# iframe based editors
within_frame find(:rich_text, "Label") do
expect(page).to have_text "My content"
end
Also see:
section
Finds a section of the site based on the first heading in the section.
A section is html sectioning element: <section>
, <article>
, <aside>
, <footer>
, <header>
, <main>
or <form>
.
locator
[String, Symbol] The text of the first headingheading_level
[Integer, Enumerable] The heading level to find. Defaults to (1..6)
section_element
[String, Symbol, Array] The section element to use. Defaults to %i[section article aside footer header main form]
<section>
<div>
<h2>My section</h2>
</div>
Some content
</section>
within :section, "My section" do
expect(page).to have_text "Some content"
end
Also see
tab_panel
Finds a tab panel.
locator
[String, Symbol] The text label of the tab button associated with the panelopen
[Boolean] Is the tab panel open.Note that a closed tab panel is not visible. Using open: false
will only find an element where visible:
is set to false
or :all
.
Also see
tab_button
Finds the button that opens a tab.
locator
[String, Symbol] The text label of the tab buttonopen
[Boolean] Is the tab panel open.Also see:
fill_in_rich_text(locator, **options)
Fill in a rich text field with plain text.
locator
[String] - Find the rich text areaoptions
:
with
[String] - The text to fill the field, or nil to emptyclear
[Boolean] - Clear the rich text area first, defaults to truefill_in_rich_text "Diary entry", with: "Today I published a gem"
Also see ↑ rich_text
selector
select_tab(name, &block)
Opens a tab by name.
name
[String] - The tab label to openblock
[Block] - Optional block to run within the tabselect_tab "Client details"
Also see ↑ tab_panel
selector
select_combo_box_option(with, **options)
Fill in a combo box and select an option
with
[String] - Option to select, or an empty string to clear the combo boxoptions
:
from
[String, Symbol, Array] - Locator for the fieldsearch
[String] - Alternative text to search for in the inputcurrently_with
[String] - Current value for the fieldoption_
will be used to find the option. eg option_text
, option_match
select_combo_box_option "Apple", from: "Fruits"
Also see ↑ combo_box
selector
select_disclosure(name)
Open disclosure if not already open, and return the disclosure.
name
[String] - Locator for the disclosure buttonblock
- When present, the block
argument is forwarded to a
within_disclosure
callselect_disclosure("Client details")
select_disclosure "Client details" do
expect(page).to have_text "The Client details contents"
end
Also see ↑ disclosure
selector
toggle_disclosure(name, expand:)
Toggle a disclosure open or closed, and return the button
name
[String] - Locator for the disclosure buttonexpand
[Boolean] - Force open or closed rather than toggling.block
- When present, the block
argument is forwarded to a
within_disclosure
calltoggle_disclosure("Client details")
toggle_disclosure "Client details", expand: true do
expect(page).to have_text "The Client details contents"
end
Also see ↑ disclosure
selector
within_disclosure(name, **find_options, &block)
Executing the block within a disclosure.
within_disclosure "Client details" do
expect(page).to have_text "Name: Frank"
end
Also see ↑ disclosure
selector
within_dialog(name, **find_options, &block)
Execute the block within a dialog
within_dialog "Settings" do
check "Dark mode"
end
Also see ↑ dialog
selector
within_modal(name, **find_options, &block)
Execute the block within a modal.
within_modal "Are you sure?" do
click_button "Confirm"
end
Also see ↑ modal
selector
within_rich_text(name, **find_options, &block)
Execute within the rich text. If the rich text is iframe based this will execute "within_frame
".
within_rich_text "Journal entry" do
expect(page).to have_text "Today I went to the zoo"
end
Also see ↑ rich_text
selector
within_section(name, **find_options, &block)
Execute the block within a section.
within_section "Heading" do
expect(page).to have_text "Section content"
end
Also see ↑ section
selector
within_tab_panel(name, **find_options, &block)
Executing the block within a tab panel.
within_tab_panel "Client details" do
expect(page).to have_text "Name: Fred"
end
Also see ↑ tab_panel
selector
have_validation_errors(&block)
Checks if a page has a set of validation errors.
This will compare all the validation errors on a page with an expected set of errors. If the errors do not match it will not pass.
&block
- this takes a block. In the block each validation error exception should be added using the following DSL:expect(page).to have_validation_errors do
field "Name", validation_error: "This is required"
select "Gender", validation_error: "This is required"
field "Age", validation_error: "Choose a number less than 120"
radio_group "Radio questions", validation_error: "Select an option"
# You can use all the field selectors in the block
# checkbox datalist_input field file_field fillable_field radio_button select combo_box rich_text
# Additionally a "radio_group" selector will find all radios in a fieldset
# You can also use "within" and "within_fieldset" methods
end
Also see ↑ validation_error
filter
have_no_validation_errors
Checks if a page has no invalid fields.
expect(page).to have_no_validation_errors
Also see ↑ validation_error
filter
The following expectation shortcuts are also added for both have_selector_
and have_no_selector_
:
have_alert
have_article
have_banner
have_columnheader
have_combo_box
have_contentinfo
have_disclosure
have_disclosure_button
have_grid
have_gridcell
have_heading
have_item
have_main
have_modal
have_navigation
have_region
have_row
have_section
have_tab_panel
have_tab_button
For example the following two are equivalent:
expect(page).to have_selector :combo_box, "Foo"
expect(page).to have_combo_box, "Foo"
# install
bundle install
# lint
bundle exec rubocop
# test
# A local install of Chrome is required for the selenium web driver
bundle exec rspec