Repository navigation
Soundcheck Forms And Flows
How Soundcheck tests that forms still send (level 3, submit), the form plugins it knows, the test data it types, the YAML flow format for scripted journeys (level 4, flows) and logging in with a test account.
Warning
Submitting is real. With the submit level or a flow that sends a form, messages arrive, sign-ups are created, orders may be placed. Use it on staging, with Echo sealing it so the messages stop there. List your live site under production so Soundcheck refuses to submit there. See Interactions.
Turn it on for a run and pass the same levels to compare:
rhythm soundcheck snapshot staging.yoursite.com --level inspect,click,submit
rhythm soundcheck compare --level inspect,click,submitOn each page, up to five forms are submitted, each in two steps, reloading the page in between:
-
Empty, expecting validation. Text fields are cleared and the form is submitted. If the browser's own validation blocks it (
requiredattributes), that's a pass. Otherwise Soundcheck waits up to 10 s for the plugin's error messages. This step runs for forms with required fields and for known form plugins. - Valid, expecting success. Fields are filled with test data, required and consent checkboxes ticked, a radio picked in each group and the first real option of each select chosen. The form is submitted through its own submit button, so the plugin's JavaScript runs. Soundcheck waits up to 10 s for a confirmation.
| Check | Level | Title | When |
|---|---|---|---|
interact/form-submit |
error | Form no longer submits: Contact Form 7 #12 |
the valid submit showed no confirmation, the plugin reported a failure, or the response was 4xx |
interact/form-error |
error | Form submission fails (HTTP 500): WPForms #8 |
the submission's request answered 5xx |
interact/form-validation |
warning | Form shows no validation: form #quote |
the empty submit showed no error at all |
A contact form whose mail started failing after an update:
[BROKE] Form no longer submits: Contact Form 7 #12 - /contact/
There was an error trying to send your message. Please try again later.
fix: Submit the form by hand and watch the browser's network tab; check the plugin's settings, mail setup and spam protection.
The detail is the message the plugin showed. A successful submit isn't reported as an issue; it's recorded in the run's JSON under interactions:
{"kind": "submit", "selector": "form.wpcf7-form", "label": "Contact Form 7 #12", "outcome": "validation", "detail": "One or more fields have an error. Please check and try again."}
{"kind": "submit", "selector": "form.wpcf7-form", "label": "Contact Form 7 #12", "outcome": "success", "detail": "Thank you for your message. It has been sent."}A form can show "sent" and still email nobody. With Echo running, --echo records what each successful submit sent, adds it to the detail (… · sent 2 emails, 1 chat message), reports Form says sent, but no message went out and compares each form's messages with the snapshot's. See With Soundcheck.
| Form | Recorded as |
|---|---|
any form, on a URL under production (unless allowlisted in interactions.forms) |
production URL; allowlist the form in interactions.forms to submit it |
| hidden forms | hidden |
| login and other forms with a password field | has a password field |
search forms and GET forms (except known plugins) |
search or GET form |
| the WordPress comment form | comment form; allowlist it in interactions.forms to test it |
| the WooCommerce cart form | cart form |
forms that post to /wp-admin or wp-login.php
|
posts to WordPress admin |
| forms with no fields | no fields |
A form listed in interactions.forms (by CSS selector) is always eligible, on production too.
Soundcheck recognizes the big form plugins and reads their own success, failure and validation markers, so it knows the difference between "sent", "mail failed" and "a field is missing":
| Profile | Recognized by | Named in results as | Success means |
|---|---|---|---|
| Contact Form 7 |
.wpcf7 wrapper or _wpcf7 field |
Contact Form 7 #12 |
form.sent / .wpcf7-mail-sent-ok
|
| WPForms | form.wpforms-form |
WPForms #8 |
the confirmation container |
| Gravity Forms |
form#gform_3 or .gform_wrapper
|
Gravity Forms #3 |
.gform_confirmation_message |
| Elementor | form.elementor-form |
Elementor form contact |
.elementor-message-success |
| WooCommerce add to cart | form.cart |
WooCommerce add to cart |
.woocommerce-message or the "View cart" link |
| WooCommerce checkout | form.checkout |
WooCommerce checkout |
the order received page |
| any other form |
form #quote, form "Get a quote", form 2
|
a redirect, a 2xx answer with no error shown, or a success message |
WooCommerce checkout is only submitted with a payment method that takes no money: Cash on delivery, Direct bank transfer or Check (cod, bacs, cheque), or a gateway that says it's in test or sandbox mode. Otherwise it's skipped with no offline (cod, bacs, cheque) or test-mode payment method.
For other forms, Soundcheck looks for common error markers ([aria-invalid=true], .invalid-feedback, .has-error, .alert-danger and similar) and treats a redirect or a clean AJAX answer as success.
Every value Soundcheck types contains soundcheck-test, so test entries are easy to find and delete in your inbox, CRM or database. Values are chosen from the field's type, autocomplete, name, id and label:
| Field | Value |
|---|---|
soundcheck-test+3fa1c2@example.com (the tag changes per run) |
|
| name |
Soundcheck Test (first name Soundcheck, last name Test) |
| phone |
+1 555 0100 (a number reserved for fiction) |
| URL | https://example.com/soundcheck-test |
| company | Soundcheck Test Co |
| subject | soundcheck-test 3fa1c2 |
| message, any textarea | soundcheck-test: an automated form check by Rhythm Soundcheck (3fa1c2). Please ignore. |
| address, city, postcode |
1 Test Street, Testville, 12345
|
| number |
1, or the field's min
|
| date |
2030-01-15, kept within min and max
|
| anything else | soundcheck-test |
Values are padded or cut to the field's minlength and maxlength. Password and file fields are never filled. The tag is derived from the run's start time, so one run types the same values everywhere and two runs can be told apart.
A flow is a scripted journey in YAML: open a page, fill fields, click, wait and check what the visitor should see. Use flows for what level 3 can't guess: a multi-step checkout, a booking widget, a search that should find something, a members area.
Write an example to start from:
rhythm soundcheck flows initWrote .soundcheck/flows/example.yaml. Edit it, then run flows with: --level inspect,click,flows
It never overwrites an existing file. Every .yaml or .yml file in the flows folder (.soundcheck/flows, or the flows setting) is a flow, run in name order, each in its own private browser session. Run them with the flows level:
rhythm soundcheck run staging.yoursite.com --level inspect,click,flowsA flow that checks the contact form:
name: Contact form sends
start: /contact/
steps:
- fill: {selector: "input[name=your-name]", value: "${fake:name}"}
- fill: {selector: "input[type=email]", value: "${fake:email}"}
- fill: {selector: textarea, value: "${fake:message}"}
- click: {text: Send}
- wait: .wpcf7 form.sent
- expect: {text: Thank you for your message}
- screenshot: contact-sentWhen a step fails, the flow becomes an issue at level error (BROKE in a comparison) that names the flow and the step:
[ERROR] Flow "Contact form sends" step 5 failed: wait for .wpcf7 form.sent - /contact/
source: .soundcheck/flows/contact.yaml
fix: Run the flow by hand from /contact/; if the page changed on purpose, update .soundcheck/flows/contact.yaml.
The detail says what went wrong (.wpcf7 form.sent not found). A failed action stops the flow, since later steps depend on it; a failed expect is reported and the flow carries on. A flow that can't open its start page is reported as Flow "…" couldn't open /contact/.
| Key | Required | Meaning |
|---|---|---|
name |
yes | Shown in results and used for screenshot names. |
start |
no | Path or URL opened first. Default: the site's home page. Relative paths resolve against the site being checked. |
login |
no |
true runs the flow as the test user (see Logging in). Without credentials, or when the login failed, the flow is skipped with a note. |
production |
no |
true lets the flow click and submit on URLs listed under production in the config. See Flows on production. |
steps |
yes | The actions, in order. Each step is a one-key mapping. |
| Step | Form | Does |
|---|---|---|
goto |
goto: /pricing/ |
Opens a path or URL and waits for it to load. |
click |
click: "#send" or click: {selector: …} or click: {text: Send}
|
Clicks an element by CSS selector, or by its visible text, value or aria-label (exact match first, then contains; with selector, only among matching elements). Then waits for the page to settle. |
fill |
fill: {selector: "#email", value: "${fake:email}"} |
Clears a field and types the value. |
select |
select: {selector: "#size", value: Large} |
Picks a <select> option by value, or else by its text. |
check |
check: "#terms" or check: {selector: "#news", checked: false}
|
Ticks (default) or unticks a checkbox or radio. |
submit |
submit: or submit: "#quote-form" or submit: {text: Send}
|
Submits a form, or the form of the element given, through its submit button. With no target, the first form on the page. |
wait |
wait: 1500 or wait: .done or wait: {selector: .done} / {ms: 500}
|
Waits for milliseconds (at most 60,000), or until an element is visible. |
expect |
expect: Thank you or expect: {…}
|
Checks the page; see below. |
screenshot |
screenshot: contact-sent |
Saves a screenshot as flows/<flow>-<step>-<name>.png in the run folder. |
expect takes any of these; every one given must hold. It retries until the step timeout before failing.
| Key | Holds when |
|---|---|
text |
the page's visible text contains it (case-insensitive) |
selector |
an element matching it is visible |
url |
the current URL contains it |
status |
the last page load answered this HTTP status |
absent: true |
turns text and selector around: they must not be there |
message |
with Echo running and --echo, the flow made the site send this message: {channel, to, subject, text, count}; see Flows: expect a message
|
- expect: {url: /order-received/, text: Thank you}
- expect: {selector: .woocommerce-error, absent: true}
- expect: {message: {channel: email, to: "${fake:email}", subject: Your order}}Each step waits up to the run's timeout (default 30 s) for its element.
| Reference | Becomes |
|---|---|
${fake:email}, ${fake:name}, ${fake:first_name}, ${fake:last_name}, ${fake:phone}, ${fake:url}, ${fake:company}, ${fake:subject}, ${fake:text}, ${fake:message}, ${fake:number}, ${fake:address}, ${fake:city}, ${fake:zip}, ${fake:date}
|
the test data above (aliases such as ${fake:tel}, ${fake:postcode} and ${fake:full_name} work too) |
${env:NAME} |
the environment variable NAME
|
Values from ${env:…} are never stored or printed: every error message is masked (****). Use them for coupon codes, test card numbers and anything else you don't want in the repository. A missing variable fails the step.
Flow files are checked strictly: unknown keys, unknown steps and unknown ${fake:…} kinds are errors. A file that can't be read is an error-level issue (interact/flow-file), so it fails --fail-on error in CI. The other flows still run:
[ERROR] Flow file "bad.yaml" can't be read - site-wide
source: .soundcheck/flows/bad.yaml
line 3: unknown step "clik" (actions: goto, click, fill, select, check, submit, wait, expect, screenshot)
fix: Fix the file; `rhythm soundcheck flows init` writes an example of the format.
On URLs listed under production in the config, a flow's click and submit steps are refused, because a flow does exactly what its steps say. Read-only steps (goto, fill, select, check, wait, expect, screenshot) still run. The flow stops at the first refused step, which becomes a warning:
[WARNING] Flow "Contact form sends" step 4 refused on production: click "Send" - /contact/
source: .soundcheck/flows/contact.yaml
refused on a production URL (set production: true in the flow to allow it)
fix: Run flows against staging, or add production: true to .soundcheck/flows/contact.yaml if this flow is safe on the live site.
A flow that's safe on the live site, such as a search or a members-area check that changes nothing, can opt in:
name: Search finds products
production: true
start: /
steps:
- fill: {selector: "input[name=s]", value: mug}
- submit: "form[role=search]"
- expect: {text: Blue Mug}Set a test account in the environment:
export SOUNDCHECK_USER=soundcheck-test
export SOUNDCHECK_PASSWORD='…'
rhythm soundcheck snapshot staging.yoursite.comLogin: as so*** (from SOUNDCHECK_USER)
Credentials are read only from these two variables, never from flags or the config file, so they don't end up in shell history, config files or reports. They're masked in every error message. Cookies stay in the browser's memory.
The login is used by:
- the cache leak test, which visits up to ten pages as the test user and checks that anonymous visitors don't get those copies. See Cache.
-
flows with
login: true, which start already logged in.
The pages themselves are always checked as an anonymous visitor. --no-login ignores the variables for one run.
Soundcheck tries the login once, at the start. If it fails, one note says why. The checks that need it are skipped and the run's options.logged_in stays false:
note: login failed, so the checks that need it were skipped: WordPress refused the login: Error: The password you entered for the username is incorrect.
On WordPress, Soundcheck logs in through /wp-login.php (#user_login, #user_pass, #wp-submit), with "Remember me" off. It succeeds when WordPress sets its wordpress_logged_in_* cookie and the front end shows the admin bar or the logged-in body class. For a login page elsewhere, set login.url: /account/login/. Errors are specific:
| Message | Means |
|---|---|
WordPress refused the login: <message> |
wrong user or password; WordPress's own message |
… has no WordPress login form (#user_login); set login.url, or login.flow for other sites |
the login page isn't WordPress's |
logged in, but the site doesn't show the admin bar or the logged-in body class (a cache may be serving anonymous pages to logged-in users) |
the cookie was set; the site doesn't look logged in |
For sites that aren't WordPress, write a flow that logs in and point login.flow at it:
# .soundcheck/login.yaml
name: Log in
start: /account/login/
steps:
- fill: {selector: "#email", value: "${env:SOUNDCHECK_USER}"}
- fill: {selector: "#password", value: "${env:SOUNDCHECK_PASSWORD}"}
- click: {text: Sign in}
- expect: {selector: .account-menu}# .soundcheck/config.yaml
login:
flow: .soundcheck/login.yamlKeep the login flow outside the flows folder, or it will also run as a level 4 flow.
Tip
Create a dedicated test account with the lowest role that sees what you need (Subscriber or Customer). Never use an administrator account for automated checks.
← Interactions · Home · Cache →
rhythm · Useful ideas. Working systems. · Check only sites you own or have permission to test. Keep staging sealed (responsible use) · This wiki is published from wiki/ in the rhythm repository; suggest changes there with a pull request.
Get started
rhythm in depth
Tuner
- Overview
- Your first scan
- Reading results
- Choosing checks
- Requests and politeness
- Output and reports
- Batch scans
- Local scans
Tuner in depth
- Known vulnerabilities
- PHP advisories
- CI and code scanning
- Writing checks
- Check packs
- Finding reference
- CLI reference
- FAQ and troubleshooting
Soundcheck
- Overview
- Your first check
- Reading results
- Discovery
- Page health
- Visual changes
- Interactions
- Forms and flows
- Cache
- Speed
Soundcheck in depth
Echo
- Overview
- Your first run
- Connectors
- Sealing staging
- Messages and checks
- Actions and compare
- With Soundcheck
- Replay
- Deliverability
Echo in depth
Project