As such, it has not been published yet. Work that remains to be done to get this ready for prime time:
- Tests that don't live in the Flexport codebase (the
spec_view_spec.rb
file is only provided for reference) - Documentation for setup
- Linting
- Detection and support for multiple react versions (mostly relates to tag enums, which change between react versions)
SpecViews offer a way to encapsulate UX interactions with the app when writing integration tests. This has several benefits:
- Makes tests more readable by factoring out DOM access logic. Instead, tests become a list of high level instructions and questions.
- DRYs view interaction logic, so that:
- New tests often require minimal new DOM interaction logic
- When a change is made to the DOM structure, tests don't usually have to be repaired individually.
- Helps with code organization, as your interaction objects reflect the structure of the app itself
- Defers fetching of elements until they are actually used, which reduces the risk of errors like stale element references
These are the main concepts needed to understand how to use SpecViews in your tests, and create your own custom SpecViews:
A SpecView definition corresponds to a single piece of the UI. For example, consider the following HTML for a sign-in form:
<form class="sign-in-form">
<div class="field-section name-section">
<label>Name</label>
<input type="text" name="name" class="name-field">
</div>
<div class="field-section pass-section">
<label>Password</label>
<input type="password" name="pass" class="pass-field">
</div>
<div class="buttons">
<button class="sign-in">Sign in</button>
<button class="forgot">Forgot password</button>
</div>
<div class="message"></div>
</form>
This might have the following SpecView
module SpecView
class SignInFormView < Base
default_selector '.signin-form'
component :name_field, '.name-field'
component :pass_field, '.pass-field'
component :button_section, '.buttons'
component :sign_in_button, '.sign-in'
component :forgot_button, '.forgot'
component :message, '.message'
def login(name, password)
name_field.send_keys(name)
pass_field.send_keys(password)
sign_in_button.click
end
end
end
The most important part of a SpecView definition is its component
declarations. In their most basic form, these are formatted as:
component :name, '<jquery selector>'
The point of components is to offer an easy way to access an element of the view (or a set of elements; see below about list components).
- Note: if you're unfamiliar with jQuery, these selectors work much like CSS selectors, except there are some additional helper functions you can use like
:contains(text)
, and:has(.some .selector)
. The SpecView-flavored selectors also support@ComponentName
for querying react components. See the "Querying react components" section below.
Now in our test code, we can have
let(:view) { SignInFormView.new }
it 'should show an error if no password' do
view.name_field.click # 1
view.name_field.send_keys('bob')
view.button_section.sign_in_button.click # 2
expect(view.message.text).to eq 'You must enter a password'
view.login('bob', 'password1')
expect(view.message.text).to eq 'Login successful!'
end
You can probably guess what this does in practice, but lets break down some details by line number:
- You can call any method that a Capybara element responds to, and a component will pass it along to its element. So since elements respond to
click
, this line clicks on the name field. See the Capybara API docs for available methods. - Components are also SpecViews, so they get all of the same components as the parent SpecView. So this line says "look up the sign in button, inside the button section". In this particular case, of course,
view.sign_in_button
would have the same result
Often, you won't have a fixed number of elements in a view, or you'll want to deal with multiple components as a list. For this you can use list components. For example, we could add the following to our SignInFormView:
component :labels, ['label']
And our test could include:
expect(view.labels[0].text).to eq 'Name'
expect(view.labels.text).to eql ['Name', 'Password']
Again, let's break down the details. In the first example, we can see that the result of the list component behaves like an ordinary array. You can index it, map over it, slice, find, etc.
However, we can also see that these objects also delegate to their items. So when we call text
on the list, we get the result of calling text
on each item.
When you get the result of a method like text
, you'll have a SpecViewArray, which continues to provide this delegation behavior. For example, you can do:
expect(view.labels.text.upcase).to eql ['NAME', 'PASSWORD']
Of course, sometimes the SpecViewArray's own method names will collide with something you want to delegate. In those cases, you can always use map:
expect(view.labels.text.map(&:length)) to eql [4, 8]
And if you ever need a real array, you can just call to_a
, and the SpecViewArray will be deeply converted.
You can refine a list component to a specific item with the at
method. As a simple example:
view.addresses.at(1).text
will give the text of the second address on the page. Negative numbers will index from the end of the list. As a convenience, first
and last
are provided, which simply call at(0)
and at(-1)
.
In the case above, view.addresses.text[1]
would provide the same result (although it would be less efficient). However, at
/first
/last
have different semantics with nested lists. For example:
addresses.first.phone_numbers.text
addresses.phone_numbers.first.text
These two lines do very different things.
The first means "Get the the text of every phone number of the first address". The second means "Get the text of the first phone number of every address". The indexing method is attached to the component directly preceding it. This also means that a line like:
addresses.middle_name.first.text
will error, because while addresses
is a list component, middle_name
(presumably) isn't.
You can also use at
directly in your component definitions. For example:
component :second_address, ['@Address'], at: 1
will produce a single react component, which is the second Address
component instance on the page.
A gotcha: react components are actually fetched in order of their HTML elements in the DOM. In the vast majority of cases, this will be exactly what you expect from looking at your React tree in the dev tools. However, if your component contains a portal, it may appear out of order. This is because the component's elements are (generally) appended to the end of the document body.
It is useful to be able to refactor your SpecView definitions into reusable components. For example, we could define a "field section" view for our form above like so:
module SpecView
class FieldSectionView < Base
default_selector '.field-section'
component :label, 'label'
component :field, 'input'
end
end
Now, in our SignInFormView definition, we could have:
component :field_sections, [SpecView::FieldSectionView]
This is where default_selector
comes into play; our SignInFormView now knows that it must find .field-section
and wrap the result as a FieldSectionView. Note that the resulting components do not inherit SignInFormView's components and methods.
So now we could test:
expect(view.field_sections.label.text).to eql ['Name', 'Password']
Often, you will want to provide a more specific selector for a subview. You can override the selector by using the with_selector
method. Adding to our example above, you could do
component :address_field_section, SpecView::FieldSectionView.with_selector(
'.field_section:contains(Address)'
)
Waiting in integration tests is often a very important piece of making your tests pass consistently. The recommended way to wait for your views to get completely rendered, and for your network requests to resolve is to call settle
, like so:
> view.settle
For more information see the waiting section of the API Docs. If you encounter situations where setle
doesn't wait long enough, please report it to the integration-test-infra slack channel, and we can help. Sometimes it is necessary to use wait_until_exists
, but you should prefer settle
.
When debugging, it is a very slow process to repeatedly update your component selectors, then rerun your tests. If you're trying to pin down the right selector for a component, it is easier to use the find
method. For example, you might byebug
at a certain point in your tests, and then try:
> view.control_section.find(["@Widget"]).count
to check if you match the expected number of elements. If you matched too many, you might then try something like:
> view.control_section.find(["@Widget{label: 'Disengage Plantary Rotor Manifold'}"]).count
This can all be done inside of a single test run. Then, if you've found the right selector (probably meaning in this case that count
returned 1), you can finally add to your SpecView:
component manifold_disengage_widget, "@Widget{label: 'Disengage Plantary Rotor Manifold'}"
Note: find
is slower than component
and non-idiomatic. It's useful for debugging, but you should prefer component
in your actual implementation, except in rare cases where you need to customize your selector according to input arguments.
find
takes all the same arguments as component
, except for name
, and produces the same output as the equivalent component method. So once you've tested out your arguments with find
, you can simply paste them into your component
definition.
Consider this HTML:
<h1>Document browser</h1>
<p>Click the previews below to display full detail</p>
<div class="document-preview">
<h1>Summary of ICTX deferrals</h1>
<p>Lorem ipsum dolor…</p>
<p>"Cupcake icing dollop," said Ahmed…</p>
</div>
<div class="document-preview">
<h1>First-class tangential overviews</h1>
<p>In the first quarter of 2017…</p>
<p>Standard rates apply in the following…</p>
</div>
<div class="main-document">
<h1>Fiscal Year 2018 External Proxies
<p>When that April with his showers sweet</p>
<p>The drought of March has pierced in the feet</p>
</div>
Suppose you're making a SpecView, and you mainly care about the contents of .main-document
above. You could write your components like this:
component :main_document, '.main-document'
component :main_headers, ['.main-document h1']
component :main_paragraphs, ['.main-document p']
But this isn't very DRY, since you need to put .main-document
before each selector. You can probably see that in more complicated real world cases, this can get really verbose. Worse, if the structure of the DOM changes, you have to track down and change all of those prefixes.
Instead, you can use child components to achieve the same result:
component :main_document, '.main-document' do
child :main_headers, ['h1']
child :main_paragraphs, ['p']
end
This also gives your SpecView a clearer visual flow, since your components are grouped together with a similar structure to the page itself.
In either version, the way you access e.g. the text of the first header, would be the same:
view.main_headers[0].text
# or
view.main_headers.first.text
React codebases often don't have many semantic class annotations, which makes querying just based on CSS selectors cumbersome. There are some workarounds for this, like adding .test-someUsefulTag
to elements, and using :contains(Some text)
selectors. But that's often very clumsy, and can produce an undesirable and tight coupling between your test code and your markup.
On the other hand, React markup provides a ton of semantic information, and SpecView selectors let you take advantage of that by querying about react components themselves. Here is an example selector which queries react components:
@Label{value: "Address"} @TextInput input;
And here is a snippet of markdown (as you might see in the react devtools extension) which the above selector would match:
<Label value="Address">
<TextInput theme="Base" size="m">
<input type="text" value=""></input>
</TextInput>
</Label>
As you might expect, the above selector specifically matches the input
tag on the third line.
React selectors begin with an @
sign and then have two parts:
- The component name (required). This is exactly the display name you see in the react devtools. So if you see
<puritan(Foo …>
, your react selector should be@puritan(Foo)
. - A property matcher (optional). If given, this will be checked against the properties of each component. The main points to keep in mind are:
- It looks for a superset of the properties given. So if the
Label
above were<Label value="Address" weight="bold">
, the above selector would still match - It can match deeper properties either by using a nested object, or by using syntax like
{"foo.bar.baz": "qux"}
- It must be a valid JS object literal, but doesn't have to be valid JSON
- You can also match regexes like this:
@Label{value:/^Addr.*$/}
- It looks for a superset of the properties given. So if the
You can mix the two types of selectors freely in one query, but the only relationship allowed between them is "descendent of". For example, this selector is fine:
div @SomeContainer div:eq(3) @Label{value:'Foo'} @TextInput label + input
label + input
is allowed because that entire snippet is only about DOM elements. However, no other form of mixing is allowed. The following examples are not supported:
div > @TextInput
@Label > div
.foo + @TextInput
.foo ~ @TextInput
@Container > @TextInput
@Container:not(div)
@Container:first-child
@Container:eq(1)
div:has(@Container)
Consider the following SpecView components:
component :name_field, '@TextInput{label:"Name"}'
component :input, 'input'
If you get the result of view.name_field
, the SpecView you get back represents the TextInput react component itself and not any element inside it. However, there is no way to fetch react components from the browser directly. Therefore, if you try to use view.name_field.style
, you will get an error; style
is a Capybara element method, and no element can be fetched this way. Similarly, you cannot use []
indexing on a list of react components (but you can use at
, as described above)
However, there are several convenience methods that can directly interact with react fetchers. These are click
, send_keys
, checked
?, hover
, value
, toggle
, and clear
. These will automatically search for an appropriate element inside the react component to interact with. You can also directly call text
on a react component or list. This will fetch the concatenated text of all the component's child elements.