@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.
Files changed (34) hide show
  1. package/README.md +124 -1
  2. package/lib/clipboard.d.ts +31 -0
  3. package/lib/clipboard.js +98 -0
  4. package/lib/clipboard.js.map +1 -0
  5. package/lib/commands/init.d.ts +2 -1
  6. package/lib/commands/init.js +39 -13
  7. package/lib/commands/init.js.map +1 -1
  8. package/lib/commands/snapshot/console/import.d.ts +26 -0
  9. package/lib/commands/snapshot/console/import.js +96 -0
  10. package/lib/commands/snapshot/console/import.js.map +1 -0
  11. package/lib/commands/snapshot/console/snippet.d.ts +25 -0
  12. package/lib/commands/snapshot/console/snippet.js +72 -0
  13. package/lib/commands/snapshot/console/snippet.js.map +1 -0
  14. package/lib/commands/snapshot/find.d.ts +25 -0
  15. package/lib/commands/snapshot/find.js +82 -0
  16. package/lib/commands/snapshot/find.js.map +1 -0
  17. package/lib/commands/snapshot/lookup.d.ts +20 -0
  18. package/lib/commands/snapshot/lookup.js +57 -0
  19. package/lib/commands/snapshot/lookup.js.map +1 -0
  20. package/lib/commands/snapshot/node.d.ts +22 -0
  21. package/lib/commands/snapshot/node.js +61 -0
  22. package/lib/commands/snapshot/node.js.map +1 -0
  23. package/lib/commands/snapshot/path.d.ts +21 -0
  24. package/lib/commands/snapshot/path.js +77 -0
  25. package/lib/commands/snapshot/path.js.map +1 -0
  26. package/lib/commands/snapshot/tree.d.ts +19 -0
  27. package/lib/commands/snapshot/tree.js +50 -0
  28. package/lib/commands/snapshot/tree.js.map +1 -0
  29. package/lib/snapshot-flags.d.ts +40 -0
  30. package/lib/snapshot-flags.js +70 -0
  31. package/lib/snapshot-flags.js.map +1 -0
  32. package/oclif.manifest.json +553 -3
  33. package/package.json +16 -4
  34. 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.1.7",
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.1.3"
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": "630b75cd098255bf3c2126911a0a1f8c087eeea3"
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.