Writing Your First Specification¶
In this tutorial we’ll specify and check an audio player web application using Quickstrom Cloud.
Begin by signing in at Quickstrom Cloud.
Accessing the Audio Player¶
The web application we’re going to test is already written and available on GitHub. We’re going to access it directly using htmlpreview.github.io. Try opening the following link:
That’s the web application we’re going to test. We’re now ready to write our specification.
A Minimal Specification¶
We’ll begin by writing a specification that always makes the tests pass:
Create a new specification by clicking Specifications in the top navigation bar, and then click the New button
Give the specification a name, like “audio-player”
Delete the existing code and replace it with the following:
1 2 3 4 5 6 7 8 9 10 11 12 13
module AudioPlayer where import Quickstrom import Data.Maybe (Maybe(..)) readyWhen :: Selector readyWhen = ".audio-player" actions :: Actions actions = clicks proposition :: Boolean proposition = true
Click the Create button
A bunch of things are going on in this specification. Let’s break it down line by line:
Line 1: We declare the
AudioPlayer
module. We must have a module declaration, but it can be named whatever we like.Line 3-4: We import the Quickstrom module. This is where we find definitions for DOM queries, actions, and logic. We also import Maybe which we’ll need later on.
Line 6-7: The
readyWhen
definitions tells Quickstrom to wait until there’s an element in the DOM that matches this CSS selector. After this condition holds, Quickstrom will start performing actions. We use.audio-player
as the selector, which is used as a class for the top-leveldiv
in the audio player web application.Line 9-10: Our
actions
specify what Quickstrom should try to do. In this case, we want it to click any available links, buttons, and so on.Line 12-13: In the
proposition
, we specify what it means for the system under test to be valid. For now, we’ll set it totrue
, meaning that any behavior is considered valid.
Running Tests¶
Let’s run some tests:
Create a new check configuration by clicking New in the Configurations section
Give it a name, like “htmlpreview”
Use the following URL as the origin:
Click the Create button
Find your newly created configuration in the table and click Check
This schedules a new check and opens the check view. It updates live as the check makes progress. After some time, you should see log output like the following:
Running 10 tests...
―――――――――――――――――――――――――――
10 Actions
Test passed!
―――――――――――――――――――――――――――
20 Actions
Test passed!
―――――――――――――――――――――――――――
30 Actions
Test passed!
―――――――――――――――――――――――――――
40 Actions
Test passed!
―――――――――――――――――――――――――――
50 Actions
Test passed!
―――――――――――――――――――――――――――
60 Actions
Test passed!
―――――――――――――――――――――――――――
70 Actions
Test passed!
―――――――――――――――――――――――――――
80 Actions
Test passed!
―――――――――――――――――――――――――――
90 Actions
Test passed!
―――――――――――――――――――――――――――
100 Actions
Test passed!
―――――――――――――――――――――――――――
Passed 10 tests.
Cool, we have it running! So far, though, we haven’t done much testing. Quickstrom is happily clicking its way around the web application, but whatever it finds we say “it’s all good!” Let’s make our specification actually say something about the audio player’s intended behavior.
Refining the Proposition¶
Our system under test, the audio player, is very simple. There’s a button for playing or pausing the audio player, and there’s a time display.
Our specification will describe how the player should work. Informally, we state the requirements as follows:
Initially, the player should be
paused
When
paused
, and when the play/pause button is clicked, it should transition to theplaying
stateWhen in the
playing
state, the time display should reflect the progress with a ticking minutes and seconds displayWhen
playing
, and when the play/pause button is clicked, it should go to thepaused
stateIn the
paused
state, the button should say “Play”In the
playing
state, the button should say “Pause”
Let’s translate those requirements to a formal specification in Quickstrom. Go back to your specification (you’ll find it under Specifications in the top navigation bar), and click the Edit button.
Now it’s time to edit the specification code. Begin by defining two
helpers, extracting the text content of the time display and the
play/pause button. Place these definitions at the bottom of
AudioPlayer.spec.purs
:
timeDisplayText :: Maybe String
timeDisplayText =
map _.textContent (queryOne ".time-display" { textContent })
buttonText :: Maybe String
buttonText =
map _.textContent (queryOne ".play-pause" { textContent })
Next, we’ll change the proposition
. Remove true
and type in
the following code:
proposition :: Boolean
proposition =
let
playing = ?playing
paused = ?paused
play = ?play
pause = ?pause
tick = ?tick
in
paused && always (play || pause || tick)
All those terms prefixed with question marks are called holes. A hole is a part of a program that is yet to be written, like a placeholder. We’ll fill the holes one by one.
The last line in our proposition can be read in English as:
Initially, the record player is paused. From that point, one can either play or pause, or the time can tick while playing, all indefinitely.
OK, onto filling the holes!
Filling Holes in the Specification¶
Let’s start with the definitions that describe states that the program can be in.
The playing
definition should describe what it means to be in the
playing
state. We specify it by stating that the button text
should be “Pause”. Replace ?playing
with the following expression:
buttonText == Just "Pause"
The Just "Pause"
means that there is a matching element with text
content “Pause”. Nothing
would mean that the query didn’t find any
element.
Similary, the paused
state is defined as the button text being
“Play”. Replace ?paused
with:
buttonText == Just "Play"
We’ve now specified the two states that the audio player can be in. Next, we specify transitions between states.
The definition play
describes a transition between paused
and
playing
. Replace the hole ?play
with the following expression:
paused && next playing
OK, so what’s going on here? We specify that the current state is
paused
, and that the next state is playing
. That’s how we
encode state transitions.
The pause
transition is similar. Replace ?pause
with the
following expression:
playing && next paused
Finally, we have the tick
. When we’re in the playing
state,
the time display changes its text on a tick
. The displayed time
should be monotonically increasing, so we compare alphabetically the
current and the next time.
Replace the hole ?tick
with the following expression:
playing
&& next playing
&& timeDisplayText < next timeDisplayText
If the time display would go past “99:59”, we’d get into trouble with this specification. But because we won’t run tests for that long, we can get away with the string comparison.
That’s it! We’ve filled all the holes. Your proposition should now look something like this:
proposition :: Boolean
proposition =
let
playing = buttonText == Just "Pause"
paused = buttonText == Just "Play"
play = paused && next playing
pause = playing && next paused
tick =
playing
&& next playing
&& timeDisplayText < next timeDisplayText
in
paused && always (play || pause || tick)
Let’s run some more tests.
Catching a Bug¶
Schedule a new check, now that we’ve fleshed out the specification.
You’ll see a bunch of output, involving shrinking tests and more. It should end with something like the following:
1. State
• .play-pause
-
- property "textContent" = "Play"
• .time-display
-
- property "textContent" = "00:00"
2. click button[0]
3. click button[0]
4. State
• .play-pause
-
- property "textContent" = "Play"
• .time-display
-
- property "textContent" = "NaN:NaN"
Failed after 1 tests and 4 levels of shrinking.
Whoops, look at that! It says that the time display shows “NaN:NaN”. We’ve found our first bug using Quickstrom!
There’s another version of the web application with a fix in place for this bug. Create a new check configuration but using the following URL as the origin:
Check again but with your new configuration. All tests pass!
Are we done? Is the audio player correct? Not quite.
Transitions Based on Time¶
The audio player transitions between states mainly as a result of
user action, but not only. A tick
transition (going from
playing
to playing
with an incremented progress) is triggered
by time.
We’ll try tweaking Quickstrom’s options related to trailing state changes to test more of the time-related behavior of the application.
Create a new check configuration with the same origin URL as the previously created one, but open up the Advanced options section and set Max trailing state changes to 1
rather than 0
.
You should see output such as the following:
1. State
• .play-pause
-
- property "textContent" = "Play"
• .time-display
-
- property "textContent" = "00:00"
2. click button[0]
3. State
• .play-pause
-
- property "textContent" = "Play"
• .time-display
-
- property "textContent" = "00:01"
Failed after 1 tests and 5 levels of shrinking.
Look, another bug! It seems that there are tick
transitions even
though the play/pause button indicates that we’re in the paused
state.
In fact, the problem is the button text, not the time display. There’s another version at the following URL with another bug fix:
Create yet another check configuration with this origin URL, and you should have all tests pass.
Summary¶
Congratulations! You’ve completed the tutorial, created your first specification, and found multiple bugs.
Have we found all bugs? Possibly not. This is the thing with testing. We can’t know if we’ve found all problems. However, Quickstrom tries very hard to find more of them for you, requiring less effort.
This tutorial is intentionally fast-paced and low on theory. Now that you’ve got your hands dirty, it’s a good time to check out The Specification Language to learn more about the operators in Quickstrom.