rebrowser-playwright-core 1.47.100 → 1.49.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 +1 -1
- package/ThirdPartyNotices.txt +70 -384
- package/bin/reinstall_chrome_beta_linux.sh +10 -8
- package/bin/reinstall_chrome_stable_linux.sh +10 -8
- package/bin/reinstall_msedge_beta_linux.sh +11 -9
- package/bin/reinstall_msedge_dev_linux.sh +11 -9
- package/bin/reinstall_msedge_stable_linux.sh +11 -9
- package/browsers.json +24 -12
- package/lib/cli/program.js +49 -19
- package/lib/client/api.js +6 -0
- package/lib/client/browserContext.js +20 -2
- package/lib/client/channelOwner.js +6 -2
- package/lib/client/connection.js +3 -0
- package/lib/client/fetch.js +16 -21
- package/lib/client/jsHandle.js +0 -8
- package/lib/client/localUtils.js +1 -0
- package/lib/client/locator.js +7 -0
- package/lib/client/network.js +180 -17
- package/lib/client/page.js +25 -2
- package/lib/client/playwright.js +6 -3
- package/lib/client/tracing.js +29 -20
- package/lib/client/waiter.js +15 -11
- package/lib/generated/consoleApiSource.js +1 -1
- package/lib/generated/injectedScriptSource.js +1 -1
- package/lib/generated/pollingRecorderSource.js +7 -0
- package/lib/generated/webSocketMockSource.js +7 -0
- package/lib/protocol/validator.js +103 -17
- package/lib/server/ariaSnapshot.js +33 -0
- package/lib/server/bidi/bidiBrowser.js +23 -8
- package/lib/server/bidi/bidiChromium.js +124 -0
- package/lib/server/bidi/bidiConnection.js +1 -3
- package/lib/server/bidi/bidiExecutionContext.js +0 -6
- package/lib/server/bidi/bidiFirefox.js +15 -21
- package/lib/server/bidi/bidiInput.js +16 -32
- package/lib/server/bidi/bidiNetworkManager.js +39 -5
- package/lib/server/bidi/bidiOverCdp.js +103 -0
- package/lib/server/bidi/bidiPage.js +98 -25
- package/lib/server/bidi/bidiPdf.js +140 -0
- package/lib/server/bidi/third_party/firefoxPrefs.js +221 -0
- package/lib/server/browser.js +13 -2
- package/lib/server/browserContext.js +6 -23
- package/lib/server/browserType.js +43 -12
- package/lib/server/chromium/chromium.js +9 -17
- package/lib/server/chromium/chromiumSwitches.js +3 -1
- package/lib/server/chromium/crBrowser.js +4 -3
- package/lib/server/chromium/crConnection.js +153 -0
- package/lib/server/chromium/crExecutionContext.js +0 -20
- package/lib/server/chromium/crPage.js +13 -7
- package/lib/server/chromium/videoRecorder.js +1 -1
- package/lib/server/codegen/csharp.js +12 -17
- package/lib/server/codegen/java.js +5 -8
- package/lib/server/codegen/javascript.js +29 -7
- package/lib/server/codegen/language.js +14 -2
- package/lib/server/codegen/python.js +13 -17
- package/lib/server/cookieStore.js +73 -0
- package/lib/server/debugController.js +10 -7
- package/lib/server/deviceDescriptorsSource.json +127 -127
- package/lib/server/dialog.js +1 -0
- package/lib/server/dispatchers/androidDispatcher.js +13 -2
- package/lib/server/dispatchers/browserContextDispatcher.js +19 -2
- package/lib/server/dispatchers/debugControllerDispatcher.js +1 -1
- package/lib/server/dispatchers/dispatcher.js +1 -1
- package/lib/server/dispatchers/frameDispatcher.js +8 -1
- package/lib/server/dispatchers/jsHandleDispatcher.js +0 -5
- package/lib/server/dispatchers/pageDispatcher.js +9 -0
- package/lib/server/dispatchers/playwrightDispatcher.js +2 -1
- package/lib/server/dispatchers/tracingDispatcher.js +10 -0
- package/lib/server/dispatchers/webSocketRouteDispatcher.js +189 -0
- package/lib/server/dom.js +79 -36
- package/lib/server/download.js +9 -2
- package/lib/server/fetch.js +105 -96
- package/lib/server/firefox/ffBrowser.js +6 -4
- package/lib/server/firefox/ffExecutionContext.js +0 -15
- package/lib/server/firefox/ffNetworkManager.js +1 -1
- package/lib/server/firefox/ffPage.js +6 -3
- package/lib/server/firefox/firefox.js +2 -13
- package/lib/server/frameSelectors.js +1 -1
- package/lib/server/frames.js +68 -67
- package/lib/server/har/harTracer.js +11 -0
- package/lib/server/index.js +0 -6
- package/lib/server/input.js +0 -1
- package/lib/server/javascript.js +0 -13
- package/lib/server/page.js +53 -9
- package/lib/server/playwright.js +5 -2
- package/lib/server/progress.js +0 -3
- package/lib/server/recorder/contextRecorder.js +33 -50
- package/lib/server/recorder/recorderApp.js +22 -25
- package/lib/server/recorder/recorderCollection.js +68 -79
- package/lib/server/recorder/recorderInTraceViewer.js +144 -0
- package/lib/server/recorder/recorderRunner.js +75 -97
- package/lib/server/recorder/recorderUtils.js +48 -6
- package/lib/server/recorder.js +87 -47
- package/lib/server/registry/index.js +154 -26
- package/lib/server/registry/nativeDeps.js +6 -3
- package/lib/server/socksClientCertificatesInterceptor.js +15 -3
- package/lib/server/trace/recorder/snapshotter.js +2 -12
- package/lib/server/trace/recorder/snapshotterInjected.js +21 -3
- package/lib/server/trace/recorder/tracing.js +127 -17
- package/lib/server/trace/test/inMemorySnapshotter.js +4 -4
- package/lib/server/trace/viewer/traceViewer.js +4 -6
- package/lib/server/webkit/webkit.js +1 -1
- package/lib/server/webkit/wkBrowser.js +8 -7
- package/lib/server/webkit/wkExecutionContext.js +0 -16
- package/lib/server/webkit/wkPage.js +11 -4
- package/lib/utils/comparators.js +12 -30
- package/lib/utils/crypto.js +1 -4
- package/lib/utils/happy-eyeballs.js +13 -0
- package/lib/utils/hostPlatform.js +8 -2
- package/lib/utils/httpServer.js +1 -0
- package/lib/utils/isomorphic/ariaSnapshot.js +267 -0
- package/lib/utils/isomorphic/locatorGenerators.js +30 -21
- package/lib/utils/isomorphic/locatorParser.js +2 -2
- package/lib/utils/isomorphic/recorderUtils.js +227 -0
- package/lib/utils/isomorphic/stringUtils.js +30 -0
- package/lib/utils/isomorphic/urlMatch.js +5 -1
- package/lib/utils/network.js +1 -1
- package/lib/utils/sequence.js +64 -0
- package/lib/utils/stackTrace.js +16 -3
- package/lib/utils/zones.js +32 -23
- package/lib/utilsBundle.js +4 -5
- package/lib/utilsBundleImpl/index.js +190 -33
- package/lib/vite/htmlReport/index.html +19 -16
- package/lib/vite/recorder/assets/codeMirrorModule-AFvV6hAs.js +24 -0
- package/lib/vite/recorder/assets/index-_cTWgVuJ.js +184 -0
- package/lib/vite/recorder/assets/{index-B-MT5gKo.css → index-iA1aAGZg.css} +1 -1
- package/lib/vite/recorder/index.html +2 -2
- package/lib/vite/traceViewer/assets/codeMirrorModule-KatbITLF.js +24 -0
- package/lib/vite/traceViewer/assets/inspectorTab-DdpLd2bb.js +68 -0
- package/lib/vite/traceViewer/assets/workbench-CdYbzWFD.js +9 -0
- package/lib/vite/traceViewer/embedded.6m3UZh7r.js +2 -0
- package/lib/vite/traceViewer/embedded.html +5 -3
- package/lib/vite/traceViewer/index.WUV-8boJ.js +2 -0
- package/lib/vite/traceViewer/index.html +24 -5
- package/lib/vite/traceViewer/inspectorTab.DEOUW62d.css +1 -0
- package/lib/vite/traceViewer/recorder.B_SY1GJM.css +0 -0
- package/lib/vite/traceViewer/recorder.OT2tVHgn.js +2 -0
- package/lib/vite/traceViewer/recorder.html +17 -0
- package/lib/vite/traceViewer/sw.bundle.js +3 -3
- package/lib/vite/traceViewer/uiMode.BZBTyvGn.js +5 -0
- package/lib/vite/traceViewer/uiMode.html +6 -4
- package/lib/vite/traceViewer/{uiMode.D3cNFP6u.css → uiMode.voC1ZiOQ.css} +1 -1
- package/lib/vite/traceViewer/workbench.C-zR9ysA.css +1 -0
- package/package.json +1 -1
- package/types/protocol.d.ts +135 -16
- package/types/types.d.ts +2149 -965
- package/lib/generated/recorderSource.js +0 -7
- package/lib/third_party/diff_match_patch.js +0 -2222
- package/lib/vite/recorder/assets/codeMirrorModule-aW6Vz3gf.js +0 -24
- package/lib/vite/recorder/assets/index-DCpXOVIh.js +0 -47
- package/lib/vite/traceViewer/assets/codeMirrorModule-eFKCUIu2.js +0 -24
- package/lib/vite/traceViewer/assets/workbench-D0cirtfA.js +0 -72
- package/lib/vite/traceViewer/embedded.BkuvsLJp.js +0 -2
- package/lib/vite/traceViewer/index.C968kAiC.js +0 -2
- package/lib/vite/traceViewer/uiMode.BkFnr4V0.js +0 -10
- package/lib/vite/traceViewer/workbench.DjbIuxix.css +0 -1
- /package/lib/server/recorder/{recorderActions.js → recorderFrontend.js} +0 -0
package/lib/utils/comparators.js
CHANGED
|
@@ -5,7 +5,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.getComparator = getComparator;
|
|
7
7
|
var _utilsBundle = require("../utilsBundle");
|
|
8
|
+
var _pixelmatch = _interopRequireDefault(require("../third_party/pixelmatch"));
|
|
8
9
|
var _compare = require("../image_tools/compare");
|
|
10
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
9
11
|
/**
|
|
10
12
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
11
13
|
* Modifications copyright (c) Microsoft Corporation.
|
|
@@ -23,13 +25,8 @@ var _compare = require("../image_tools/compare");
|
|
|
23
25
|
* limitations under the License.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
diff_match_patch,
|
|
29
|
-
DIFF_INSERT,
|
|
30
|
-
DIFF_DELETE,
|
|
31
|
-
DIFF_EQUAL
|
|
32
|
-
} = require('../third_party/diff_match_patch');
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
|
|
33
30
|
function getComparator(mimeType) {
|
|
34
31
|
if (mimeType === 'image/png') return compareImages.bind(null, 'image/png');
|
|
35
32
|
if (mimeType === 'image/jpeg') return compareImages.bind(null, 'image/jpeg');
|
|
@@ -83,7 +80,7 @@ function compareImages(mimeType, actualBuffer, expectedBuffer, options = {}) {
|
|
|
83
80
|
});
|
|
84
81
|
} else if (((_options$comparator = options.comparator) !== null && _options$comparator !== void 0 ? _options$comparator : 'pixelmatch') === 'pixelmatch') {
|
|
85
82
|
var _options$threshold;
|
|
86
|
-
count =
|
|
83
|
+
count = (0, _pixelmatch.default)(expected.data, actual.data, diff.data, size.width, size.height, {
|
|
87
84
|
threshold: (_options$threshold = options.threshold) !== null && _options$threshold !== void 0 ? _options$threshold : 0.2
|
|
88
85
|
});
|
|
89
86
|
} else {
|
|
@@ -116,32 +113,17 @@ function compareText(actual, expectedBuffer) {
|
|
|
116
113
|
};
|
|
117
114
|
const expected = expectedBuffer.toString('utf-8');
|
|
118
115
|
if (expected === actual) return null;
|
|
119
|
-
const
|
|
120
|
-
const d = dmp.diff_main(expected, actual);
|
|
121
|
-
dmp.diff_cleanupSemantic(d);
|
|
116
|
+
const diffs = _utilsBundle.diff.diffChars(expected, actual);
|
|
122
117
|
return {
|
|
123
|
-
errorMessage: diff_prettyTerminal(
|
|
118
|
+
errorMessage: diff_prettyTerminal(diffs)
|
|
124
119
|
};
|
|
125
120
|
}
|
|
126
121
|
function diff_prettyTerminal(diffs) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
switch (op) {
|
|
133
|
-
case DIFF_INSERT:
|
|
134
|
-
html[x] = _utilsBundle.colors.green(text);
|
|
135
|
-
break;
|
|
136
|
-
case DIFF_DELETE:
|
|
137
|
-
html[x] = _utilsBundle.colors.reset(_utilsBundle.colors.strikethrough(_utilsBundle.colors.red(text)));
|
|
138
|
-
break;
|
|
139
|
-
case DIFF_EQUAL:
|
|
140
|
-
html[x] = text;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return html.join('');
|
|
122
|
+
const result = diffs.map(part => {
|
|
123
|
+
const text = part.value;
|
|
124
|
+
if (part.added) return _utilsBundle.colors.green(text);else if (part.removed) return _utilsBundle.colors.reset(_utilsBundle.colors.strikethrough(_utilsBundle.colors.red(text)));else return text;
|
|
125
|
+
});
|
|
126
|
+
return result.join('');
|
|
145
127
|
}
|
|
146
128
|
function resizeImage(image, size) {
|
|
147
129
|
if (image.width === size.width && image.height === size.height) return image;
|
package/lib/utils/crypto.js
CHANGED
|
@@ -45,7 +45,6 @@ function encodeBase128(value) {
|
|
|
45
45
|
} while (value > 0);
|
|
46
46
|
return Buffer.from(bytes.reverse());
|
|
47
47
|
}
|
|
48
|
-
;
|
|
49
48
|
|
|
50
49
|
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
|
|
51
50
|
class DER {
|
|
@@ -60,9 +59,7 @@ class DER {
|
|
|
60
59
|
const parts = oid.split('.').map(v => Number(v));
|
|
61
60
|
// Encode the second part, which could be large, using base-128 encoding if necessary
|
|
62
61
|
const output = [encodeBase128(40 * parts[0] + parts[1])];
|
|
63
|
-
for (let i = 2; i < parts.length; i++)
|
|
64
|
-
output.push(encodeBase128(parts[i]));
|
|
65
|
-
}
|
|
62
|
+
for (let i = 2; i < parts.length; i++) output.push(encodeBase128(parts[i]));
|
|
66
63
|
return this._encode(0x06, Buffer.concat(output));
|
|
67
64
|
}
|
|
68
65
|
static encodeNull() {
|
|
@@ -7,6 +7,7 @@ exports.createConnectionAsync = createConnectionAsync;
|
|
|
7
7
|
exports.createSocket = createSocket;
|
|
8
8
|
exports.createTLSSocket = createTLSSocket;
|
|
9
9
|
exports.httpsHappyEyeballsAgent = exports.httpHappyEyeballsAgent = void 0;
|
|
10
|
+
exports.timingForSocket = timingForSocket;
|
|
10
11
|
var dns = _interopRequireWildcard(require("dns"));
|
|
11
12
|
var http = _interopRequireWildcard(require("http"));
|
|
12
13
|
var https = _interopRequireWildcard(require("https"));
|
|
@@ -14,6 +15,7 @@ var net = _interopRequireWildcard(require("net"));
|
|
|
14
15
|
var tls = _interopRequireWildcard(require("tls"));
|
|
15
16
|
var _manualPromise = require("./manualPromise");
|
|
16
17
|
var _debug = require("./debug");
|
|
18
|
+
var _time = require("./time");
|
|
17
19
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
18
20
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
19
21
|
/**
|
|
@@ -37,6 +39,8 @@ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e;
|
|
|
37
39
|
|
|
38
40
|
// Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
|
|
39
41
|
const connectionAttemptDelayMs = 300;
|
|
42
|
+
const kDNSLookupAt = Symbol('kDNSLookupAt');
|
|
43
|
+
const kTCPConnectionAt = Symbol('kTCPConnectionAt');
|
|
40
44
|
class HttpHappyEyeballsAgent extends http.Agent {
|
|
41
45
|
createConnection(options, oncreate) {
|
|
42
46
|
// There is no ambiguity in case of IP address.
|
|
@@ -101,6 +105,7 @@ async function createConnectionAsync(options, oncreate, useTLS) {
|
|
|
101
105
|
const lookup = options.__testHookLookup || lookupAddresses;
|
|
102
106
|
const hostname = clientRequestArgsToHostName(options);
|
|
103
107
|
const addresses = await lookup(hostname);
|
|
108
|
+
const dnsLookupAt = (0, _time.monotonicTime)();
|
|
104
109
|
const sockets = new Set();
|
|
105
110
|
let firstError;
|
|
106
111
|
let errorCount = 0;
|
|
@@ -125,10 +130,12 @@ async function createConnectionAsync(options, oncreate, useTLS) {
|
|
|
125
130
|
port: options.port,
|
|
126
131
|
host: address
|
|
127
132
|
});
|
|
133
|
+
socket[kDNSLookupAt] = dnsLookupAt;
|
|
128
134
|
|
|
129
135
|
// Each socket may fire only one of 'connect', 'timeout' or 'error' events.
|
|
130
136
|
// None of these events are fired after socket.destroy() is called.
|
|
131
137
|
socket.on('connect', () => {
|
|
138
|
+
socket[kTCPConnectionAt] = (0, _time.monotonicTime)();
|
|
132
139
|
connected.resolve();
|
|
133
140
|
oncreate === null || oncreate === void 0 || oncreate(null, socket);
|
|
134
141
|
// TODO: Cache the result?
|
|
@@ -178,4 +185,10 @@ function clientRequestArgsToHostName(options) {
|
|
|
178
185
|
if (options.hostname) return options.hostname;
|
|
179
186
|
if (options.host) return options.host;
|
|
180
187
|
throw new Error('Either options.hostname or options.host must be provided');
|
|
188
|
+
}
|
|
189
|
+
function timingForSocket(socket) {
|
|
190
|
+
return {
|
|
191
|
+
dnsLookupAt: socket[kDNSLookupAt],
|
|
192
|
+
tcpConnectionAt: socket[kTCPConnectionAt]
|
|
193
|
+
};
|
|
181
194
|
}
|
|
@@ -24,6 +24,12 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
function calculatePlatform() {
|
|
27
|
+
if (process.env.PLAYWRIGHT_HOST_PLATFORM_OVERRIDE) {
|
|
28
|
+
return {
|
|
29
|
+
hostPlatform: process.env.PLAYWRIGHT_HOST_PLATFORM_OVERRIDE,
|
|
30
|
+
isOfficiallySupportedPlatform: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
27
33
|
const platform = _os.default.platform();
|
|
28
34
|
if (platform === 'darwin') {
|
|
29
35
|
const ver = _os.default.release().split('.').map(a => parseInt(a, 10));
|
|
@@ -37,9 +43,9 @@ function calculatePlatform() {
|
|
|
37
43
|
macVersion = 'mac10.15';
|
|
38
44
|
} else {
|
|
39
45
|
// ver[0] >= 20
|
|
40
|
-
const
|
|
46
|
+
const LAST_STABLE_MACOS_MAJOR_VERSION = 15;
|
|
41
47
|
// Best-effort support for MacOS beta versions.
|
|
42
|
-
macVersion = 'mac' + Math.min(ver[0] - 9,
|
|
48
|
+
macVersion = 'mac' + Math.min(ver[0] - 9, LAST_STABLE_MACOS_MAJOR_VERSION);
|
|
43
49
|
// BigSur is the first version that might run on Apple Silicon.
|
|
44
50
|
if (_os.default.cpus().some(cpu => cpu.model.includes('Apple'))) macVersion += '-arm64';
|
|
45
51
|
}
|
package/lib/utils/httpServer.js
CHANGED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.AriaKeyError = void 0;
|
|
7
|
+
exports.parseAriaKey = parseAriaKey;
|
|
8
|
+
exports.parseYamlTemplate = parseYamlTemplate;
|
|
9
|
+
/**
|
|
10
|
+
* Copyright (c) Microsoft Corporation.
|
|
11
|
+
*
|
|
12
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
* you may not use this file except in compliance with the License.
|
|
14
|
+
* You may obtain a copy of the License at
|
|
15
|
+
*
|
|
16
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
*
|
|
18
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
* See the License for the specific language governing permissions and
|
|
22
|
+
* limitations under the License.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// https://www.w3.org/TR/wai-aria-1.2/#role_definitions
|
|
26
|
+
|
|
27
|
+
function parseYamlTemplate(fragment) {
|
|
28
|
+
const result = {
|
|
29
|
+
kind: 'role',
|
|
30
|
+
role: 'fragment'
|
|
31
|
+
};
|
|
32
|
+
populateNode(result, fragment);
|
|
33
|
+
if (result.children && result.children.length === 1) return result.children[0];
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function populateNode(node, container) {
|
|
37
|
+
for (const object of container) {
|
|
38
|
+
if (typeof object === 'string') {
|
|
39
|
+
const childNode = KeyParser.parse(object);
|
|
40
|
+
node.children = node.children || [];
|
|
41
|
+
node.children.push(childNode);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const key of Object.keys(object)) {
|
|
45
|
+
node.children = node.children || [];
|
|
46
|
+
const value = object[key];
|
|
47
|
+
if (key === 'text') {
|
|
48
|
+
node.children.push({
|
|
49
|
+
kind: 'text',
|
|
50
|
+
text: valueOrRegex(value)
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const childNode = KeyParser.parse(key);
|
|
55
|
+
if (childNode.kind === 'text') {
|
|
56
|
+
node.children.push({
|
|
57
|
+
kind: 'text',
|
|
58
|
+
text: valueOrRegex(value)
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'string') {
|
|
63
|
+
node.children.push({
|
|
64
|
+
...childNode,
|
|
65
|
+
children: [{
|
|
66
|
+
kind: 'text',
|
|
67
|
+
text: valueOrRegex(value)
|
|
68
|
+
}]
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
node.children.push(childNode);
|
|
73
|
+
populateNode(childNode, value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function normalizeWhitespace(text) {
|
|
78
|
+
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
|
|
79
|
+
}
|
|
80
|
+
function valueOrRegex(value) {
|
|
81
|
+
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value);
|
|
82
|
+
}
|
|
83
|
+
class KeyParser {
|
|
84
|
+
static parse(input) {
|
|
85
|
+
return new KeyParser(input)._parse();
|
|
86
|
+
}
|
|
87
|
+
constructor(input) {
|
|
88
|
+
this._input = void 0;
|
|
89
|
+
this._pos = void 0;
|
|
90
|
+
this._length = void 0;
|
|
91
|
+
this._input = input;
|
|
92
|
+
this._pos = 0;
|
|
93
|
+
this._length = input.length;
|
|
94
|
+
}
|
|
95
|
+
_peek() {
|
|
96
|
+
return this._input[this._pos] || '';
|
|
97
|
+
}
|
|
98
|
+
_next() {
|
|
99
|
+
if (this._pos < this._length) return this._input[this._pos++];
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
_eof() {
|
|
103
|
+
return this._pos >= this._length;
|
|
104
|
+
}
|
|
105
|
+
_isWhitespace() {
|
|
106
|
+
return !this._eof() && /\s/.test(this._peek());
|
|
107
|
+
}
|
|
108
|
+
_skipWhitespace() {
|
|
109
|
+
while (this._isWhitespace()) this._pos++;
|
|
110
|
+
}
|
|
111
|
+
_readIdentifier(type) {
|
|
112
|
+
if (this._eof()) this._throwError(`Unexpected end of input when expecting ${type}`);
|
|
113
|
+
const start = this._pos;
|
|
114
|
+
while (!this._eof() && /[a-zA-Z]/.test(this._peek())) this._pos++;
|
|
115
|
+
return this._input.slice(start, this._pos);
|
|
116
|
+
}
|
|
117
|
+
_readString() {
|
|
118
|
+
let result = '';
|
|
119
|
+
let escaped = false;
|
|
120
|
+
while (!this._eof()) {
|
|
121
|
+
const ch = this._next();
|
|
122
|
+
if (escaped) {
|
|
123
|
+
result += ch;
|
|
124
|
+
escaped = false;
|
|
125
|
+
} else if (ch === '\\') {
|
|
126
|
+
escaped = true;
|
|
127
|
+
} else if (ch === '"') {
|
|
128
|
+
return result;
|
|
129
|
+
} else {
|
|
130
|
+
result += ch;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this._throwError('Unterminated string');
|
|
134
|
+
}
|
|
135
|
+
_throwError(message, pos) {
|
|
136
|
+
throw new AriaKeyError(message, this._input, pos || this._pos);
|
|
137
|
+
}
|
|
138
|
+
_readRegex() {
|
|
139
|
+
let result = '';
|
|
140
|
+
let escaped = false;
|
|
141
|
+
let insideClass = false;
|
|
142
|
+
while (!this._eof()) {
|
|
143
|
+
const ch = this._next();
|
|
144
|
+
if (escaped) {
|
|
145
|
+
result += ch;
|
|
146
|
+
escaped = false;
|
|
147
|
+
} else if (ch === '\\') {
|
|
148
|
+
escaped = true;
|
|
149
|
+
result += ch;
|
|
150
|
+
} else if (ch === '/' && !insideClass) {
|
|
151
|
+
return result;
|
|
152
|
+
} else if (ch === '[') {
|
|
153
|
+
insideClass = true;
|
|
154
|
+
result += ch;
|
|
155
|
+
} else if (ch === ']' && insideClass) {
|
|
156
|
+
result += ch;
|
|
157
|
+
insideClass = false;
|
|
158
|
+
} else {
|
|
159
|
+
result += ch;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this._throwError('Unterminated regex');
|
|
163
|
+
}
|
|
164
|
+
_readStringOrRegex() {
|
|
165
|
+
const ch = this._peek();
|
|
166
|
+
if (ch === '"') {
|
|
167
|
+
this._next();
|
|
168
|
+
return this._readString();
|
|
169
|
+
}
|
|
170
|
+
if (ch === '/') {
|
|
171
|
+
this._next();
|
|
172
|
+
return new RegExp(this._readRegex());
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
_readAttributes(result) {
|
|
177
|
+
let errorPos = this._pos;
|
|
178
|
+
while (true) {
|
|
179
|
+
this._skipWhitespace();
|
|
180
|
+
if (this._peek() === '[') {
|
|
181
|
+
this._next();
|
|
182
|
+
this._skipWhitespace();
|
|
183
|
+
errorPos = this._pos;
|
|
184
|
+
const flagName = this._readIdentifier('attribute');
|
|
185
|
+
this._skipWhitespace();
|
|
186
|
+
let flagValue = '';
|
|
187
|
+
if (this._peek() === '=') {
|
|
188
|
+
this._next();
|
|
189
|
+
this._skipWhitespace();
|
|
190
|
+
errorPos = this._pos;
|
|
191
|
+
while (this._peek() !== ']' && !this._isWhitespace() && !this._eof()) flagValue += this._next();
|
|
192
|
+
}
|
|
193
|
+
this._skipWhitespace();
|
|
194
|
+
if (this._peek() !== ']') this._throwError('Expected ]');
|
|
195
|
+
this._next(); // Consume ']'
|
|
196
|
+
this._applyAttribute(result, flagName, flagValue || 'true', errorPos);
|
|
197
|
+
} else {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
_parse() {
|
|
203
|
+
this._skipWhitespace();
|
|
204
|
+
const role = this._readIdentifier('role');
|
|
205
|
+
this._skipWhitespace();
|
|
206
|
+
const name = this._readStringOrRegex() || '';
|
|
207
|
+
const result = {
|
|
208
|
+
kind: 'role',
|
|
209
|
+
role,
|
|
210
|
+
name
|
|
211
|
+
};
|
|
212
|
+
this._readAttributes(result);
|
|
213
|
+
this._skipWhitespace();
|
|
214
|
+
if (!this._eof()) this._throwError('Unexpected input');
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
_applyAttribute(node, key, value, errorPos) {
|
|
218
|
+
if (key === 'checked') {
|
|
219
|
+
this._assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "checked\" attribute must be a boolean or "mixed"', errorPos);
|
|
220
|
+
node.checked = value === 'true' ? true : value === 'false' ? false : 'mixed';
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (key === 'disabled') {
|
|
224
|
+
this._assert(value === 'true' || value === 'false', 'Value of "disabled" attribute must be a boolean', errorPos);
|
|
225
|
+
node.disabled = value === 'true';
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (key === 'expanded') {
|
|
229
|
+
this._assert(value === 'true' || value === 'false', 'Value of "expanded" attribute must be a boolean', errorPos);
|
|
230
|
+
node.expanded = value === 'true';
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (key === 'level') {
|
|
234
|
+
this._assert(!isNaN(Number(value)), 'Value of "level" attribute must be a number', errorPos);
|
|
235
|
+
node.level = Number(value);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (key === 'pressed') {
|
|
239
|
+
this._assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "pressed" attribute must be a boolean or "mixed"', errorPos);
|
|
240
|
+
node.pressed = value === 'true' ? true : value === 'false' ? false : 'mixed';
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (key === 'selected') {
|
|
244
|
+
this._assert(value === 'true' || value === 'false', 'Value of "selected" attribute must be a boolean', errorPos);
|
|
245
|
+
node.selected = value === 'true';
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this._assert(false, `Unsupported attribute [${key}]`, errorPos);
|
|
249
|
+
}
|
|
250
|
+
_assert(value, message, valuePos) {
|
|
251
|
+
if (!value) this._throwError(message || 'Assertion error', valuePos);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function parseAriaKey(key) {
|
|
255
|
+
return KeyParser.parse(key);
|
|
256
|
+
}
|
|
257
|
+
class AriaKeyError extends Error {
|
|
258
|
+
constructor(message, input, pos) {
|
|
259
|
+
super(message + ':\n\n' + input + '\n' + ' '.repeat(pos) + '^\n');
|
|
260
|
+
this.shortMessage = void 0;
|
|
261
|
+
this.pos = void 0;
|
|
262
|
+
this.shortMessage = message;
|
|
263
|
+
this.pos = pos;
|
|
264
|
+
this.stack = undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
exports.AriaKeyError = AriaKeyError;
|
|
@@ -37,15 +37,6 @@ function asLocators(lang, selector, isFrameLocator = false, maxOutputSize = 20,
|
|
|
37
37
|
}
|
|
38
38
|
function innerAsLocators(factory, parsed, isFrameLocator = false, maxOutputSize = 20) {
|
|
39
39
|
const parts = [...parsed.parts];
|
|
40
|
-
// frameLocator('iframe').first is actually "iframe >> nth=0 >> internal:control=enter-frame"
|
|
41
|
-
// To make it easier to parse, we turn it into "iframe >> internal:control=enter-frame >> nth=0"
|
|
42
|
-
for (let index = 0; index < parts.length - 1; index++) {
|
|
43
|
-
if (parts[index].name === 'nth' && parts[index + 1].name === 'internal:control' && parts[index + 1].body === 'enter-frame') {
|
|
44
|
-
// Swap nth and enter-frame.
|
|
45
|
-
const [nth] = parts.splice(index, 1);
|
|
46
|
-
parts.splice(index + 1, 0, nth);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
40
|
const tokens = [];
|
|
50
41
|
let nextBase = isFrameLocator ? 'frame-locator' : 'page';
|
|
51
42
|
for (let index = 0; index < parts.length; index++) {
|
|
@@ -183,18 +174,28 @@ function innerAsLocators(factory, parsed, isFrameLocator = false, maxOutputSize
|
|
|
183
174
|
continue;
|
|
184
175
|
}
|
|
185
176
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
177
|
+
if (part.name === 'internal:control' && part.body === 'enter-frame') {
|
|
178
|
+
// transform last tokens from `${selector}` into `${selector}.contentFrame()` and `frameLocator(${selector})`
|
|
179
|
+
const lastTokens = tokens[tokens.length - 1];
|
|
180
|
+
const lastPart = parts[index - 1];
|
|
181
|
+
const transformed = lastTokens.map(token => factory.chainLocators([token, factory.generateLocator(base, 'frame', '')]));
|
|
182
|
+
if (['xpath', 'css'].includes(lastPart.name)) {
|
|
183
|
+
transformed.push(factory.generateLocator(base, 'frame-locator', (0, _selectorParser.stringifySelector)({
|
|
184
|
+
parts: [lastPart]
|
|
185
|
+
})), factory.generateLocator(base, 'frame-locator', (0, _selectorParser.stringifySelector)({
|
|
186
|
+
parts: [lastPart]
|
|
187
|
+
}, true)));
|
|
188
|
+
}
|
|
189
|
+
lastTokens.splice(0, lastTokens.length, ...transformed);
|
|
190
190
|
nextBase = 'frame-locator';
|
|
191
|
-
|
|
191
|
+
continue;
|
|
192
192
|
}
|
|
193
|
+
const nextPart = parts[index + 1];
|
|
193
194
|
const selectorPart = (0, _selectorParser.stringifySelector)({
|
|
194
195
|
parts: [part]
|
|
195
196
|
});
|
|
196
|
-
const locatorPart = factory.generateLocator(base,
|
|
197
|
-
if (
|
|
197
|
+
const locatorPart = factory.generateLocator(base, 'default', selectorPart);
|
|
198
|
+
if (nextPart && ['internal:has-text', 'internal:has-not-text'].includes(nextPart.name)) {
|
|
198
199
|
const {
|
|
199
200
|
exact,
|
|
200
201
|
text
|
|
@@ -222,7 +223,7 @@ function innerAsLocators(factory, parsed, isFrameLocator = false, maxOutputSize
|
|
|
222
223
|
const selectorPart = (0, _selectorParser.stringifySelector)({
|
|
223
224
|
parts: [part]
|
|
224
225
|
}, /* forceEngineName */true);
|
|
225
|
-
locatorPartWithEngine = factory.generateLocator(base,
|
|
226
|
+
locatorPartWithEngine = factory.generateLocator(base, 'default', selectorPart);
|
|
226
227
|
}
|
|
227
228
|
tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean));
|
|
228
229
|
}
|
|
@@ -276,8 +277,10 @@ class JavaScriptLocatorFactory {
|
|
|
276
277
|
if (options.hasText !== undefined) return `locator(${this.quote(body)}, { hasText: ${this.toHasText(options.hasText)} })`;
|
|
277
278
|
if (options.hasNotText !== undefined) return `locator(${this.quote(body)}, { hasNotText: ${this.toHasText(options.hasNotText)} })`;
|
|
278
279
|
return `locator(${this.quote(body)})`;
|
|
279
|
-
case 'frame':
|
|
280
|
+
case 'frame-locator':
|
|
280
281
|
return `frameLocator(${this.quote(body)})`;
|
|
282
|
+
case 'frame':
|
|
283
|
+
return `contentFrame()`;
|
|
281
284
|
case 'nth':
|
|
282
285
|
return `nth(${body})`;
|
|
283
286
|
case 'first':
|
|
@@ -359,8 +362,10 @@ class PythonLocatorFactory {
|
|
|
359
362
|
if (options.hasText !== undefined) return `locator(${this.quote(body)}, has_text=${this.toHasText(options.hasText)})`;
|
|
360
363
|
if (options.hasNotText !== undefined) return `locator(${this.quote(body)}, has_not_text=${this.toHasText(options.hasNotText)})`;
|
|
361
364
|
return `locator(${this.quote(body)})`;
|
|
362
|
-
case 'frame':
|
|
365
|
+
case 'frame-locator':
|
|
363
366
|
return `frame_locator(${this.quote(body)})`;
|
|
367
|
+
case 'frame':
|
|
368
|
+
return `content_frame`;
|
|
364
369
|
case 'nth':
|
|
365
370
|
return `nth(${body})`;
|
|
366
371
|
case 'first':
|
|
@@ -459,8 +464,10 @@ class JavaLocatorFactory {
|
|
|
459
464
|
if (options.hasText !== undefined) return `locator(${this.quote(body)}, new ${clazz}.LocatorOptions().setHasText(${this.toHasText(options.hasText)}))`;
|
|
460
465
|
if (options.hasNotText !== undefined) return `locator(${this.quote(body)}, new ${clazz}.LocatorOptions().setHasNotText(${this.toHasText(options.hasNotText)}))`;
|
|
461
466
|
return `locator(${this.quote(body)})`;
|
|
462
|
-
case 'frame':
|
|
467
|
+
case 'frame-locator':
|
|
463
468
|
return `frameLocator(${this.quote(body)})`;
|
|
469
|
+
case 'frame':
|
|
470
|
+
return `contentFrame()`;
|
|
464
471
|
case 'nth':
|
|
465
472
|
return `nth(${body})`;
|
|
466
473
|
case 'first':
|
|
@@ -543,8 +550,10 @@ class CSharpLocatorFactory {
|
|
|
543
550
|
if (options.hasText !== undefined) return `Locator(${this.quote(body)}, new() { ${this.toHasText(options.hasText)} })`;
|
|
544
551
|
if (options.hasNotText !== undefined) return `Locator(${this.quote(body)}, new() { ${this.toHasNotText(options.hasNotText)} })`;
|
|
545
552
|
return `Locator(${this.quote(body)})`;
|
|
546
|
-
case 'frame':
|
|
553
|
+
case 'frame-locator':
|
|
547
554
|
return `FrameLocator(${this.quote(body)})`;
|
|
555
|
+
case 'frame':
|
|
556
|
+
return `ContentFrame`;
|
|
548
557
|
case 'nth':
|
|
549
558
|
return `Nth(${body})`;
|
|
550
559
|
case 'first':
|
|
@@ -63,7 +63,7 @@ function parseLocator(locator, testIdAttributeName) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Equalize languages.
|
|
66
|
-
template = template.toLowerCase().replace(/get_by_alt_text/g, 'getbyalttext').replace(/get_by_test_id/g, 'getbytestid').replace(/get_by_([\w]+)/g, 'getby$1').replace(/has_not_text/g, 'hasnottext').replace(/has_text/g, 'hastext').replace(/has_not/g, 'hasnot').replace(/frame_locator/g, 'framelocator').replace(/[{}\s]/g, '').replace(/new\(\)/g, '').replace(/new[\w]+\.[\w]+options\(\)/g, '').replace(/\.set/g, ',set').replace(/\.or_\(/g, 'or(') // Python has "or_" instead of "or".
|
|
66
|
+
template = template.toLowerCase().replace(/get_by_alt_text/g, 'getbyalttext').replace(/get_by_test_id/g, 'getbytestid').replace(/get_by_([\w]+)/g, 'getby$1').replace(/has_not_text/g, 'hasnottext').replace(/has_text/g, 'hastext').replace(/has_not/g, 'hasnot').replace(/frame_locator/g, 'framelocator').replace(/content_frame/g, 'contentframe').replace(/[{}\s]/g, '').replace(/new\(\)/g, '').replace(/new[\w]+\.[\w]+options\(\)/g, '').replace(/\.set/g, ',set').replace(/\.or_\(/g, 'or(') // Python has "or_" instead of "or".
|
|
67
67
|
.replace(/\.and_\(/g, 'and(') // Python has "and_" instead of "and".
|
|
68
68
|
.replace(/:/g, '=').replace(/,re\.ignorecase/g, 'i').replace(/,pattern.case_insensitive/g, 'i').replace(/,regexoptions.ignorecase/g, 'i').replace(/re.compile\(([^)]+)\)/g, '$1') // Python has regex strings as r"foo"
|
|
69
69
|
.replace(/pattern.compile\(([^)]+)\)/g, 'r$1').replace(/newregex\(([^)]+)\)/g, 'r$1').replace(/string=/g, '=').replace(/regex=/g, '=').replace(/,,/g, ',');
|
|
@@ -123,7 +123,7 @@ function transform(template, params, testIdAttributeName) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Transform to selector engines.
|
|
126
|
-
template = template.replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase()).replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame').replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2').replace(/locator\(([^)]+),hasnottext=([^),]+)\)/g, 'locator($1).internal:has-not-text=$2').replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2').replace(/locator\(([^)]+)\)/g, '$1').replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1').replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1').replace(/getbylabel\(([^)]+)\)/g, 'internal:label=$1').replace(/getbytestid\(([^)]+)\)/g, `internal:testid=[${testIdAttributeName}=$1]`).replace(/getby(placeholder|alt|title)(?:text)?\(([^)]+)\)/g, 'internal:attr=[$1=$2]').replace(/first(\(\))?/g, 'nth=0').replace(/last(\(\))?/g, 'nth=-1').replace(/nth\(([^)]+)\)/g, 'nth=$1').replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1').replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1').replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1').replace(/filter\(,?hasnot2=([^)]+)\)/g, 'internal:has-not=$1').replace(/,exact=false/g, '').replace(/,exact=true/g, 's').replace(/\,/g, '][');
|
|
126
|
+
template = template.replace(/\,set([\w]+)\(([^)]+)\)/g, (_, group1, group2) => ',' + group1.toLowerCase() + '=' + group2.toLowerCase()).replace(/framelocator\(([^)]+)\)/g, '$1.internal:control=enter-frame').replace(/contentframe(\(\))?/g, 'internal:control=enter-frame').replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2').replace(/locator\(([^)]+),hasnottext=([^),]+)\)/g, 'locator($1).internal:has-not-text=$2').replace(/locator\(([^)]+),hastext=([^),]+)\)/g, 'locator($1).internal:has-text=$2').replace(/locator\(([^)]+)\)/g, '$1').replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1').replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1').replace(/getbylabel\(([^)]+)\)/g, 'internal:label=$1').replace(/getbytestid\(([^)]+)\)/g, `internal:testid=[${testIdAttributeName}=$1]`).replace(/getby(placeholder|alt|title)(?:text)?\(([^)]+)\)/g, 'internal:attr=[$1=$2]').replace(/first(\(\))?/g, 'nth=0').replace(/last(\(\))?/g, 'nth=-1').replace(/nth\(([^)]+)\)/g, 'nth=$1').replace(/filter\(,?hastext=([^)]+)\)/g, 'internal:has-text=$1').replace(/filter\(,?hasnottext=([^)]+)\)/g, 'internal:has-not-text=$1').replace(/filter\(,?has2=([^)]+)\)/g, 'internal:has=$1').replace(/filter\(,?hasnot2=([^)]+)\)/g, 'internal:has-not=$1').replace(/,exact=false/g, '').replace(/,exact=true/g, 's').replace(/\,/g, '][');
|
|
127
127
|
const parts = template.split('.');
|
|
128
128
|
// Turn "internal:control=enter-frame >> nth=0" into "nth=0 >> internal:control=enter-frame"
|
|
129
129
|
// because these are swapped in locators vs selectors.
|