@applitools/core 4.69.2 → 4.70.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.70.0](https://github.com/Applitools-Dev/sdk/compare/js/core@4.69.2...js/core@4.70.0) (2026-09-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * synthesize a DOM from the accessibility hierarchy for iOS checks without NML | AD-14691 ([#4112](https://github.com/Applitools-Dev/sdk/issues/4112)) ([ee6e04e](https://github.com/Applitools-Dev/sdk/commit/ee6e04e7b46ce8d530202b9bd40cdce255a74728))
9
+
3
10
  ## [4.69.2](https://github.com/Applitools-Dev/sdk/compare/js/core@4.69.1...js/core@4.69.2) (2026-08-30)
4
11
 
5
12
 
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseIosPageSource = void 0;
4
+ /**
5
+ * Parses an iOS Appium `getPageSource` document (WebDriverAgent XML) into the
6
+ * `NativeElementNode` tree consumed by `synthesize-native-dom`. Kept separate
7
+ * from the synthesizer so the element-type -> HTML mapping stays platform-neutral
8
+ * and the (platform-specific) source shape lives here.
9
+ *
10
+ * WDA emits every element as `<XCUIElementType… type="XCUIElementType…"
11
+ * label="…" x=".." y=".." width=".." height=".." …>`; `preserveOrder` keeps the
12
+ * original sibling order, which the dashboard hover/layout relies on.
13
+ */
14
+ const fast_xml_parser_1 = require("fast-xml-parser");
15
+ const ATTRS = ':@';
16
+ const ATTR_PREFIX = '@_';
17
+ const parser = new fast_xml_parser_1.XMLParser({
18
+ ignoreAttributes: false,
19
+ preserveOrder: true,
20
+ parseAttributeValue: false,
21
+ // Decode decimal char refs too (`&#10;` for newlines in multi-line labels) — the
22
+ // default only handles named/hex. Keep label whitespace intact (`trimValues:false`)
23
+ // so labels match the on-device value; inter-element whitespace becomes stray #text
24
+ // nodes that `toNativeNode` drops (they aren't XCUIElementType tags).
25
+ htmlEntities: true,
26
+ trimValues: false,
27
+ });
28
+ function parseIosPageSource(xml) {
29
+ var _a;
30
+ // WDA truncates the page source on deep hierarchies. `parser.parse` accepts a
31
+ // truncated document (unclosed tags) and returns a partial tree instead of
32
+ // throwing, so validate first — otherwise we'd upload a partial DOM as complete.
33
+ if (fast_xml_parser_1.XMLValidator.validate(xml) !== true)
34
+ return undefined;
35
+ const top = parser.parse(xml);
36
+ const aut = top.find(node => Array.isArray(node.AppiumAUT));
37
+ const children = (_a = aut === null || aut === void 0 ? void 0 : aut.AppiumAUT) !== null && _a !== void 0 ? _a : top;
38
+ // The root is the first real XCUIElement under <AppiumAUT> (the application).
39
+ const rootNode = children.find(node => elementTag(node));
40
+ return rootNode && toNativeNode(rootNode);
41
+ }
42
+ exports.parseIosPageSource = parseIosPageSource;
43
+ function toNativeNode(node) {
44
+ var _a, _b, _c;
45
+ const tag = elementTag(node);
46
+ if (!tag)
47
+ return undefined;
48
+ const attrs = (_a = node[ATTRS]) !== null && _a !== void 0 ? _a : {};
49
+ const type = (_b = attrs[`${ATTR_PREFIX}type`]) !== null && _b !== void 0 ? _b : tag;
50
+ const label = attrs[`${ATTR_PREFIX}label`];
51
+ const children = (_c = node[tag]) !== null && _c !== void 0 ? _c : [];
52
+ return {
53
+ elementType: caseName(type),
54
+ frame: toFrame(attrs),
55
+ label: label != null && label !== '' ? String(label) : undefined,
56
+ children: children.map(toNativeNode).filter(Boolean),
57
+ };
58
+ }
59
+ /** The single `XCUIElementType…` key on a parsed node (ignoring the `:@` attrs bag). */
60
+ function elementTag(node) {
61
+ return Object.keys(node).find(key => key.startsWith('XCUIElementType'));
62
+ }
63
+ /** 'XCUIElementTypeStaticText' -> 'staticText' — the case-name form HTMLNode switches on. */
64
+ function caseName(type) {
65
+ const name = type.replace(/^XCUIElementType/, '');
66
+ return name.charAt(0).toLowerCase() + name.slice(1);
67
+ }
68
+ function toFrame(attrs) {
69
+ const x = Number(attrs[`${ATTR_PREFIX}x`]);
70
+ const y = Number(attrs[`${ATTR_PREFIX}y`]);
71
+ const width = Number(attrs[`${ATTR_PREFIX}width`]);
72
+ const height = Number(attrs[`${ATTR_PREFIX}height`]);
73
+ if ([x, y, width, height].some(Number.isNaN))
74
+ return undefined;
75
+ return { x, y, width, height };
76
+ }
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ /**
3
+ * Synthesizes an Applitools DOM-capture document from a native accessibility
4
+ * tree — a port of the iOS SDK's `HTMLNode.swift` mapping that must stay in sync
5
+ * with it. See core/llm-docs/CLASSIC.md ("iOS DOM Synthesis").
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.synthesizeNativeDom = void 0;
9
+ /** elementType -> tag + static attributes (mirrors the switch in HTMLNode.swift:205-434). */
10
+ const TYPE_MAP = {
11
+ any: { tag: 'DIV' },
12
+ other: { tag: 'DIV' },
13
+ application: { tag: 'DIV' },
14
+ group: { tag: 'DIV', attrs: { role: 'group' } },
15
+ window: { tag: 'DIV', attrs: { role: 'window' } },
16
+ sheet: { tag: 'DIV', attrs: { role: 'dialog' } },
17
+ drawer: { tag: 'DIV', attrs: { role: 'complementary' } },
18
+ alert: { tag: 'DIV', attrs: { role: 'alert' } },
19
+ dialog: { tag: 'DIV', attrs: { role: 'dialog' } },
20
+ button: { tag: 'BUTTON' },
21
+ radioButton: { tag: 'INPUT', attrs: { type: 'radio' } },
22
+ radioGroup: { tag: 'DIV', attrs: { role: 'radiogroup' } },
23
+ checkBox: { tag: 'INPUT', attrs: { type: 'checkbox' } },
24
+ disclosureTriangle: { tag: 'BUTTON', attrs: { 'aria-expanded': 'false' } },
25
+ popUpButton: { tag: 'DIV', attrs: { role: 'listbox' } },
26
+ comboBox: { tag: 'INPUT', attrs: { role: 'combobox' } },
27
+ menuButton: { tag: 'BUTTON', attrs: { 'aria-haspopup': 'true' } },
28
+ toolbarButton: { tag: 'BUTTON' },
29
+ popover: { tag: 'DIV', attrs: { role: 'dialog' } },
30
+ keyboard: { tag: 'DIV', attrs: { class: 'keyboard' } },
31
+ key: { tag: 'KBD', attrs: { class: 'key' } },
32
+ navigationBar: { tag: 'NAV', attrs: { role: 'navigation' } },
33
+ tabBar: { tag: 'DIV', attrs: { role: 'tablist' } },
34
+ tabGroup: { tag: 'DIV', attrs: { role: 'tablist' } },
35
+ toolbar: { tag: 'DIV', attrs: { role: 'toolbar' } },
36
+ statusBar: { tag: 'DIV', attrs: { role: 'status' } },
37
+ table: { tag: 'UL' },
38
+ collectionView: { tag: 'UL' },
39
+ tableRow: { tag: 'DIV', attrs: { role: 'row' } },
40
+ tableColumn: { tag: 'DIV', attrs: { role: 'column' } },
41
+ outline: { tag: 'UL', attrs: { role: 'tree' } },
42
+ outlineRow: { tag: 'LI', attrs: { role: 'treeitem' } },
43
+ browser: { tag: 'CANVAS' },
44
+ map: { tag: 'CANVAS' },
45
+ webView: { tag: 'CANVAS' },
46
+ slider: { tag: 'INPUT', attrs: { type: 'range' } },
47
+ pageIndicator: { tag: 'DIV', attrs: { class: 'page-indicator' } },
48
+ progressIndicator: { tag: 'DIV', attrs: { role: 'progressbar' } },
49
+ activityIndicator: { tag: 'DIV', attrs: { role: 'status' } },
50
+ segmentedControl: { tag: 'DIV', attrs: { role: 'group' } },
51
+ picker: { tag: 'DIV', attrs: { role: 'listbox' } },
52
+ pickerWheel: { tag: 'DIV', attrs: { role: 'listbox' } },
53
+ switch: { tag: 'INPUT', attrs: { type: 'checkbox', role: 'switch' } },
54
+ toggle: { tag: 'BUTTON', attrs: { role: 'switch', 'aria-pressed': 'false' } },
55
+ link: { tag: 'A', attrs: { role: 'link' } },
56
+ image: { tag: 'IMG' },
57
+ icon: { tag: 'DIV', attrs: { class: 'icon' } },
58
+ searchField: { tag: 'INPUT', attrs: { type: 'search' } },
59
+ scrollView: { tag: 'DIV', attrs: { style: 'overflow:auto;' } },
60
+ scrollBar: { tag: 'DIV', attrs: { class: 'scrollbar' } },
61
+ staticText: { tag: 'SPAN' },
62
+ // textField / secureTextField also set value=label||'' (handled below).
63
+ textField: { tag: 'INPUT', attrs: { type: 'text' } },
64
+ secureTextField: { tag: 'INPUT', attrs: { type: 'password' } },
65
+ datePicker: { tag: 'INPUT', attrs: { type: 'date' } },
66
+ textView: { tag: 'TEXTAREA' },
67
+ menu: { tag: 'UL', attrs: { role: 'menu' } },
68
+ menuItem: { tag: 'LI', attrs: { role: 'menuitem' } },
69
+ menuBar: { tag: 'DIV', attrs: { role: 'menubar' } },
70
+ menuBarItem: { tag: 'DIV', attrs: { role: 'menuitem' } },
71
+ incrementArrow: { tag: 'BUTTON', attrs: { 'aria-label': 'increment' } },
72
+ decrementArrow: { tag: 'BUTTON', attrs: { 'aria-label': 'decrement' } },
73
+ timeline: { tag: 'DIV', attrs: { class: 'timeline' } },
74
+ ratingIndicator: { tag: 'DIV', attrs: { role: 'meter' } },
75
+ valueIndicator: { tag: 'DIV', attrs: { role: 'output' } },
76
+ splitGroup: { tag: 'DIV', attrs: { class: 'split-group' } },
77
+ splitter: { tag: 'DIV', attrs: { role: 'separator' } },
78
+ relevanceIndicator: { tag: 'DIV', attrs: { 'aria-hidden': 'true' } },
79
+ colorWell: { tag: 'INPUT', attrs: { type: 'color' } },
80
+ helpTag: { tag: 'DIV', attrs: { class: 'help-tag' } },
81
+ matte: { tag: 'DIV', attrs: { class: 'matte' } },
82
+ dockItem: { tag: 'LI', attrs: { role: 'menuitem' } },
83
+ ruler: { tag: 'DIV', attrs: { class: 'ruler' } },
84
+ rulerMarker: { tag: 'DIV', attrs: { class: 'ruler-marker' } },
85
+ grid: { tag: 'DIV', attrs: { role: 'grid' } },
86
+ levelIndicator: { tag: 'DIV', attrs: { role: 'meter' } },
87
+ cell: { tag: 'LI' },
88
+ layoutArea: { tag: 'DIV', attrs: { class: 'layout-area' } },
89
+ layoutItem: { tag: 'DIV', attrs: { class: 'layout-item' } },
90
+ handle: { tag: 'DIV', attrs: { class: 'handle' } },
91
+ stepper: { tag: 'INPUT', attrs: { type: 'number', role: 'spinbutton' } },
92
+ tab: { tag: 'BUTTON', attrs: { role: 'tab' } },
93
+ touchBar: { tag: 'DIV', attrs: { class: 'touchbar' } },
94
+ statusItem: { tag: 'DIV', attrs: { role: 'status' } },
95
+ };
96
+ function synthesizeNativeDom(root, { source, version, metadata } = {}) {
97
+ return serialize(build(root, true, { source, version, metadata }));
98
+ }
99
+ exports.synthesizeNativeDom = synthesizeNativeDom;
100
+ function build(node, isRoot, root) {
101
+ var _a, _b, _c, _d;
102
+ const attributes = { ...((_a = node.attributes) !== null && _a !== void 0 ? _a : {}) };
103
+ attributes['elementTypeName'] = node.elementType;
104
+ const rect = node.frame
105
+ ? { width: node.frame.width, height: node.frame.height, top: node.frame.y, left: node.frame.x }
106
+ : undefined;
107
+ const childNodes = ((_b = node.children) !== null && _b !== void 0 ? _b : []).map(child => build(child, false, root));
108
+ const mapping = (_c = TYPE_MAP[node.elementType]) !== null && _c !== void 0 ? _c : { tag: 'DIV' };
109
+ const current = { tagName: mapping.tag, attributes, childNodes, rect };
110
+ if (mapping.attrs)
111
+ Object.assign(current.attributes, mapping.attrs);
112
+ if (node.elementType === 'textField' || node.elementType === 'secureTextField') {
113
+ current.attributes['value'] = (_d = node.label) !== null && _d !== void 0 ? _d : '';
114
+ }
115
+ if (isRoot) {
116
+ // Wrap: the decoded root element becomes BODY (a value copy — clone attributes so
117
+ // a later apply() on the HTML node can't leak into BODY), and a fresh HTML node
118
+ // carries it plus source/version/metadata. Mirrors HTMLNode.swift:436-448.
119
+ const bodyNode = { ...current, attributes: { ...current.attributes } };
120
+ bodyNode.tagName = 'BODY';
121
+ current.childNodes = [bodyNode];
122
+ current.tagName = 'HTML';
123
+ if (root.source !== undefined)
124
+ current.source = root.source;
125
+ if (root.version !== undefined)
126
+ current.version = root.version;
127
+ if (root.metadata !== undefined)
128
+ current.metadata = root.metadata;
129
+ }
130
+ if (node.label)
131
+ apply(current, node.label);
132
+ return current;
133
+ }
134
+ /** Mirrors HTMLNode.apply(text:) — HTMLNode.swift:455-491. */
135
+ function apply(node, text) {
136
+ if (!text)
137
+ return;
138
+ switch (node.tagName) {
139
+ case 'INPUT':
140
+ node.attributes['value'] = text;
141
+ break;
142
+ case 'A':
143
+ if (node.childNodes.length === 0)
144
+ node.childNodes.push(textNode(text, node.rect));
145
+ else
146
+ node.attributes['title'] = text;
147
+ break;
148
+ case '#text':
149
+ node.text = text;
150
+ break;
151
+ case 'HTML':
152
+ node.attributes['title'] = text;
153
+ break;
154
+ case 'BUTTON':
155
+ // Swift only appends when empty; a labelled button with children keeps its children.
156
+ if (node.childNodes.length === 0)
157
+ node.childNodes.push(textNode(text, node.rect));
158
+ break;
159
+ case 'IMG':
160
+ node.attributes['src'] = text;
161
+ break;
162
+ default:
163
+ // A container with children shouldn't also emit a #text child over its full rect
164
+ // (it would confuse the Eyes UI hover/select) — set a title instead.
165
+ if (node.childNodes.length === 0)
166
+ node.childNodes.push(textNode(text, node.rect));
167
+ else
168
+ node.attributes['title'] = text;
169
+ }
170
+ }
171
+ function textNode(text, rect) {
172
+ return { tagName: '#text', attributes: {}, childNodes: [], rect, text };
173
+ }
174
+ function serialize(node) {
175
+ const out = { tagName: node.tagName, childNodes: [] };
176
+ if (Object.keys(node.attributes).length > 0)
177
+ out.attributes = node.attributes;
178
+ out.childNodes = node.childNodes.map(serialize);
179
+ if (node.rect) {
180
+ // The diff/ignore algo expects #text nodes to carry a `rects` array; all other
181
+ // tags keep a single `rect` (HTMLNode.swift:63-85).
182
+ if (node.tagName === '#text')
183
+ out.rects = [node.rect];
184
+ else
185
+ out.rect = node.rect;
186
+ }
187
+ if (node.text !== undefined)
188
+ out.text = node.text;
189
+ if (node.version !== undefined)
190
+ out.version = node.version;
191
+ if (node.source !== undefined)
192
+ out.source = node.source;
193
+ if (node.metadata !== undefined)
194
+ out.metadata = node.metadata;
195
+ return out;
196
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.takeIosDomCapture = void 0;
4
+ const parse_ios_page_source_1 = require("./parse-ios-page-source");
5
+ const synthesize_native_dom_1 = require("./synthesize-native-dom");
6
+ /**
7
+ * iOS no-NML counterpart of `takeDomCapture`: reads the accessibility hierarchy
8
+ * through the driver and synthesizes an Applitools DOM document from it (see
9
+ * docs/native-dom-without-nml-design.md). Returns undefined — an image-only
10
+ * check — when the driver has no page source or synthesis fails; a DOM problem
11
+ * must never fail the check itself.
12
+ */
13
+ async function takeIosDomCapture({ driver, logger, }) {
14
+ try {
15
+ const pageSource = await driver.getPageSource();
16
+ if (!pageSource)
17
+ return undefined;
18
+ const root = (0, parse_ios_page_source_1.parseIosPageSource)(pageSource);
19
+ if (!root)
20
+ return undefined;
21
+ return JSON.stringify((0, synthesize_native_dom_1.synthesizeNativeDom)(root, { source: 'mobile-ios-appium' }));
22
+ }
23
+ catch (error) {
24
+ logger.error('failed to synthesize DOM from the iOS page source', error);
25
+ return undefined;
26
+ }
27
+ }
28
+ exports.takeIosDomCapture = takeIosDomCapture;
@@ -25,10 +25,11 @@ var __importStar = (this && this.__importStar) || function (mod) {
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.takeScreenshots = void 0;
27
27
  const take_dom_capture_1 = require("./take-dom-capture");
28
+ const take_ios_dom_capture_1 = require("./take-ios-dom-capture");
28
29
  const take_screenshot_1 = require("../../automation/utils/take-screenshot");
29
30
  const utils = __importStar(require("@applitools/utils"));
30
31
  async function takeScreenshots({ driver, settings, logger, }) {
31
- var _a, _b;
32
+ var _a, _b, _c, _d;
32
33
  const screenshot = await logger.withTime((0, take_screenshot_1.takeScreenshot)({ driver, settings, logger }), 'takeScreenshot');
33
34
  const name = await logger.withTime(driver.getTitle(), 'getTitle');
34
35
  const source = await logger.withTime(driver.getUrl(), 'getUrl');
@@ -52,6 +53,27 @@ async function takeScreenshots({ driver, settings, logger, }) {
52
53
  return undefined;
53
54
  }), 'takeDomCapture');
54
55
  }
56
+ else if (environment.isNative &&
57
+ environment.isIOS &&
58
+ !((_c = environment.applitoolsLib) === null || _c === void 0 ? void 0 : _c.instrumented) &&
59
+ !settings.webview &&
60
+ settings.domSettings) {
61
+ // No-NML iOS app: synthesize a DOM from the accessibility hierarchy so the
62
+ // check still gets layout matching / dashboard hover / RCA. Android pending.
63
+ const dom = await logger.withTime((0, take_ios_dom_capture_1.takeIosDomCapture)({ driver, logger }), 'takeIosDomCapture');
64
+ if (dom) {
65
+ baseTarget.dom = dom;
66
+ // The native screenshot is cropped below the status bar while the page-source
67
+ // DOM stays in full-screen coordinates. Shift the reported location down by the
68
+ // band the screenshoter actually cropped, so the server seats the DOM correctly
69
+ // (same fix as the iOS SDK's classic no-NML path). Using the screenshoter's own
70
+ // reported offset keeps this consistent with the pixels for viewport, region and
71
+ // framed captures alike. Rounded — the server rejects fractional locations.
72
+ if (screenshot.statusBarOffset) {
73
+ baseTarget.locationInViewport = utils.geometry.round(utils.geometry.offset((_d = baseTarget.locationInViewport) !== null && _d !== void 0 ? _d : { x: 0, y: 0 }, { x: 0, y: screenshot.statusBarOffset }));
74
+ }
75
+ }
76
+ }
55
77
  if (settings.calculateView) {
56
78
  const scrollingElement = await driver.mainContext.getScrollingElement();
57
79
  const scrollingOffset = !scrollingElement || environment.isNative ? { x: 0, y: 0 } : await scrollingElement.getScrollOffset();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/core",
3
- "version": "4.69.2",
3
+ "version": "4.70.0",
4
4
  "homepage": "https://applitools.com",
5
5
  "bugs": {
6
6
  "url": "https://github.com/applitools/eyes.sdk.javascript1/issues"
@@ -48,17 +48,19 @@
48
48
  "build": "yarn build:dist",
49
49
  "build:dist": "run --top-level tsc --project ./tsconfig.build.json",
50
50
  "test": "run --top-level mocha './test/**/*.spec.ts' --exclude 'test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
51
- "test:local": "MOCHA_OMIT_TAGS=sauce,browserstack run test",
51
+ "test:local": "MOCHA_OMIT_TAGS=sauce,browserstack,ios-simulator run test",
52
52
  "test:sauce": "MOCHA_ONLY_TAGS=sauce,browserstack run test",
53
53
  "test:bin": "MOCHA_GROUP=bin run --top-level mocha './test/bin/**/*.spec.ts' --parallel --jobs ${MOCHA_JOBS:-15} --require ./test/mocha-global-setup.js",
54
- "test:e2e": "MOCHA_OMIT_TAGS=http2 MOCHA_GROUP=e2e run --top-level mocha './test/e2e/**/*.spec.ts' --exclude 'test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
55
- "test:e2e:http2": "APPLITOOLS_HTTP_VERSION=2 MOCHA_OMIT_TAGS=http1,sauce,browserstack MOCHA_GROUP=e2e run --top-level mocha './test/e2e/**/*.spec.ts' --exclude 'test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
54
+ "test:e2e": "MOCHA_OMIT_TAGS=http2,ios-simulator MOCHA_GROUP=e2e run --top-level mocha './test/e2e/**/*.spec.ts' --exclude 'test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
55
+ "test:e2e:http2": "APPLITOOLS_HTTP_VERSION=2 MOCHA_OMIT_TAGS=http1,sauce,browserstack,ios-simulator MOCHA_GROUP=e2e run --top-level mocha './test/e2e/**/*.spec.ts' --exclude 'test/e2e/mocha-sync/**' --parallel --jobs ${MOCHA_JOBS:-15} --exit --require ./test/mocha-global-setup.js",
56
56
  "test:it": "MOCHA_GROUP=it run --top-level mocha './test/it/**/*.spec.ts' --require ./test/mocha-global-setup.js",
57
57
  "test:e2e:sync": "MOCHA_GROUP=e2e SYNC=true run --top-level mocha './test/e2e/mocha-sync/**/*.spec.ts' --exit --require ./test/mocha-global-setup.js",
58
58
  "test:unit": "MOCHA_GROUP=unit run --top-level mocha './test/unit/**/*.spec.ts' --require ./test/mocha-global-setup.js",
59
59
  "setup": "run --top-level browsers:setup && run --top-level xvfb:setup && run playwright:setup",
60
60
  "playwright:setup": "node -e \"var v=parseInt(process.version.slice(1));if(v>=18){require('child_process').execSync('yarn playwright install --with-deps firefox',{stdio:'inherit'})}else{console.log('Skipping playwright install (Node '+process.version+')')}\"",
61
- "setup:standalone": "sh -c 'yarn chromedriver --port=4444 --verbose &'"
61
+ "setup:standalone": "sh -c 'yarn chromedriver --port=4444 --verbose &'",
62
+ "setup:ios-simulator": "npx --yes appium@3 driver install xcuitest && cd ../../../test/apps/ios/IOSTestApp && xcodebuild -project IOSTestApp.xcodeproj -target IOSTestApp -configuration Release -sdk iphonesimulator CODE_SIGNING_ALLOWED=NO SYMROOT=build build",
63
+ "test:ios-simulator": "MOCHA_ONLY_TAGS=ios-simulator MOCHA_GROUP=ios-simulator run --top-level mocha ./test/e2e/classic/webview.spec.ts --exit --require ./test/mocha-global-setup.js"
62
64
  },
63
65
  "dependencies": {
64
66
  "@applitools/core-base": "1.38.2",
@@ -77,6 +79,7 @@
77
79
  "@applitools/utils": "1.15.2",
78
80
  "abort-controller": "3.0.0",
79
81
  "chalk": "4.1.2",
82
+ "fast-xml-parser": "5.11.1",
80
83
  "node-fetch": "2.6.7",
81
84
  "semver": "7.6.2",
82
85
  "shell-quote": "^1.8.4",
@@ -13,6 +13,8 @@ export type Screenshot<TSpec extends SpecType> = {
13
13
  selector?: Selector;
14
14
  regions: Region[];
15
15
  }[];
16
+ /** Points the native status bar was cropped from the image top (0 if kept or on web). */
17
+ statusBarOffset: number;
16
18
  };
17
19
  export declare function takeScreenshot<TSpec extends SpecType>({ driver, settings, logger, }: {
18
20
  driver: Driver<TSpec>;
@@ -0,0 +1,2 @@
1
+ import type { NativeElementNode } from './synthesize-native-dom';
2
+ export declare function parseIosPageSource(xml: string): NativeElementNode | undefined;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Synthesizes an Applitools DOM-capture document from a native accessibility
3
+ * tree — a port of the iOS SDK's `HTMLNode.swift` mapping that must stay in sync
4
+ * with it. See core/llm-docs/CLASSIC.md ("iOS DOM Synthesis").
5
+ */
6
+ export type NativeElementNode = {
7
+ /** XCUIElementType case-name form, e.g. 'button', 'staticText', 'application'. */
8
+ elementType: string;
9
+ /** Element frame in points; origin is top-left. */
10
+ frame?: {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ };
16
+ /** Accessibility label (iOS gives '' for unlabeled; the parser should map ''->undefined). */
17
+ label?: string;
18
+ /** Any pre-existing attributes (usually none from the Appium path). */
19
+ attributes?: Record<string, string>;
20
+ children?: NativeElementNode[];
21
+ };
22
+ type Rect = {
23
+ width: number;
24
+ height: number;
25
+ top: number;
26
+ left: number;
27
+ };
28
+ export type SyntheticDomNode = {
29
+ tagName: string;
30
+ attributes?: Record<string, string>;
31
+ childNodes: SyntheticDomNode[];
32
+ rect?: Rect;
33
+ rects?: Rect[];
34
+ text?: string;
35
+ version?: string;
36
+ source?: string;
37
+ metadata?: unknown;
38
+ };
39
+ export declare function synthesizeNativeDom(root: NativeElementNode, { source, version, metadata }?: {
40
+ source?: string;
41
+ version?: string;
42
+ metadata?: unknown;
43
+ }): SyntheticDomNode;
44
+ export {};
@@ -0,0 +1,13 @@
1
+ import { type Logger } from '@applitools/logger';
2
+ import { type SpecType, type Driver } from '@applitools/driver';
3
+ /**
4
+ * iOS no-NML counterpart of `takeDomCapture`: reads the accessibility hierarchy
5
+ * through the driver and synthesizes an Applitools DOM document from it (see
6
+ * docs/native-dom-without-nml-design.md). Returns undefined — an image-only
7
+ * check — when the driver has no page source or synthesis fails; a DOM problem
8
+ * must never fail the check itself.
9
+ */
10
+ export declare function takeIosDomCapture<TSpec extends SpecType>({ driver, logger, }: {
11
+ driver: Driver<TSpec>;
12
+ logger: Logger;
13
+ }): Promise<string | undefined>;