@leera.io/qa-runner 1.0.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/LICENSE +64 -0
- package/README.md +333 -0
- package/dist/src/appium/drivers.js +60 -0
- package/dist/src/appium/home.js +60 -0
- package/dist/src/appium/server.js +237 -0
- package/dist/src/builds/cache.js +133 -0
- package/dist/src/builds/cleanup.js +208 -0
- package/dist/src/builds/download.js +64 -0
- package/dist/src/builds/install.js +75 -0
- package/dist/src/builds/ios-app.js +99 -0
- package/dist/src/builds/unpack.js +347 -0
- package/dist/src/capabilities.js +93 -0
- package/dist/src/ci/builds.js +68 -0
- package/dist/src/ci/client.js +90 -0
- package/dist/src/ci/env.js +118 -0
- package/dist/src/ci/junit.js +79 -0
- package/dist/src/ci/run.js +320 -0
- package/dist/src/ci/summary.js +126 -0
- package/dist/src/cli/commands/builds.js +104 -0
- package/dist/src/cli/commands/ci.js +167 -0
- package/dist/src/cli/commands/config.js +83 -0
- package/dist/src/cli/commands/connect.js +70 -0
- package/dist/src/cli/commands/doctor.js +57 -0
- package/dist/src/cli/commands/service.js +165 -0
- package/dist/src/cli/commands/setup.js +193 -0
- package/dist/src/cli/commands/start.js +94 -0
- package/dist/src/cli/commands/update.js +189 -0
- package/dist/src/cli/commands/version.js +28 -0
- package/dist/src/cli/main.js +59 -0
- package/dist/src/cli/output.js +67 -0
- package/dist/src/cli.js +40 -0
- package/dist/src/client.js +146 -0
- package/dist/src/config.js +260 -0
- package/dist/src/debug.js +137 -0
- package/dist/src/devices/android/adb.js +251 -0
- package/dist/src/devices/android/avd.js +95 -0
- package/dist/src/devices/android/emulator.js +153 -0
- package/dist/src/devices/android/index.js +133 -0
- package/dist/src/devices/android/logcat.js +205 -0
- package/dist/src/devices/android/prepare.js +40 -0
- package/dist/src/devices/android/setup.js +162 -0
- package/dist/src/devices/android/system-dialog.js +78 -0
- package/dist/src/devices/desktop.js +145 -0
- package/dist/src/devices/ios/devicectl.js +87 -0
- package/dist/src/devices/ios/index.js +130 -0
- package/dist/src/devices/ios/prepare.js +39 -0
- package/dist/src/devices/ios/record.js +122 -0
- package/dist/src/devices/ios/settings.js +55 -0
- package/dist/src/devices/ios/setup.js +186 -0
- package/dist/src/devices/ios/simctl.js +201 -0
- package/dist/src/devices/ios/syslog.js +185 -0
- package/dist/src/devices/ios/wda.js +108 -0
- package/dist/src/devices/macos.js +248 -0
- package/dist/src/devices/manager.js +206 -0
- package/dist/src/devices/screen-record.js +95 -0
- package/dist/src/devices/tauri.js +189 -0
- package/dist/src/devices/types.js +6 -0
- package/dist/src/devices/windows.js +362 -0
- package/dist/src/doctor.js +394 -0
- package/dist/src/download.js +39 -0
- package/dist/src/drivers/android-appium.js +525 -0
- package/dist/src/drivers/common.js +64 -0
- package/dist/src/drivers/desktop-appium.js +282 -0
- package/dist/src/drivers/driver.js +15 -0
- package/dist/src/drivers/electron-playwright.js +566 -0
- package/dist/src/drivers/ios-appium.js +535 -0
- package/dist/src/drivers/mac2-appium.js +313 -0
- package/dist/src/drivers/registry.js +47 -0
- package/dist/src/drivers/snapshot/aria.js +351 -0
- package/dist/src/drivers/snapshot/format.js +106 -0
- package/dist/src/drivers/tauri-webdriver.js +678 -0
- package/dist/src/drivers/web-playwright.js +403 -0
- package/dist/src/drivers/webdriver/actions.js +328 -0
- package/dist/src/drivers/webdriver/desktop-keys.js +141 -0
- package/dist/src/drivers/webdriver/dom-locate.js +113 -0
- package/dist/src/drivers/webdriver/dom-snapshot.js +376 -0
- package/dist/src/drivers/webdriver/dom-tree.js +118 -0
- package/dist/src/drivers/webdriver/dom.js +213 -0
- package/dist/src/drivers/webdriver/ios-actions.js +299 -0
- package/dist/src/drivers/webdriver/ios-locate.js +121 -0
- package/dist/src/drivers/webdriver/locate.js +136 -0
- package/dist/src/drivers/webdriver/native-actions.js +175 -0
- package/dist/src/drivers/webdriver/native-locate.js +273 -0
- package/dist/src/drivers/webdriver/session.js +105 -0
- package/dist/src/drivers/webdriver/xml-tree-desktop.js +425 -0
- package/dist/src/drivers/webdriver/xml-tree-ios.js +314 -0
- package/dist/src/drivers/webdriver/xml-tree.js +393 -0
- package/dist/src/drivers/windows-appium.js +319 -0
- package/dist/src/errors.js +21 -0
- package/dist/src/executor.js +189 -0
- package/dist/src/home.js +76 -0
- package/dist/src/index.js +22 -0
- package/dist/src/log.js +69 -0
- package/dist/src/runner.js +498 -0
- package/dist/src/service/index.js +64 -0
- package/dist/src/service/launchd.js +85 -0
- package/dist/src/service/names.js +2 -0
- package/dist/src/service/schtasks.js +128 -0
- package/dist/src/service/systemd.js +65 -0
- package/dist/src/session/commands.js +165 -0
- package/dist/src/session/execute.js +232 -0
- package/dist/src/session/loop.js +148 -0
- package/dist/src/template.js +62 -0
- package/dist/src/types.js +7 -0
- package/dist/src/update/apply.js +223 -0
- package/dist/src/update/check.js +59 -0
- package/dist/src/update/manifest.js +93 -0
- package/dist/src/update/verify.js +65 -0
- package/dist/src/version.js +42 -0
- package/package.json +44 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { clampDepth, clampMaxChars, clipText, renderSnapshotText, SNAPSHOT_LIMITS, } from '../snapshot/format.js';
|
|
2
|
+
import { parsePageSource, suggestionFor } from './xml-tree.js';
|
|
3
|
+
/**
|
|
4
|
+
* XCUITest page source (XML) → snapshot tree with refs.
|
|
5
|
+
*
|
|
6
|
+
* The source is `<AppiumAUT><XCUIElementTypeApplication …>`, one element per accessibility
|
|
7
|
+
* element; attributes are `type`, `name` (the accessibility identifier, or the label when there
|
|
8
|
+
* is none), `label`, `value`, `placeholderValue`, `enabled`, `visible`, `accessible`, `selected`,
|
|
9
|
+
* `x`/`y`/`width`/`height` in points.
|
|
10
|
+
*/
|
|
11
|
+
/** XCUIElementType (without the prefix) → role. Types not listed are `group`. */
|
|
12
|
+
export const IOS_ROLE_BY_TYPE = {
|
|
13
|
+
Button: 'button',
|
|
14
|
+
Link: 'link',
|
|
15
|
+
Icon: 'button',
|
|
16
|
+
MenuButton: 'button',
|
|
17
|
+
PopUpButton: 'button',
|
|
18
|
+
TextField: 'textfield',
|
|
19
|
+
SecureTextField: 'textfield',
|
|
20
|
+
TextView: 'textfield',
|
|
21
|
+
SearchField: 'search',
|
|
22
|
+
StaticText: 'text',
|
|
23
|
+
Image: 'image',
|
|
24
|
+
Cell: 'cell',
|
|
25
|
+
Switch: 'switch',
|
|
26
|
+
Toggle: 'switch',
|
|
27
|
+
CheckBox: 'checkbox',
|
|
28
|
+
RadioButton: 'radio',
|
|
29
|
+
Slider: 'slider',
|
|
30
|
+
Stepper: 'stepper',
|
|
31
|
+
Picker: 'picker',
|
|
32
|
+
PickerWheel: 'picker',
|
|
33
|
+
DatePicker: 'picker',
|
|
34
|
+
SegmentedControl: 'tablist',
|
|
35
|
+
TabBar: 'tablist',
|
|
36
|
+
TabGroup: 'tablist',
|
|
37
|
+
Tab: 'tab',
|
|
38
|
+
NavigationBar: 'toolbar',
|
|
39
|
+
Toolbar: 'toolbar',
|
|
40
|
+
StatusBar: 'statusbar',
|
|
41
|
+
Table: 'list',
|
|
42
|
+
CollectionView: 'list',
|
|
43
|
+
Outline: 'list',
|
|
44
|
+
ScrollView: 'scrollview',
|
|
45
|
+
WebView: 'webview',
|
|
46
|
+
Alert: 'alert',
|
|
47
|
+
Sheet: 'dialog',
|
|
48
|
+
Dialog: 'dialog',
|
|
49
|
+
Keyboard: 'keyboard',
|
|
50
|
+
Key: 'key',
|
|
51
|
+
ActivityIndicator: 'progressbar',
|
|
52
|
+
ProgressIndicator: 'progressbar',
|
|
53
|
+
PageIndicator: 'pager',
|
|
54
|
+
Map: 'map',
|
|
55
|
+
Menu: 'menu',
|
|
56
|
+
MenuItem: 'menuitem',
|
|
57
|
+
Application: 'screen',
|
|
58
|
+
};
|
|
59
|
+
const TYPE_PREFIX = 'XCUIElementType';
|
|
60
|
+
export function iosTypeOf(element) {
|
|
61
|
+
const type = element.attrs.type || element.tag;
|
|
62
|
+
return type.startsWith(TYPE_PREFIX) ? type.slice(TYPE_PREFIX.length) : type;
|
|
63
|
+
}
|
|
64
|
+
export function iosRoleFor(element) {
|
|
65
|
+
return IOS_ROLE_BY_TYPE[iosTypeOf(element)] ?? 'group';
|
|
66
|
+
}
|
|
67
|
+
function num(value) {
|
|
68
|
+
if (value === undefined || value === '')
|
|
69
|
+
return null;
|
|
70
|
+
const n = Number(value);
|
|
71
|
+
return Number.isFinite(n) ? Math.round(n) : null;
|
|
72
|
+
}
|
|
73
|
+
export function iosBounds(element) {
|
|
74
|
+
const x = num(element.attrs.x);
|
|
75
|
+
const y = num(element.attrs.y);
|
|
76
|
+
const width = num(element.attrs.width);
|
|
77
|
+
const height = num(element.attrs.height);
|
|
78
|
+
if (x === null || y === null || width === null || height === null)
|
|
79
|
+
return null;
|
|
80
|
+
return { x, y, width, height };
|
|
81
|
+
}
|
|
82
|
+
function isSecure(element) {
|
|
83
|
+
return iosTypeOf(element) === 'SecureTextField';
|
|
84
|
+
}
|
|
85
|
+
/** The accessibility identifier: `name` when it differs from the label (XCUITest falls back to the label). */
|
|
86
|
+
export function identifierOf(element) {
|
|
87
|
+
const { name, label, value } = element.attrs;
|
|
88
|
+
if (!name)
|
|
89
|
+
return '';
|
|
90
|
+
if (name === label)
|
|
91
|
+
return '';
|
|
92
|
+
// Static texts without a label report their text as the name.
|
|
93
|
+
if (!label && value && name === value)
|
|
94
|
+
return '';
|
|
95
|
+
return name;
|
|
96
|
+
}
|
|
97
|
+
/** What an element shows: its label, else (not for secure fields) its value, else its placeholder. */
|
|
98
|
+
function shownName(element) {
|
|
99
|
+
const a = element.attrs;
|
|
100
|
+
if (a.label)
|
|
101
|
+
return a.label;
|
|
102
|
+
const type = iosTypeOf(element);
|
|
103
|
+
if (!isSecure(element) && a.value && ['StaticText', 'TextField', 'TextView', 'SearchField', 'Link'].includes(type))
|
|
104
|
+
return a.value;
|
|
105
|
+
if (['TextField', 'SecureTextField', 'TextView', 'SearchField'].includes(type) && a.placeholderValue)
|
|
106
|
+
return a.placeholderValue;
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
/** The value worth printing next to the name (not a switch's 0/1, never a secure field's). */
|
|
110
|
+
function shownValue(element, name) {
|
|
111
|
+
const value = element.attrs.value ?? '';
|
|
112
|
+
if (!value || isSecure(element) || value === name)
|
|
113
|
+
return '';
|
|
114
|
+
if (['Switch', 'Toggle', 'CheckBox'].includes(iosTypeOf(element)))
|
|
115
|
+
return '';
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
function onScreen(bounds, screen) {
|
|
119
|
+
if (!bounds)
|
|
120
|
+
return true;
|
|
121
|
+
if (bounds.width <= 0 || bounds.height <= 0)
|
|
122
|
+
return false;
|
|
123
|
+
if (!screen)
|
|
124
|
+
return true;
|
|
125
|
+
return bounds.x < screen.x + screen.width && bounds.x + bounds.width > screen.x && bounds.y < screen.y + screen.height && bounds.y + bounds.height > screen.y;
|
|
126
|
+
}
|
|
127
|
+
function stateOf(element) {
|
|
128
|
+
const a = element.attrs;
|
|
129
|
+
const state = [];
|
|
130
|
+
const type = iosTypeOf(element);
|
|
131
|
+
if (['Switch', 'Toggle', 'CheckBox'].includes(type))
|
|
132
|
+
state.push(a.value === '1' || a.value === 'true' ? 'checked' : 'unchecked');
|
|
133
|
+
if (a.enabled === 'false')
|
|
134
|
+
state.push('disabled');
|
|
135
|
+
if (a.hasFocus === 'true' || a.focused === 'true')
|
|
136
|
+
state.push('focused');
|
|
137
|
+
if (a.selected === 'true')
|
|
138
|
+
state.push('selected');
|
|
139
|
+
if (isSecure(element))
|
|
140
|
+
state.push('password');
|
|
141
|
+
if (a.visible === 'false')
|
|
142
|
+
state.push('hidden');
|
|
143
|
+
return state;
|
|
144
|
+
}
|
|
145
|
+
/** Containers that carry nothing an agent needs: their children take their place. */
|
|
146
|
+
function layoutOnly(element) {
|
|
147
|
+
const role = iosRoleFor(element);
|
|
148
|
+
const a = element.attrs;
|
|
149
|
+
if (role === 'image')
|
|
150
|
+
return !a.label && a.accessible !== 'true';
|
|
151
|
+
// Scroll indicators ("Vertical scroll bar, 2 pages", value "0%") are not part of the app.
|
|
152
|
+
if (role === 'group' && /scroll bar/i.test(a.label ?? '') && /^\d+%$/.test(a.value ?? ''))
|
|
153
|
+
return true;
|
|
154
|
+
const hollow = !a.label && !a.name && !a.value && a.accessible !== 'true';
|
|
155
|
+
if (role === 'group' || role === 'cell')
|
|
156
|
+
return hollow;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function add(map, key, element) {
|
|
160
|
+
if (!key)
|
|
161
|
+
return;
|
|
162
|
+
const list = map.get(key);
|
|
163
|
+
if (list) {
|
|
164
|
+
if (list.at(-1) !== element)
|
|
165
|
+
list.push(element);
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
map.set(key, [element]);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* XCUITest queries return hidden elements too, so every element counts for `nth`; the
|
|
172
|
+
* application itself is not one of its descendants and never matches.
|
|
173
|
+
*/
|
|
174
|
+
function index(all) {
|
|
175
|
+
const counts = { name: new Map(), label: new Map(), text: new Map(), type: new Map() };
|
|
176
|
+
for (const element of all) {
|
|
177
|
+
if (iosTypeOf(element) === 'Application')
|
|
178
|
+
continue;
|
|
179
|
+
const a = element.attrs;
|
|
180
|
+
add(counts.name, a.name, element);
|
|
181
|
+
add(counts.label, a.label, element);
|
|
182
|
+
// `text` matches label or value.
|
|
183
|
+
add(counts.text, a.label, element);
|
|
184
|
+
if (!isSecure(element))
|
|
185
|
+
add(counts.text, a.value, element);
|
|
186
|
+
add(counts.type, a.type || element.tag, element);
|
|
187
|
+
}
|
|
188
|
+
return counts;
|
|
189
|
+
}
|
|
190
|
+
function withNth(target, list, element) {
|
|
191
|
+
if (!list || list.length <= 1)
|
|
192
|
+
return target;
|
|
193
|
+
return { ...target, nth: list.indexOf(element) };
|
|
194
|
+
}
|
|
195
|
+
/** `/AppiumAUT/XCUIElementTypeApplication[1]/…` → `//XCUIElementTypeApplication/…` (WDA's XML has no AppiumAUT root). */
|
|
196
|
+
export function wdaXpath(xpath) {
|
|
197
|
+
return xpath.replace(/^\/AppiumAUT\/XCUIElementTypeApplication\[1\]/, '//XCUIElementTypeApplication');
|
|
198
|
+
}
|
|
199
|
+
/** Locator candidates, best first: id > label > text > class chain (fragile) > xpath (fragile). */
|
|
200
|
+
export function iosCandidatesFor(element, counts) {
|
|
201
|
+
const out = [];
|
|
202
|
+
const a = element.attrs;
|
|
203
|
+
const id = identifierOf(element);
|
|
204
|
+
if (id && id.length <= 2000) {
|
|
205
|
+
const list = counts.name.get(id);
|
|
206
|
+
out.push({ target: withNth({ id }, list, element), fragile: (list?.length ?? 0) > 1 });
|
|
207
|
+
}
|
|
208
|
+
if (a.label && a.label.length <= 2000) {
|
|
209
|
+
const list = counts.label.get(a.label);
|
|
210
|
+
out.push({ target: withNth({ label: a.label }, list, element), fragile: (list?.length ?? 0) > 1 });
|
|
211
|
+
}
|
|
212
|
+
const value = a.value;
|
|
213
|
+
if (value && value !== a.label && !isSecure(element) && value.length <= 2000 && !['Switch', 'Toggle', 'CheckBox'].includes(iosTypeOf(element))) {
|
|
214
|
+
const list = counts.text.get(value);
|
|
215
|
+
out.push({ target: withNth({ text: value, exact: true }, list, element), fragile: (list?.length ?? 0) > 1 });
|
|
216
|
+
}
|
|
217
|
+
const type = a.type || element.tag;
|
|
218
|
+
const position = counts.type.get(type)?.indexOf(element) ?? -1;
|
|
219
|
+
if (position >= 0) {
|
|
220
|
+
// Class chain indexes are 1-based over every element of the type in document order.
|
|
221
|
+
out.push({ target: { class_chain: `**/${type}[${position + 1}]` }, fragile: true });
|
|
222
|
+
}
|
|
223
|
+
const xpath = wdaXpath(element.xpath);
|
|
224
|
+
if (xpath.length <= 500)
|
|
225
|
+
out.push({ target: { xpath }, fragile: true });
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/** Builds the snapshot tree, text and refs from XCUITest page source XML. */
|
|
229
|
+
export function iosSnapshotFromXml(xml, options = {}) {
|
|
230
|
+
const tree = parsePageSource(xml);
|
|
231
|
+
const app = tree.all.find((element) => iosTypeOf(element) === 'Application') ?? null;
|
|
232
|
+
const screenBounds = app ? iosBounds(app) : null;
|
|
233
|
+
const counts = index(tree.all);
|
|
234
|
+
const depthLimit = clampDepth(options.depth);
|
|
235
|
+
const maxNodes = Math.min(options.maxNodes ?? SNAPSHOT_LIMITS.nodes, SNAPSHOT_LIMITS.nodes);
|
|
236
|
+
let next = options.firstRef ?? 1;
|
|
237
|
+
let emitted = 0;
|
|
238
|
+
let truncated = false;
|
|
239
|
+
const refs = {};
|
|
240
|
+
const bundleId = app?.attrs.bundleId || null;
|
|
241
|
+
const appName = app?.attrs.label || app?.attrs.name || null;
|
|
242
|
+
const root = { ref: 'e0', role: 'screen', ...(appName || bundleId ? { name: (appName ?? bundleId) } : {}) };
|
|
243
|
+
if (screenBounds)
|
|
244
|
+
root.bounds = { x: 0, y: 0, width: screenBounds.width, height: screenBounds.height };
|
|
245
|
+
/** `context` = the nearest emitted ancestor's name and role, to skip texts that repeat it. */
|
|
246
|
+
const convert = (element, depth, parent, context) => {
|
|
247
|
+
const a = element.attrs;
|
|
248
|
+
const bounds = iosBounds(element);
|
|
249
|
+
const role = iosRoleFor(element);
|
|
250
|
+
const name = shownName(element);
|
|
251
|
+
const hidden = a.visible === 'false' || !onScreen(bounds, screenBounds);
|
|
252
|
+
const id = identifierOf(element);
|
|
253
|
+
// A button or text inside a cell or button that says the same thing (same name, no other id) adds nothing.
|
|
254
|
+
const redundant = (role === 'text' || role === 'button') && name !== '' && name === context.name && (!id || id === context.id);
|
|
255
|
+
const decorative = role === 'image' && !a.label && context.interactive;
|
|
256
|
+
const skip = role === 'screen' || layoutOnly(element) || redundant || decorative || (hidden && !options.includeHidden);
|
|
257
|
+
if (skip) {
|
|
258
|
+
for (const child of element.children)
|
|
259
|
+
convert(child, depth, parent, context);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (emitted >= maxNodes || depth > depthLimit) {
|
|
263
|
+
truncated = true;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
emitted += 1;
|
|
267
|
+
const ref = `e${next}`;
|
|
268
|
+
next += 1;
|
|
269
|
+
const node = { ref, role };
|
|
270
|
+
if (name)
|
|
271
|
+
node.name = clipText(name);
|
|
272
|
+
const value = shownValue(element, name);
|
|
273
|
+
if (value)
|
|
274
|
+
node.text = clipText(value);
|
|
275
|
+
if (id)
|
|
276
|
+
node.ids = { accessibility_id: id };
|
|
277
|
+
if (bounds)
|
|
278
|
+
node.bounds = bounds;
|
|
279
|
+
const state = stateOf(element);
|
|
280
|
+
if (state.length > 0)
|
|
281
|
+
node.state = state;
|
|
282
|
+
const { suggest, alternatives, fragile } = suggestionFor(iosCandidatesFor(element, counts));
|
|
283
|
+
refs[ref] = {
|
|
284
|
+
locate: { xpath: wdaXpath(element.xpath), ...(bounds ? { bounds } : {}), type: a.type || element.tag },
|
|
285
|
+
suggest,
|
|
286
|
+
alternatives,
|
|
287
|
+
fragile,
|
|
288
|
+
bounds: bounds ?? null,
|
|
289
|
+
};
|
|
290
|
+
(parent.children ??= []).push(node);
|
|
291
|
+
const interactive = context.interactive || ['button', 'cell', 'link', 'switch', 'checkbox', 'radio', 'tab'].includes(role);
|
|
292
|
+
for (const child of element.children)
|
|
293
|
+
convert(child, depth + 1, node, { name: name || context.name, id: id || context.id, interactive });
|
|
294
|
+
};
|
|
295
|
+
for (const child of tree.root.children)
|
|
296
|
+
convert(child, 1, root, { name: '', id: '', interactive: false });
|
|
297
|
+
const rendered = renderSnapshotText(root, clampMaxChars(options.maxChars));
|
|
298
|
+
for (const ref of Object.keys(refs)) {
|
|
299
|
+
if (!rendered.refs.has(ref))
|
|
300
|
+
delete refs[ref];
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
root,
|
|
304
|
+
text: rendered.text,
|
|
305
|
+
refs,
|
|
306
|
+
element_count: emitted,
|
|
307
|
+
truncated: truncated || rendered.truncated,
|
|
308
|
+
width: screenBounds?.width ?? null,
|
|
309
|
+
height: screenBounds?.height ?? null,
|
|
310
|
+
rotation: null,
|
|
311
|
+
bundleId,
|
|
312
|
+
appName,
|
|
313
|
+
};
|
|
314
|
+
}
|