Repository navigation
Expand file tree
/
Copy pathtrace-actions.ts
More file actions
219 lines (207 loc) · 10.2 KB
/
Copy pathtrace-actions.ts
File metadata and controls
219 lines (207 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// The trace action vocabulary: maps runner-native command names to the trace
// class/method pair. Single source of truth for both the exporter (core, forward
// lookup) and the reader (backend, reverse lookup) so the two cannot drift.
export interface TraceAction {
class: string
method: string
}
/** Whether an action is a wait: every runner's waits map onto a `waitFor*` method. */
export function isWaitAction(action: TraceAction | null | undefined): boolean {
return action?.method.startsWith('waitFor') ?? false
}
/** Trace action class assertion commands map to. */
export const ASSERT_ACTION_CLASS = 'Assert'
/** node:assert methods the core assert patcher wraps; the reader derives its
* `Assert.<m>` → `assert.<m>` reverse entries from the same list. */
export const TRACKED_ASSERT_METHODS = [
'equal',
'strictEqual',
'deepEqual',
'deepStrictEqual',
'notEqual',
'notStrictEqual',
'notDeepEqual',
'notDeepStrictEqual',
'ok',
'fail',
'throws',
'doesNotThrow',
'rejects',
'doesNotReject',
'match',
'doesNotMatch'
] as const
// assert.<m> (node:assert), verify.<m> (nightwatch soft variants), and
// expect.<m> (synthesized failing-matcher entries) all render as Assert.
// Only FAILING expect-webdriverio matchers reach the command log today (via the
// reporter); recording passing matchers needs a per-adapter capture change.
const ASSERT_COMMAND_RE = /^(?:assert|verify|expect)\.(\w+)$/
export function mapAssertCommand(command: string): TraceAction | null {
const match = ASSERT_COMMAND_RE.exec(command)
return match ? { class: ASSERT_ACTION_CLASS, method: match[1] } : null
}
export const ACTION_MAP: Record<string, TraceAction> = {
// WDIO browser-level
url: { class: 'Page', method: 'navigate' },
navigateTo: { class: 'Page', method: 'navigate' },
back: { class: 'Page', method: 'goBack' },
forward: { class: 'Page', method: 'goForward' },
refresh: { class: 'Page', method: 'reload' },
newWindow: { class: 'Page', method: 'goto' },
// Selenium WebDriver navigation (driver.get, driver.navigate().to/back/forward/refresh)
get: { class: 'Page', method: 'navigate' },
to: { class: 'Page', method: 'navigate' },
// WDIO element-level
click: { class: 'Element', method: 'click' },
doubleClick: { class: 'Element', method: 'dblclick' },
setValue: { class: 'Element', method: 'fill' },
addValue: { class: 'Element', method: 'fill' },
selectByVisibleText: { class: 'Element', method: 'selectOption' },
moveTo: { class: 'Element', method: 'hover' },
scrollIntoView: { class: 'Element', method: 'scrollIntoViewIfNeeded' },
dragAndDrop: { class: 'Element', method: 'dragTo' },
// Selenium WebElement actions
sendKeys: { class: 'Element', method: 'fill' },
clear: { class: 'Element', method: 'clear' },
submit: { class: 'Element', method: 'submit' },
// Cross-runner
keys: { class: 'Keyboard', method: 'press' },
execute: { class: 'Page', method: 'evaluate' },
executeAsync: { class: 'Page', method: 'evaluate' },
switchToFrame: { class: 'Frame', method: 'goto' },
touchAction: { class: 'Element', method: 'tap' },
// WDIO element reads — surfaced so query steps appear in the timeline the way
// locator queries do in standard trace viewers. Adapters already capture
// these; only the export allow-list kept them out.
getText: { class: 'Element', method: 'getText' },
getValue: { class: 'Element', method: 'getValue' },
getAttribute: { class: 'Element', method: 'getAttribute' },
getProperty: { class: 'Element', method: 'getProperty' },
getCSSProperty: { class: 'Element', method: 'getCSSProperty' },
getTagName: { class: 'Element', method: 'getTagName' },
getLocation: { class: 'Element', method: 'getLocation' },
getSize: { class: 'Element', method: 'getSize' },
isDisplayed: { class: 'Element', method: 'isDisplayed' },
isExisting: { class: 'Element', method: 'isExisting' },
isEnabled: { class: 'Element', method: 'isEnabled' },
isSelected: { class: 'Element', method: 'isSelected' },
isClickable: { class: 'Element', method: 'isClickable' },
isFocused: { class: 'Element', method: 'isFocused' },
// Explicit user-facing waits (not the internal polling loops behind them).
waitForDisplayed: { class: 'Element', method: 'waitForDisplayed' },
waitForExist: { class: 'Element', method: 'waitForExist' },
waitForEnabled: { class: 'Element', method: 'waitForEnabled' },
waitForClickable: { class: 'Element', method: 'waitForClickable' },
waitUntil: { class: 'Browser', method: 'waitForFunction' },
// Nightwatch's wait family, normalized onto the WDIO names above. These are
// written by the user and are the commands a Nightwatch test most often fails
// on, so leaving them out of the allow-list meant the failing step produced no
// row at all — and with no row, its error had nowhere to attach and neither the
// action list nor the Errors tab showed the failure.
waitForElementVisible: { class: 'Element', method: 'waitForDisplayed' },
waitForElementNotVisible: { class: 'Element', method: 'waitForHidden' },
waitForElementPresent: { class: 'Element', method: 'waitForExist' },
waitForElementNotPresent: { class: 'Element', method: 'waitForNotExist' },
// A Selenium wait on an element locator (`until.elementLocated`, `presence_of_element_located`), named by its adapter.
waitForElementLocated: { class: 'Element', method: 'waitForExist' },
// WDIO page/browser reads
getTitle: { class: 'Page', method: 'getTitle' },
getUrl: { class: 'Page', method: 'getUrl' },
getPageSource: { class: 'Page', method: 'getPageSource' },
// Selenium read aliases — normalized onto the WDIO names above so both runners
// read identically. getText/getAttribute/getTagName/isDisplayed/isEnabled/
// isSelected share the command name across runners and need no alias.
getCssValue: { class: 'Element', method: 'getCSSProperty' },
getRect: { class: 'Element', method: 'getRect' },
getCurrentUrl: { class: 'Page', method: 'getUrl' },
// A Selenium wait on any condition other than an element locator.
wait: { class: 'Browser', method: 'waitForFunction' },
// Raw W3C protocol names. The JS adapters wrap a client library and report
// its method names, but an adapter that patches the protocol chokepoint —
// the Python one, and any future language binding — reports what WebDriver
// itself calls the command. Unmapped commands are dropped from a trace
// outright (`trace-action-events.ts` skips them), so without these a Python
// run exported 7 actions where the live dashboard had shown ~60: `get`,
// `getCurrentUrl` and `getTitle` survived only because those three names
// happen to be shared with the vocabulary above.
//
// `findElement`/`findElements` are deliberately absent, matching the JS
// adapters: locating an element is plumbing for the action that follows, not
// an action a reader wants a row for. So is `w3cExecuteScript`: selenium
// implements `is_displayed()` and the expected-condition waits by running its
// own JS atoms through it, so mapping it filled the trace with
// `Page.evaluate("/* isDisplayed */ …")` rows the test never wrote. The JS
// Selenium adapter's `executeScript` is unmapped for the same reason, and a
// user's own `execute_script` is the cost both pay.
clickElement: { class: 'Element', method: 'click' },
sendKeysToElement: { class: 'Element', method: 'fill' },
clearElement: { class: 'Element', method: 'clear' },
getElementText: { class: 'Element', method: 'getText' },
getElementAttribute: { class: 'Element', method: 'getAttribute' },
getElementProperty: { class: 'Element', method: 'getProperty' },
getElementValueOfCssProperty: { class: 'Element', method: 'getCSSProperty' },
getElementTagName: { class: 'Element', method: 'getTagName' },
getElementRect: { class: 'Element', method: 'getRect' },
isElementEnabled: { class: 'Element', method: 'isEnabled' },
isElementSelected: { class: 'Element', method: 'isSelected' },
goBack: { class: 'Page', method: 'goBack' },
goForward: { class: 'Page', method: 'goForward' },
switchToWindow: { class: 'Page', method: 'switchToWindow' },
switchToParentFrame: { class: 'Frame', method: 'goto' }
}
/** Trace methods (ACTION_MAP values) that act at a point on the page — the
* exporter emits a hit `point` for these so the player can mark where the
* action landed. `fill` is included (typing focuses the element first). */
export const POINTABLE_METHODS: ReadonlySet<string> = new Set([
'click',
'dblclick',
'hover',
'tap',
'dragTo',
'scrollIntoViewIfNeeded',
'selectOption',
'fill'
])
/** Runner command names (native or trace method) that type into an element —
* drives the timeline's keyboard glyph vs the pointer glyph. */
const KEYBOARD_COMMANDS: ReadonlySet<string> = new Set([
'setValue',
'sendKeys',
'addValue',
'clearValue',
'clear',
'keys',
'fill',
'press'
])
export function isKeyboardCommand(command: string): boolean {
return KEYBOARD_COMMANDS.has(command)
}
/** Runner-native pointer commands the trace vocabulary deliberately doesn't
* name — Nightwatch's low-level mouse API has no meaningful trace method, so
* adding it to ACTION_MAP would put noise in the timeline. Listed here because
* they still dispatch, and a command that dispatches unrecognized keeps the
* screenshot poll running straight through it. */
const UNMAPPED_POINTER_COMMANDS: ReadonlySet<string> = new Set([
'moveToElement',
'mouseButtonClick',
'mouseButtonDown',
'mouseButtonUp',
'rightClick',
'submitForm'
])
/**
* Whether a runner command makes the driver dispatch trusted input into the
* page. Derived from the vocabulary above rather than declared as a second list:
* a command qualifies when it types (`isKeyboardCommand`) or when its trace
* method acts at a point (`POINTABLE_METHODS`). A command wrongly classified as
* non-dispatching is the costly direction — it leaves the screencast poll racing
* the dispatch — so the unmapped pointer commands above are named explicitly.
*/
export function isInputDispatchingCommand(command: string): boolean {
if (isKeyboardCommand(command) || UNMAPPED_POINTER_COMMANDS.has(command)) {
return true
}
const action = ACTION_MAP[command]
return action !== undefined && POINTABLE_METHODS.has(action.method)
}