Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/components/ThemeToggle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ const LABEL: Record<Theme, string> = {
<button
type="button"
class="toggle"
:aria-label="`${LABEL[theme]} colour theme. Activate to change.`"
data-testid="theme-toggle"
:aria-label="`${LABEL[theme]}-Mode colour theme. Activate to change.`"
@click="cycle"
>
>
Comment thread
Snooz82 marked this conversation as resolved.
<span class="dot" aria-hidden="true" />
{{ LABEL[theme] }}
</button>
Expand Down
69 changes: 69 additions & 0 deletions app/components/content/DocFigure.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
/**
* An image with a caption.
*
* ::doc-figure{src="/images/selectors.png" alt="How a selector resolves to an element"}
* The path a selector takes from string to element.
* ::
*
* Named doc-figure, not figure: `figure` is a native HTML element, so MDC
* would render `::figure` as the built-in tag and never reach this component.
* The doc-note / doc-table components dodge the same collision the same way.
*
* `alt` is the screen-reader text and is required — it describes the image for
* someone who cannot see it. The body is the visible caption below the image
* and may hold Markdown (links, `code`, emphasis), so it is a slot rather than
* a prop. A figure with no body renders the image alone, still with its alt.
*/
const props = defineProps<{
/** Root-absolute path to a file in public/, e.g. /images/foo.png. */
src: string
/** Screen-reader description. Required; keep it a real description. */
alt: string
}>()
</script>

<template>
<figure class="figure">
<img class="figure-img" :src="props.src" :alt="props.alt" loading="lazy" decoding="async" />
<figcaption class="figure-caption"><slot /></figcaption>
</figure>
</template>

<style scoped>
/*
* fit-content shrinks the figure to the image's natural width, so the caption
* box is exactly as wide as the image and `text-align: center` centres the
* caption under the image rather than across the page. max-width keeps a wide
* image from overflowing a narrow column.
*/
.figure {
width: fit-content;
max-width: 100%;
margin: var(--sp-6) 0;
}

/* display:block + max-width:100% renders at natural size, never upscaled, and
* only shrinks when the column is narrower than the image. */
.figure-img {
display: block;
max-width: 100%;
height: auto;
}

/* An empty caption (image-only usage) leaves no stray gap. */
.figure-caption:empty {
display: none;
}

.figure-caption {
margin-top: var(--sp-2);
font-size: var(--step--1);
color: var(--faint);
text-align: center;
}

.figure-caption :deep(p) {
margin: 0;
}
</style>
2 changes: 1 addition & 1 deletion app/utils/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { type Lang, ROBOT, ROBOT_REPL, THEME } from './lang'
*/

/** Languages the comparison and guides need, beyond Robot Framework. */
const BUNDLED: BundledLanguage[] = ['python', 'typescript', 'javascript', 'bash', 'json', 'yaml', 'dockerfile']
const BUNDLED: BundledLanguage[] = ['python', 'typescript', 'javascript', 'bash', 'json', 'yaml', 'dockerfile', 'html']

const robot = {
...(rfGrammar as unknown as LanguageRegistration),
Expand Down
1 change: 1 addition & 0 deletions app/utils/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export const LANG_LABEL: Record<string, string> = {
json: 'JSON',
yaml: 'YAML',
dockerfile: 'Dockerfile',
html: 'HTML',
}
90 changes: 80 additions & 10 deletions content/docs/concepts/selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,41 @@ rows:
---
::


### Example: Our Landing Page

On our landing page, we have some buttons and links.

One of them is a colour theme toggle, which is a button with a visible label and aria-label. It is the only button with that aria-label, so it is a perfect candidate for a `role=` selector. However, it also has a `data-testid` attribute, which is a good candidate for a `data-testid=` selector. The visible text is "DARK", which is a good candidate for a `text=` selector.

::doc-figure{src="/images/color-toggle.png" alt="A toggle button labelled DARK in the top-right corner"}
Color toggle at top right
::

See the following HTML DOM snippet for the button:

```html [Color Theme Toggle]
<button
type="button"
class="toggle"
data-testid="theme-toggle"
aria-label="DARK-Mode colour theme. Activate to change."
>
<span class="dot" aria-hidden="true" />
DARK
</button>
```

### 1. `role=` — how the user finds it

```robot-repl
Click role=button[name="Save"]
Click role=button[name="DARK-Mode colour theme. Activate to change."]
# ^ matches the aria-label as exact match

Click role=button[name*="colour theme"]
# ^ matches the aria-label which contains (*=) the substring "colour theme"

# other examples:
Click role=link[name="Get started"]
Fill Text role=textbox[name="Email"] admin@example.com
```
Expand All @@ -79,10 +110,28 @@ because the element has no proper role or no accessible name, **you have found
an accessibility bug**. A screen-reader user cannot identify that control
either. That is worth an issue, not a workaround.

When accessible names are long and complex, `role=` selectors can be brittle if the name changes in a redesign.
In that case, matching by substring or regex is a good compromise, e.g. `role=button[name*="colour theme"]` matches the aria-label by substring.

The following operators are available for matching the name:

| Operator | Meaning | Example |
| -------- | ------- | ------- |
| `=` | exact match | `role=button[name="DARK-Mode colour theme. Activate to change."]` |
| `*=` | contains substring | `role=button[name*="Activ"]` |
| `^=` | starts with string | `role=button[name^="DARK"]` |
| `$=` | ends with string | `role=button[name$="Activate to change."]` |
| `~=` | contains one whole word | `role=button[name~="Activate"]` |
| `\|=` | contains hyphenated word | `role=button[name\|="DARK"]` |

Regex is also supported when the expected text is surrounded by slashes,
e.g. `role=button[name=/^(DARK|LIGHT|CONTRAST|AUTO)-Mode colour theme/]` matches the aria-label as a regex, case-sensitively.
Regex flags can be added after the closing slash, e.g. `i` for case-insensitive matching.

### 2. `data-testid=` — the one attribute that belongs to us

```robot-repl
Click [data-testid="checkout-submit"]
Click data-testid=theme-toggle
```

Every other attribute on the page belongs to someone else. Classes belong to the
Expand All @@ -101,9 +150,11 @@ just make it deliberately rather than by default.
### 3. `text=` — what is written on it

```robot-repl
Click text=Sign in
Click "Sign in"
Click text=/^Sign in$/i
Click text=DARK # contains match
Click "DARK" # exact match

# if the aria-label would be the text:
Click text=/^(DARK|LIGHT|CONTRAST|AUTO)-Mode colour theme/i # regex match
```

Text selectors use a user-facing property, like `role=`, which is why they rank
Expand Down Expand Up @@ -139,6 +190,9 @@ escaped as `\#submit-button`, or written as `id=submit-button`.
### 5. `css=` — acceptable, not preferable

```robot-repl
Click button.toggle:has-text("DARK") #button with the class "toggle" that contains the text "DARK"

# other examples:
Click css=button.primary
Click .checkout > button
```
Expand All @@ -152,27 +206,35 @@ exactly the things a redesign changes. A class name is a styling decision, typic

CSS is the implicit default: a selector that is not obviously something else is treated as CSS.

::doc-note{kind="aside"}
CSS is way more powerful than many realise. It can select by attribute, by position, by relationship, and even by text content. See [CSS Basics and Advanced](#css-basics-and-advanced) for a full reference.
::

### 6. `xpath=` — the last resort

```robot-repl
Click xpath=//button[@type="submit"]
Click //div[@class="row"]//button
Click xpath=//button[contains(@class, "toggle") and contains(text(), 'DARK')]
# ^ the literal same as the CSS above, but in XPath

Click xpath=//header//button[contains(text(),'DARK')] # text contains, preceding whitespace ignored
Click //header//button[text()=' DARK'] # exact match, whitespace matters!
```

XPath is CSS's powerful, unpleasant relative. It is more verbose for the same
result, many web developers do not read it fluently, it is not web-native, and
it invites selecting by document position rather than function — which is the
most brittle thing you can possibly do.

It is genuinely more powerful, and occasionally something is unselectable
It is partially more powerful, and occasionally something is unselectable
without it. Use it then, and only then. It is the last resort, not a
general-purpose tool.
general-purpose tool. One of the very rare occasions where it is appropriate
to use XPath is when you need to navigate relative to a reliably identified element

And if you are about to paste something like this out of your browser's
devtools:

```robot-repl
Click /html/body/div[3]/div/div[2]/button
Click //body/div[1]/div/header/span/button
```

**DON'T!** That selector describes where the button sits today, not what it is.
Expand Down Expand Up @@ -402,6 +464,14 @@ you something false.

One useful distinction to xpath: CSS can select **following** siblings with `+` and `~`, but it has no simple equivalent of XPath's `..` for selecting a parent directly.

### Attribute selection with comparison operators

CSS supports a few comparison operators for attributes similar to the ones used in `role=` selectors.

So the same operators are available: `=`, `*=`, `^=`, `$=`, `~=`, and `|=`.
[See role table above](#_1-role-how-the-user-finds-it) for explanation.
However RegEx selection is not possible.

### Filtering inside a CSS selector

Playwright adds pseudo-classes to CSS that stay inside one step, rather than
Expand Down
8 changes: 1 addition & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "robotframework-browser-org",
"private": true,
"type": "module",
"packageManager": "pnpm@10.30.3",
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
"scripts": {
"dev": "nuxt dev",
"build": "pnpm libdoc && nuxt build",
Expand Down Expand Up @@ -43,11 +43,5 @@
"sanitize-html": "^2.17.6",
"vitest": "^4.1.10",
"yaml": "^2.9.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"better-sqlite3"
]
}
}
4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
allowBuilds:
better-sqlite3: true
esbuild: true
sharp: true
Comment thread
Snooz82 marked this conversation as resolved.
Binary file added public/images/color-toggle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading