Skip to content

gost-dom/shaman

Repository files navigation

Shaman

Introduction

Shaman is a support library to work on top of Gost-DOM. It helps writing tests using a higher level of abstraction than the native DOM. This encourages building accessibility in and results in code that is not only easier to read, but also resilient to changes in UI that doesn't change semantics:

Warning

This library is pre 0.1! Breaking changes may be pushed with no warning (where feasible, old versions will live with a deprecation warning for a while)

Important

Shaman is very limited in functionality. Functionality is primarily driven by the needs of Project Harmony, a test application using Gost-DOM to test an HTMX-based application. As new scenarios are covered by Harmony, so will the support be added to Shaman.

Example

As an example, to write test to interact with an input field, you may write:

package my_test

import (
    "github.com/gost-dom/shaman"
    . "github.com/gost-dom/shaman/predicates"
)

func TestSomething(t *testing.T) {
    win := initWindow(t) // Return a gost-dom/browser/html.Window
    // Find the <form> in the main landmark of the page.
    scope := shaman.WindowScope(t, win)
    mainContent := scope.SubScope(ariarole.Main)
    form := mainContent.SubScope(ByRole(ariarole.Form))
    // Find a textbox with the accessibility name, "Email"
    form.Textbox(ByName("Email")).Write("jd@example.com")
    // Find a password input field with the accessibility name, "Password"
    form.PasswordText(ByName("Password")).Write("very_secret")
    form.Get(ByName(ByRole(ariarole.Button), ByName("Submit"))).Click()
    // ...
}

Easier to write

A common pattern is to assign id or data-testid attributes to elements just to be able to find them in test cases. This practice does adds to mental load of the developer:

  • Which id-attributes to assign to which elements?
  • When writing tests first, you have to deal with this before even addressing the problem.

The shaman style of thinking in terms of textboxes with labels, placed inside a form, forces the developer to work in same level of abstraction as the problem domain itself: A user interacting with a page, identifying form elements by the labels they have.

Accessibility

The shaman style encourages writing tests that implcitly verify that elements have the proper attributes to support accessibility, i.e., all input fields have labels.

See the patterns section below for guidelines how to write tests that enforces a higher level of accessibility.

Resilient to changes in layout

If the layout of the application changes, but the functionality remains, the test is resilient to this change, if the new layout has the same semantics.

For example, the following 4 examples all have the same semantics, and provides the same functionality to the user.

<!-- A <label> is associated with an input field with the for-attribute -->
<label for="email-input">Email: </label>
<input id="email-input" type="text" />

<!-- An <input> field is a child of the label -->
<label>Email: <input id="email-input" type="text" /></label>

<!-- The input field references a random element using aria-labelledby -->
<input type="text" aria-labelledby="email-label" />
<span id="email-label">Your email address</span>

<!-- The label doesn't appear on the page, the input has an aria-label -->
<input type="text" aria-label="Email" placeholder="email" />

The shaman style of testing is not only resilient to the user interface changing; it detects if you forget to add a label.

Note

In my experience, 80% of all developers and UI designers are ignorant of accessibility. As a consequence any new project member are statistically very likely to break accessibility if not verified at design time. Fast developer-friendly tests is the best way to detect this early, preventing an unproductive path.

(80% was a pretty conservative number. It's probably more like 95%)

Good patterns

This is a collection of patterns that should help write more resilient tests that also help achieve better accessibility.

Verify exactly one <h1> heading

An <h1> is treated as a page title, so there should be exactly one. Using the Get method on document scope fails when multiple elements match the predicate, effectively verifying that exactly one <h1> exists with the expected title:

titleElm := shaman.WindowScope(t, win).Get(shaman.ByH1)
if got := titleElm.TextContent(); got != "Expected page title" {
    t.Error("Wrong title")
}

Scope by landmarks

Scope by relevant landmarks. Most tests would generally verify behaviour of the main content, typically in a <main> element; so to enforce the document structure.

mainContent := windowScope.SubScope(ByRole(ariarole.Main))

Screen reader users typically rely on landmarks to find relevant content. Using correct landmarks has a dramatic effect on the usability of the web application for that user base.

Having input fields? Always scope them by form

Browsers add default behaviour to input elements in a form, e.g., pressing enter tries to submit the form.

loginForm := mainContent.SubScope(ByRole(ariarole.Form))
emailField := loginForm.Get(ByRole(ariarole.Textbox), ByName("Email"))

Note

You should add a ByName when finding a form, to ensure that it has a proper title. That isn't supported by shaman at the time of writing this. #2

Can I use this with other libraries? (e.g., selenium, playwright)

Shaman is currently coupled to the interfaces exposed by Gost-DOM, but the code only depend on methods defined in the DOM and HTML DOM standards adapted to Go idioms (errors as values, and Go naming conventions).

Shaman could define a general interace, for which you could then write an adapter for other libraries. This is not a priority, but a contribution in this direction will not be rejected. Do reach out before starting on such work.

However, Shaman relies on very chatty communication when processing the DOM tree, which would cause significant overhead using any kind of inter-process communication.

About

Provide high-level DOM interaction

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages