@simplysf/simply-utam 0.1.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -1
- package/lib/clipboard.d.ts +31 -0
- package/lib/clipboard.js +98 -0
- package/lib/clipboard.js.map +1 -0
- package/lib/commands/init.d.ts +2 -1
- package/lib/commands/init.js +39 -13
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/snapshot/console/import.d.ts +26 -0
- package/lib/commands/snapshot/console/import.js +96 -0
- package/lib/commands/snapshot/console/import.js.map +1 -0
- package/lib/commands/snapshot/console/snippet.d.ts +25 -0
- package/lib/commands/snapshot/console/snippet.js +72 -0
- package/lib/commands/snapshot/console/snippet.js.map +1 -0
- package/lib/commands/snapshot/find.d.ts +25 -0
- package/lib/commands/snapshot/find.js +82 -0
- package/lib/commands/snapshot/find.js.map +1 -0
- package/lib/commands/snapshot/lookup.d.ts +20 -0
- package/lib/commands/snapshot/lookup.js +57 -0
- package/lib/commands/snapshot/lookup.js.map +1 -0
- package/lib/commands/snapshot/node.d.ts +22 -0
- package/lib/commands/snapshot/node.js +61 -0
- package/lib/commands/snapshot/node.js.map +1 -0
- package/lib/commands/snapshot/path.d.ts +21 -0
- package/lib/commands/snapshot/path.js +77 -0
- package/lib/commands/snapshot/path.js.map +1 -0
- package/lib/commands/snapshot/tree.d.ts +19 -0
- package/lib/commands/snapshot/tree.js +50 -0
- package/lib/commands/snapshot/tree.js.map +1 -0
- package/lib/snapshot-flags.d.ts +40 -0
- package/lib/snapshot-flags.js +70 -0
- package/lib/snapshot-flags.js.map +1 -0
- package/oclif.manifest.json +553 -3
- package/package.json +16 -4
- package/skills/utam-dom-snapshots/SKILL.md +390 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysf/simply-utam",
|
|
3
3
|
"description": "Salesforce UTAM UI testing and build automation CLI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"author": "@ClayChipps",
|
|
6
6
|
"homepage": "https://github.com/SimplySF/simply-utam",
|
|
7
7
|
"bugs": "https://github.com/SimplySF/simply-utam/issues",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"/bin",
|
|
21
21
|
"/lib",
|
|
22
|
+
"/skills",
|
|
22
23
|
"README.md",
|
|
23
24
|
"oclif.manifest.json"
|
|
24
25
|
],
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@oclif/core": "^5.1.8",
|
|
45
|
-
"@simplysf/simply-utam-core": "0.
|
|
46
|
+
"@simplysf/simply-utam-core": "0.3.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@vitest/coverage-v8": "^4.1.10",
|
|
@@ -72,7 +73,17 @@
|
|
|
72
73
|
"commands": "./lib/commands",
|
|
73
74
|
"topicSeparator": " ",
|
|
74
75
|
"plugins": [],
|
|
75
|
-
"devPlugins": []
|
|
76
|
+
"devPlugins": [],
|
|
77
|
+
"topics": {
|
|
78
|
+
"snapshot": {
|
|
79
|
+
"description": "Capture and query DOM snapshots, and look up page objects.",
|
|
80
|
+
"subtopics": {
|
|
81
|
+
"console": {
|
|
82
|
+
"description": "Capture DOM snapshots by pasting a snippet into a browser's DevTools console."
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
76
87
|
},
|
|
77
88
|
"wireit": {
|
|
78
89
|
"build": {
|
|
@@ -138,6 +149,7 @@
|
|
|
138
149
|
},
|
|
139
150
|
"files": [
|
|
140
151
|
"test/**/*.ts",
|
|
152
|
+
"skills/**",
|
|
141
153
|
"src/**/*.ts",
|
|
142
154
|
"**/tsconfig.json",
|
|
143
155
|
"../../tsconfig.json",
|
|
@@ -146,5 +158,5 @@
|
|
|
146
158
|
"output": []
|
|
147
159
|
}
|
|
148
160
|
},
|
|
149
|
-
"gitHead": "
|
|
161
|
+
"gitHead": "7a8febc345f771e17a39247082eb47333c75001b"
|
|
150
162
|
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: utam-dom-snapshots
|
|
3
|
+
description: Captures and queries DOM snapshots of Salesforce Lightning pages, and looks up UTAM page objects, to work out which page object and selector reach an element and why a UTAM step failed. Use when writing or fixing a UTAM step definition, page object or selector in this project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DOM snapshots
|
|
7
|
+
|
|
8
|
+
A DOM snapshot is a JSON capture of the page a test was on: every custom element and shadow root,
|
|
9
|
+
which parts are hidden, and the attributes and labels a selector needs. Record data is masked. Don't
|
|
10
|
+
read or script over the JSON: the `npx simply-utam snapshot` commands answer questions about it, and
|
|
11
|
+
`node <handle>` shows everything recorded about one element.
|
|
12
|
+
|
|
13
|
+
## Snapshot lifecycle
|
|
14
|
+
|
|
15
|
+
- **Where they come from.** A test run writes snapshots in three ways, and a person can capture one
|
|
16
|
+
from their own browser:
|
|
17
|
+
- **On failure.** When a step throws, the `afterStep` hook in `wdio.conf.mjs` captures the page
|
|
18
|
+
before anything else happens.
|
|
19
|
+
- **On demand.** The step `And I capture the DOM snapshot` (or `... snapshot "<label>"`) captures
|
|
20
|
+
the page wherever you put it in a scenario.
|
|
21
|
+
- **While paused.** `await browser.domSnapshot("<label>")` in the REPL of `browser.debug()` (see
|
|
22
|
+
"Capture by hand"). It needs a person at the terminal.
|
|
23
|
+
- **From the browser console.** A person pastes a snippet into the DevTools console of any page,
|
|
24
|
+
and `snapshot console import` saves it (see "Capture by hand"). No test run needed.
|
|
25
|
+
- **Where they go.** Each run gets a folder, `.utam/snapshots/<run>/`, with one file per capture,
|
|
26
|
+
named `<scenario>--<step or label>.json`; a capture that doesn't know its scenario is
|
|
27
|
+
`manual--<label>.json`. Captures from the console go to `.utam/snapshots/manual/`, which pruning
|
|
28
|
+
never deletes. Every capture is also copied to
|
|
29
|
+
`.utam/snapshots/latest.json`, which the commands read by default; `--file <path>` reads another.
|
|
30
|
+
- **`latest.json` is only the most recent capture.** It may come from an earlier run, another
|
|
31
|
+
scenario, or another step. Every command prints a header naming the snapshot's file, trigger,
|
|
32
|
+
time, URL and mask mode, then its scenario, step and, for a failure, the error, with an
|
|
33
|
+
assertion's `Expected` and `Received` lines. Check it before you trust the answer.
|
|
34
|
+
- **A capture is one moment.** The on-demand step doesn't wait for anything. Lightning keeps
|
|
35
|
+
rendering after navigation returns, so a capture taken too early shows the app's frame with the
|
|
36
|
+
page half-built: the target missing, components with nothing inside. A failure capture shows the
|
|
37
|
+
page as it was when the step gave up.
|
|
38
|
+
- **An undefined step writes no snapshot.** Cucumber doesn't call `afterStep` for a step with no
|
|
39
|
+
definition. The steps before it still run, so an on-demand capture placed before it still works.
|
|
40
|
+
|
|
41
|
+
## Getting the snapshot you need
|
|
42
|
+
|
|
43
|
+
Each live run is slow, so get a snapshot from a run you need anyway, and run only the scenario you
|
|
44
|
+
need:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run test:ui -- --spec <path/to/file>.feature:<line>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`<line>` is the line of the scenario's `Scenario:`. `test:ui` compiles your page objects first, then
|
|
51
|
+
runs `wdio run wdio.conf.mjs` with the arguments after `--`.
|
|
52
|
+
|
|
53
|
+
| You have | Do this |
|
|
54
|
+
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| A failing step | Run it. The failure snapshot shows the page at the moment it failed. |
|
|
56
|
+
| A step with no definition yet | Write your best first attempt and run it. If it fails, its failure snapshot is the page you need. Or add `And I capture the DOM snapshot` before it, if you'd rather look first. |
|
|
57
|
+
| A snapshot where the target isn't there yet | The capture was too early. Capture after a step that waits for something on the page: `utam.load` waits for its root, and `waitFor…` methods (elements marked `"wait": true`) wait for theirs. |
|
|
58
|
+
| A page state no step reaches yet | Add the steps that reach it, then capture after them. Or, to look before writing any steps, ask a person to capture it from the console (see "Capture by hand"). |
|
|
59
|
+
|
|
60
|
+
Remove any capture step you added to a feature file before you finish.
|
|
61
|
+
|
|
62
|
+
### Capture by hand
|
|
63
|
+
|
|
64
|
+
A page a person drives by hand isn't necessarily the page the test sees: a different user, profile,
|
|
65
|
+
window size or feature flag can render a different DOM. A capture from a test run is the faithful
|
|
66
|
+
one; capture by hand to explore a page, and confirm with a live run.
|
|
67
|
+
|
|
68
|
+
**From the browser console.** Works on any page, in any browser, with no test run. An agent runs
|
|
69
|
+
the two commands; a person runs the snippet:
|
|
70
|
+
|
|
71
|
+
1. `npx simply-utam snapshot console snippet --copy` puts the snippet on the clipboard (without
|
|
72
|
+
`--copy` it prints it). `--mask` sets the mask mode, which is fixed in the snippet.
|
|
73
|
+
2. The person opens DevTools on the page, checks the console's context selector says `top`, pastes
|
|
74
|
+
and presses Enter. Chrome asks to type `allow pasting` the first time. The snippet copies the
|
|
75
|
+
snapshot to the clipboard and prints a one-line summary. Where the console's `copy` isn't
|
|
76
|
+
available, it downloads `dom-snapshot-<time>.json` instead.
|
|
77
|
+
3. `npx simply-utam snapshot console import --label "<label>"` saves it and prints its header. For a
|
|
78
|
+
download, add `--file <path>`; `--file -` reads standard input.
|
|
79
|
+
|
|
80
|
+
The snippet can be saved as a DevTools Snippet (Sources → Snippets) and run again on each page.
|
|
81
|
+
|
|
82
|
+
**While paused.** For a person at the terminal, not an agent: the REPL needs someone to type into
|
|
83
|
+
it. Add `await browser.debug();` to a step definition where you want to stop, and run the scenario
|
|
84
|
+
with `DEBUG` set (`DEBUG=1 npm run test:ui -- --spec …`), so the step can stay paused for up to 30
|
|
85
|
+
minutes instead of timing out. In the REPL, click around the page as you need, then capture it:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
await browser.domSnapshot('after opening Account');
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It returns the file it wrote, and copies it to `latest.json` like any other capture. Remove the
|
|
92
|
+
`browser.debug()` call before you finish.
|
|
93
|
+
|
|
94
|
+
## Workflow
|
|
95
|
+
|
|
96
|
+
**Writing a step:**
|
|
97
|
+
|
|
98
|
+
1. Get a snapshot of the page (see above) and check its header.
|
|
99
|
+
2. `find` the target by its label (values are masked), or by tag or attribute.
|
|
100
|
+
3. `path --node <handle>` to see the components from the page down to it, with their page objects
|
|
101
|
+
and the routes between them. `node <handle>` shows any one of them in full.
|
|
102
|
+
4. Load the outermost **root** page object on the path, and follow its public methods and elements
|
|
103
|
+
down (`lookup` shows them). Where the page objects stop, write your own page object for the rest
|
|
104
|
+
(see "Attach your own page object below the last shipped one"); keep every step below the last
|
|
105
|
+
page object in a page object, not in step code. Check each element's selector with
|
|
106
|
+
`find --css "<selector>" --within <handle>` (and `--shadow`) before a live run.
|
|
107
|
+
5. If `find` shows hidden matches, make sure what you load can only match the visible one: UTAM
|
|
108
|
+
takes the first element that matches a selector, in document order, whether or not it's visible.
|
|
109
|
+
See "Scope a shipped root page object to the visible copy", and test your root selector with
|
|
110
|
+
`find --css "<selector>"` before a live run.
|
|
111
|
+
6. Confirm with a live run.
|
|
112
|
+
|
|
113
|
+
**Fixing a failing step:**
|
|
114
|
+
|
|
115
|
+
1. Run it, then read the failure snapshot's header: it names the step and the error. UTAM's error
|
|
116
|
+
names the selector it couldn't find, relative to the element it searched in:
|
|
117
|
+
`inside its scope element` means that element's light DOM (the selector is declared in
|
|
118
|
+
`elements`), and `inside its scope shadow root` means its shadow root (`shadow.elements`). For
|
|
119
|
+
that error, the header also gives the `find --css` that repeats UTAM's search, with `--within`
|
|
120
|
+
the element it searched from when one of your page objects identifies it. Run it.
|
|
121
|
+
2. `find` that selector's tag (`--tag`), or the target's label, with `--all`.
|
|
122
|
+
3. `path` to the match and compare it with the page object: the right root, the right shadow
|
|
123
|
+
crossings, the visible copy. A wrongly scoped root fails with a wrong value, not an error: it
|
|
124
|
+
loaded a hidden copy. After your fix, run `path` again on the same snapshot: a scope warning that
|
|
125
|
+
is gone, or your page object on the path above the target, confirms it before a live run.
|
|
126
|
+
4. Fix, and confirm with a live run.
|
|
127
|
+
|
|
128
|
+
## Commands
|
|
129
|
+
|
|
130
|
+
Run from the project folder, or point `--project-dir <dir>` at it. Every command takes `--json`;
|
|
131
|
+
text output stops at 200 lines, and `--limit <n>` changes that. Every command but `lookup` reads
|
|
132
|
+
`latest.json` unless you give `--file <path>` (relative to the current folder).
|
|
133
|
+
|
|
134
|
+
### `find`: which nodes match
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx simply-utam snapshot find --text "<label>"
|
|
138
|
+
npx simply-utam snapshot find --tag <custom-element-tag> --all
|
|
139
|
+
npx simply-utam snapshot find --attr <attribute>=<value>
|
|
140
|
+
npx simply-utam snapshot find --class <class>
|
|
141
|
+
npx simply-utam snapshot find --css "<selector>"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`--text` is a case-insensitive substring of a node's own text, its shadow text, or any attribute
|
|
145
|
+
value except `class`. `--attr name` matches any node with that attribute; `--attr name=value` is a
|
|
146
|
+
case-insensitive substring of the value (`class=slds-button` also matches `slds-button_brand`).
|
|
147
|
+
`--class` matches one whole class token, as `.name` does. Each match is one line: its node handle,
|
|
148
|
+
then the node written like CSS (`tag#id.class[attr="value"]`), its text in quotes and its shadow text
|
|
149
|
+
after `#shadow`. Visible matches come first, in document order; hidden ones are counted, or listed
|
|
150
|
+
with `--all`, each with the hidden subtree it's in. When there are hidden matches, a last line says
|
|
151
|
+
which match comes first in the page.
|
|
152
|
+
|
|
153
|
+
`--css` tests a selector the way UTAM's `findElement` runs it: every match in document order, hidden
|
|
154
|
+
ones included, with the first (the one UTAM takes) marked, and a warning when that one is hidden. A
|
|
155
|
+
selector doesn't reach into a shadow root, as in the browser. It searches the document, which is
|
|
156
|
+
where a root page object's selector is looked up. For an element of a page object, add `--within
|
|
157
|
+
<handle>` for the page object's root element: alone it searches that element's light DOM
|
|
158
|
+
(`elements`), and with `--shadow` its shadow root (`shadow.elements`). `%s` in an attribute matches
|
|
159
|
+
any value; a test on a masked value can't be checked, and the output says so. For `title`,
|
|
160
|
+
`aria-label`, `placeholder` and `alt`, it also says to recapture with `UTAM_SNAPSHOT_MASK=aria`
|
|
161
|
+
(`--mask aria` for the console snippet), which keeps them on controls. With no match, it lists
|
|
162
|
+
where the selector does match, in a scope the search didn't cover: inside another node's shadow
|
|
163
|
+
root, in `--within`'s light DOM when you searched its shadow root, or outside `--within` in the same
|
|
164
|
+
tree. Each comes with the `--within` (and `--shadow`) that finds it. Lightning's synthetic shadow roots are boundaries too, as they are for
|
|
165
|
+
UTAM.
|
|
166
|
+
|
|
167
|
+
### `path`: from the page down to a node
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npx simply-utam snapshot path --node <handle>
|
|
171
|
+
npx simply-utam snapshot path --text "<label>"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
One custom element per line, indented, plus any plain element that one of your own root page
|
|
175
|
+
objects' selectors matches first: that's how a page object of yours that scopes a shipped one to a
|
|
176
|
+
container shows up. `#shadow(...)` means the next element is inside the previous
|
|
177
|
+
one's shadow root; without it, the element is in the light DOM (a child, or slotted content).
|
|
178
|
+
`(hidden)` marks the top of a hidden subtree. Each custom element is annotated with one of:
|
|
179
|
+
|
|
180
|
+
- **an import path**: its page object. `root`: `utam.load` can load it. `interface`: import the
|
|
181
|
+
interface; UTAM picks the implementation. `project`: one of yours, compiled to `pageObjects/`.
|
|
182
|
+
- **`↳ reachable through <page object>, element "<name>" "<selector>"`**: a page object above it has
|
|
183
|
+
an element that likely selects this one, with the getter (public) or the methods that use it
|
|
184
|
+
(private). `container` means the getter takes the page object to load there. `light` or `shadow`
|
|
185
|
+
is where the element is declared (`elements` or `shadow.elements`). Matched by tag for any page
|
|
186
|
+
object above, and for the nearest one also by its whole selector (such as `[slot='…']`), so treat
|
|
187
|
+
it as a likely route; the target itself gets one too, when it isn't a custom element.
|
|
188
|
+
- **`⚠ <your page object>: element "…" is declared in elements, but n5021 is in n5020's shadow
|
|
189
|
+
root`**: an element of one of your own page objects selects this node, but UTAM would search the
|
|
190
|
+
wrong scope for it, so it won't find it (nor anything nested in it). The line says which list to
|
|
191
|
+
move it to. Only your project's page objects are checked. A route below it that runs through
|
|
192
|
+
that element is printed as `↳ not reachable as declared`.
|
|
193
|
+
- **`⚠ root selector "…" matches N in the document`**: under a root page object, when its root
|
|
194
|
+
selector matches more than once or its first match is hidden. `utam.load` takes the first, so check
|
|
195
|
+
it with the `find --css` the line gives. It isn't shown when a page object of yours above it
|
|
196
|
+
reaches it through an element, because then the root isn't loaded with `utam.load`.
|
|
197
|
+
- **`⚠ unreadable shadow root`**: the component has a shadow root nothing can read, most likely a
|
|
198
|
+
closed one. UTAM selectors can't reach inside it either.
|
|
199
|
+
- **`no page object found`**: nothing in the index for that tag, which doesn't prove none exists.
|
|
200
|
+
|
|
201
|
+
`path --text` needs exactly one visible match; otherwise it lists the candidates and exits with 1.
|
|
202
|
+
|
|
203
|
+
### `node`: one element in full
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npx simply-utam snapshot node <handle>
|
|
207
|
+
npx simply-utam snapshot node <handle> --ancestors
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Its handle and its address (see Reference); every recorded attribute, unclipped; its text and
|
|
211
|
+
shadow text; its size and position, or which hidden subtree it's in; its parent in the snapshot; and
|
|
212
|
+
each of its light DOM and shadow root children on one line. Add `--json` for the same as data.
|
|
213
|
+
|
|
214
|
+
With `--ancestors`, the node's ancestors instead, nearest first, one line each: its handle, then the
|
|
215
|
+
node written like CSS. `— #shadow(mode) root of <handle> —` marks where the chain leaves a shadow
|
|
216
|
+
root. A selector can't reach past that line; to scope above it, use a page object.
|
|
217
|
+
|
|
218
|
+
### `tree`: what's under a node
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx simply-utam snapshot tree --node <handle> --depth 3
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Without `--node` it starts at the page's root; `--depth` defaults to 3. Lines as in `find`, with node handles. A long tree ends by repeating which node it started at. Under a node with a shadow root, its `#shadow(...)` children come
|
|
225
|
+
first, then its light DOM children (slotted content) under `light DOM:`; a node without a shadow root
|
|
226
|
+
lists its light DOM children directly below it. Plain wrapper elements with only a class are left out of
|
|
227
|
+
snapshots, so a child in the tree isn't always a direct child in the DOM.
|
|
228
|
+
|
|
229
|
+
### `lookup`: page objects
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
npx simply-utam snapshot lookup <custom-element-tag>
|
|
233
|
+
npx simply-utam snapshot lookup <pageObjectName>
|
|
234
|
+
npx simply-utam snapshot lookup --search "<words>"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
With a tag or name, shows each matching page object's import path, whether it's a root, its root
|
|
238
|
+
selector, an interface's implementations, its methods with their parameters and what they return,
|
|
239
|
+
its public elements with their getters, and its private elements with the methods that use them
|
|
240
|
+
(directly, or through an element nested in them, including a nested public element's getter).
|
|
241
|
+
Each element is marked `[light]` (declared in `elements`, searched in its parent's light DOM) or
|
|
242
|
+
`[shadow]` (in `shadow.elements`, searched in its parent's shadow root); its parent is the page
|
|
243
|
+
object's root element, or the element it's nested in (`parent.child`).
|
|
244
|
+
The line under each method summarizes what it does: each `compose` statement's action on an element
|
|
245
|
+
(with the element's selector), `→ chain` for a step on what the previous one returned, and
|
|
246
|
+
`waitFor(...)` with the elements it waits on.
|
|
247
|
+
|
|
248
|
+
A **container** element's getter takes a page object type, such as `getSecondaryFields(MyItem)`, and
|
|
249
|
+
returns that page object (or a list of them, for `[]`) rooted at the element the container selects.
|
|
250
|
+
A `name:` line is the page object's manifest name; a `.utam.json` `"type"` uses the import path on
|
|
251
|
+
the line above it.
|
|
252
|
+
That's how you attach your own page object below a shipped one: write it without `root` or
|
|
253
|
+
`selector`, import its compiled file, and pass it in.
|
|
254
|
+
|
|
255
|
+
With `--search`, lists page objects
|
|
256
|
+
whose name, import path, tags or description contain every word, when you don't know the name yet.
|
|
257
|
+
Reads your project's page objects and the page-object packages your namespaces map to
|
|
258
|
+
(`salesforce-pageobjects` through `lightning` in `generator.config.json`, a shared package through
|
|
259
|
+
`.utam/namespace-map.json`); needs no snapshot.
|
|
260
|
+
|
|
261
|
+
### Exit codes
|
|
262
|
+
|
|
263
|
+
0 success (including no matches); 1 `path --text` without exactly one visible match; 2 bad input (no
|
|
264
|
+
such file, not a snapshot, an unknown node handle, a selector that can't be read).
|
|
265
|
+
|
|
266
|
+
## When something doesn't match
|
|
267
|
+
|
|
268
|
+
- **No match for a label.** Check the header: is this the snapshot you think it is, taken after the
|
|
269
|
+
page loaded? Record values are masked (`"•14"` is 14 characters of data), so search by a field's
|
|
270
|
+
label, not its value.
|
|
271
|
+
- **A hidden node with nothing under it** was never rendered. Lightning renders some content, such
|
|
272
|
+
as inactive tabs and collapsed sections, only when it's opened. Open it in your step, then
|
|
273
|
+
capture.
|
|
274
|
+
- **Several matches, some hidden.** A previously visited page, an inactive console tab or a closed
|
|
275
|
+
section is still in the DOM. Scope the root selector so it matches only the visible copy, and test
|
|
276
|
+
it with `find --css`: a class such as `.active` can also appear inside the hidden copy. To find a
|
|
277
|
+
container that only the visible copy is in, compare `node <handle> --ancestors` for each copy.
|
|
278
|
+
- **A header marked `TRUNCATED`.** The capture stopped at a limit (`nodes`, `hiddenNodes` or
|
|
279
|
+
`time`), so part of the page isn't in the snapshot, and a node it doesn't have may still be on the
|
|
280
|
+
page. Close console tabs and pages the test doesn't need, and capture again.
|
|
281
|
+
- **An `iframe` node marked `not captured`.** Its content isn't in the snapshot.
|
|
282
|
+
- **An unreadable shadow root.** Content behind a closed shadow root can't be automated through
|
|
283
|
+
UTAM. UTAM's error `its scope does not have shadow root` is the sign of it.
|
|
284
|
+
|
|
285
|
+
## Page-object recipes
|
|
286
|
+
|
|
287
|
+
These are about writing UTAM page objects, and hold with or without snapshots. The commands above
|
|
288
|
+
tell you where to attach a page object and which copy to scope to; these say how.
|
|
289
|
+
|
|
290
|
+
### Attach your own page object below the last shipped one
|
|
291
|
+
|
|
292
|
+
The last line of `path` that names a page object is the attach point.
|
|
293
|
+
|
|
294
|
+
- **Through a container.** If that page object has a container element whose selector matches your
|
|
295
|
+
target or an element above it (`lookup` marks it `container`), write your page object without
|
|
296
|
+
`root` or `selector`, import its compiled file, and pass it to the container's getter, such as
|
|
297
|
+
`getSecondaryFields(MyItem)`.
|
|
298
|
+
- **Otherwise, from a root of your own.** Write a root page object whose selector matches an element
|
|
299
|
+
with no `#shadow` line above it on the path: a root selector is looked up in the document. From
|
|
300
|
+
there, nest one element per hop down to the target: `shadow.elements` for each `#shadow` line
|
|
301
|
+
`path` shows between two elements, and plain `elements` for a light DOM child or slotted content.
|
|
302
|
+
Give each nested element a selector; a non-root page object has none of its own, the element that
|
|
303
|
+
holds it does. For example, to reach an object label in a record page's highlights panel:
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"root": true,
|
|
308
|
+
"selector": { "css": "one-record-home-flexipage2" },
|
|
309
|
+
"shadow": {
|
|
310
|
+
"elements": [
|
|
311
|
+
{
|
|
312
|
+
"name": "highlights",
|
|
313
|
+
"selector": { "css": "records-highlights2" },
|
|
314
|
+
"elements": [{ "name": "objectLabel", "public": true, "selector": { "css": "records-entity-label" } }]
|
|
315
|
+
}
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Check each hop with `find --css "<selector>" --within <handle>` (add `--shadow` for a
|
|
322
|
+
`shadow.elements` hop), then `path` to the target: a misdeclared hop shows as `⚠ … Move it to …`.
|
|
323
|
+
|
|
324
|
+
- Keep every hop below the last shipped page object in a page object, not in step code. Don't reach
|
|
325
|
+
into UTAM's internals (`getRootElement().element` and the like) from a step.
|
|
326
|
+
|
|
327
|
+
### Scope a shipped root page object to the visible copy
|
|
328
|
+
|
|
329
|
+
When `find --css` shows a root page object's first match is a hidden copy, write a small root page
|
|
330
|
+
object of your own whose selector is the outermost container that tells the copies apart, with one
|
|
331
|
+
public element typed as the shipped page object. Don't copy the shipped JSON: wrapping keeps its
|
|
332
|
+
methods and its upgrades.
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"root": true,
|
|
337
|
+
"selector": { "css": ".oneContent.active.lafPageHost" },
|
|
338
|
+
"elements": [
|
|
339
|
+
{
|
|
340
|
+
"name": "recordHome",
|
|
341
|
+
"public": true,
|
|
342
|
+
"wait": true,
|
|
343
|
+
"type": "salesforce-pageobjects/global/pageObjects/recordHomeFlexipage2",
|
|
344
|
+
"selector": { "css": "one-record-home-flexipage2" }
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
- Type the element as the **interface** test code imports (`recordHomeFlexipage2`), not an
|
|
351
|
+
implementation, and use its **import path** (`salesforce-pageobjects/…`), not the manifest name
|
|
352
|
+
(`utam-global/…`). `lookup` shows both.
|
|
353
|
+
- Add `"wait": true`, so `waitForRecordHome()` waits for the page to render.
|
|
354
|
+
- Scope at the outermost container that differs between the copies, and prefer a child combinator
|
|
355
|
+
(`>`): a class such as `.active` can repeat inside the hidden copy, where a descendant selector
|
|
356
|
+
still finds it. Compare `node <handle> --ancestors` for each copy to find it.
|
|
357
|
+
- Check the selector with `find --css` on a snapshot of each kind of page the step runs on, such as
|
|
358
|
+
standard navigation and a console app, and don't claim a case you didn't check.
|
|
359
|
+
|
|
360
|
+
## Reference
|
|
361
|
+
|
|
362
|
+
- **Node handles** (`n5012`) are what commands take and what `find`, `path` and `tree` print. They're
|
|
363
|
+
numbered in the order `tree` prints nodes, so within one tree (outside any shadow root, or inside
|
|
364
|
+
the same one) a smaller handle comes earlier in the page. They hold for one snapshot only, so take
|
|
365
|
+
them from the snapshot you're reading.
|
|
366
|
+
- **Addresses** (`0.1.4.s.2`), shown by `node` and in `--json` output, say where a node sits: `0` is
|
|
367
|
+
`html`, each number is the nth child element, and `s` steps into a shadow root. An address stays
|
|
368
|
+
the same in another snapshot of the same page, as long as the page's structure is the same.
|
|
369
|
+
Commands don't take them; one passed to `--node` is refused with its handle.
|
|
370
|
+
- **Masking** is set by `UTAM_SNAPSHOT_MASK` when the test runs, and the header shows the mode.
|
|
371
|
+
From least kept to most:
|
|
372
|
+
- `structure` masks all text, and the labels of controls too: the safe mode, when record data
|
|
373
|
+
mustn't leave the org. `find --text` then has nothing to match; search by tag, attribute or class.
|
|
374
|
+
- `labels` (the default) keeps field labels, button text, tab names, column headers and object
|
|
375
|
+
names, and replaces other text with its length (`"•14"`), record ids with `•id` and email
|
|
376
|
+
addresses with `•email`. It also masks accessible names, which Lightning often builds from the
|
|
377
|
+
record ("Show more actions for Acme"): `title`, `aria-label`, `placeholder` and `alt`, the
|
|
378
|
+
assistive text inside icon buttons, and headings in modals.
|
|
379
|
+
- `aria` is `labels` that keeps those accessible names: the attributes on controls, the assistive
|
|
380
|
+
text and the modal headings. Then a selector like `button[title="Edit"]` can be checked.
|
|
381
|
+
Recapture with it when a `labels` snapshot can't check one.
|
|
382
|
+
- `full` keeps everything, record values included, so `find --text` can match a value. Use it only
|
|
383
|
+
on a dev org with no real data; capture warns when it's on.
|
|
384
|
+
|
|
385
|
+
In every mode, a failure's error message is recorded as the test printed it, unmasked.
|
|
386
|
+
|
|
387
|
+
- **Your own page objects** go in a `__utam__` folder as `<name>.utam.json`.
|
|
388
|
+
`npx utam -c utam.config.json` compiles them to `pageObjects/<name>.js`, which step definitions
|
|
389
|
+
import by relative path, or by `<package name>/pageObjects/<name>` once `package.json` `exports`
|
|
390
|
+
maps it (`init` adds that entry). `lookup` says which one works.
|