Skip to content

Commit e90ddca

Browse files
authored
Merge pull request #3261 from GCWing/gcwing/dev
fix: preserve browser scroll position and configure commit attribution
2 parents ea94baf + 26eb5b5 commit e90ddca

28 files changed

Lines changed: 673 additions & 55 deletions

File tree

‎scripts/check-core-boundaries.test.mjs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3081,6 +3081,10 @@ test('third-party capability profiles reject ambient feature unions and unreview
30813081
uses_default_features: false,
30823082
features: ['vendored-libgit2'],
30833083
}]),
3084+
packageAt('openbitfun-webdriver', 'src/crates/adapters/webdriver/Cargo.toml', [{
3085+
name: 'image', kind: null, optional: false, uses_default_features: false,
3086+
features: ['jpeg', 'png'],
3087+
}]),
30843088
];
30853089

30863090
assert.deepEqual(findThirdPartyCapabilityFeatureViolations(validPackages), []);
@@ -3091,6 +3095,7 @@ test('third-party capability profiles reject ambient feature unions and unreview
30913095
mutatedPackages[2].dependencies[0].features.push('rustls-tls-native-roots');
30923096
mutatedPackages[3].dependencies[0].features.push('https');
30933097
mutatedPackages[3].dependencies[0].rename = 'private-git2';
3098+
mutatedPackages[4].dependencies[0].features = ['png', 'webp'];
30943099
mutatedPackages.push(packageAt('future-image-owner', 'src/apps/future/Cargo.toml', [{
30953100
name: 'image',
30963101
kind: null,
@@ -3107,6 +3112,8 @@ test('third-party capability profiles reject ambient feature unions and unreview
31073112
assert.match(messages, /openbitfun-core Tokio Tungstenite dependency has unexpected features: rustls-tls-native-roots/);
31083113
assert.match(messages, /openbitfun-services-core Git2 dependency has unexpected features: https/);
31093114
assert.match(messages, /openbitfun-services-core Git2 dependency does not match its reviewed owner shape/);
3115+
assert.match(messages, /openbitfun-webdriver Image dependency missing features: jpeg/);
3116+
assert.match(messages, /openbitfun-webdriver Image dependency has unexpected features: webp/);
31103117
assert.match(messages, /future-image-owner Image dependency is missing a reviewed owner profile/);
31113118
});
31123119

‎scripts/core-boundaries/cargo-dependency-boundaries.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,8 @@ const THIRD_PARTY_CAPABILITY_PROFILES = new Map([
574574
['openbitfun-skin-market-service', dependencyProfile(['gif', 'jpeg', 'png', 'webp'], {
575575
useDefaultFeatures: false,
576576
})],
577-
['openbitfun-webdriver', dependencyProfile(['png'], {
577+
// The embedded bridge captures PNG and encodes JPEG previews itself.
578+
['openbitfun-webdriver', dependencyProfile(['jpeg', 'png'], {
578579
useDefaultFeatures: false,
579580
})],
580581
]),

‎src/crates/adapters/webdriver/Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ tokio = { workspace = true, features = ["net", "rt", "sync", "time"] }
1616
serde = { workspace = true }
1717
serde_json = { workspace = true }
1818
log = { workspace = true }
19-
tauri = { workspace = true }
19+
tauri = { workspace = true, features = ["unstable"] }
2020
uuid = { workspace = true }
2121
base64 = { workspace = true }
22-
image = { workspace = true, features = ["png"] }
22+
image = { workspace = true, features = ["jpeg", "png"] }
2323

2424
[target.'cfg(target_os = "macos")'.dependencies]
2525
block2 = { workspace = true }

‎src/crates/adapters/webdriver/src/runtime/api/element.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ pub(crate) fn clear() -> &'static str {
6767
}
6868

6969
pub(crate) fn send_keys() -> &'static str {
70-
"(id, text) => { const el = window.__openbitfunWd.getElement(id); if (!el) { throw new Error('Element not found'); } window.__openbitfunWd.insertText(el, text); return null; }"
70+
"(id, text) => { const el = window.__openbitfunWd.getElement(id); if (!el) { throw new Error('Element not found'); } if ('value' in el || el.isContentEditable) { window.__openbitfunWd.scrollElementIntoView(el); window.__openbitfunWd.focusWithoutScroll(el); } window.__openbitfunWd.insertText(el, text); return null; }"
7171
}
7272

7373
pub(crate) fn screenshot_metadata() -> &'static str {
74-
"(id) => { const el = window.__openbitfunWd.getElement(id); if (!el || !el.isConnected) { throw new Error('stale element reference'); } el.scrollIntoView({ block: 'center', inline: 'center' }); const rect = el.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, devicePixelRatio: window.devicePixelRatio || 1 }; }"
74+
"(id) => { const el = window.__openbitfunWd.getElement(id); if (!el || !el.isConnected) { throw new Error('stale element reference'); } window.__openbitfunWd.scrollElementIntoView(el); const rect = el.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, devicePixelRatio: window.devicePixelRatio || 1 }; }"
7575
}
7676

7777
pub(crate) fn get_shadow_root() -> &'static str {

‎src/crates/adapters/webdriver/src/runtime/script/core/execution.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub(super) fn script() -> &'static str {
7878
validateFrameElement,
7979
getShadowRoot,
8080
isDisplayed,
81+
scrollElementIntoView,
82+
focusWithoutScroll,
8183
clearElement,
8284
insertText,
8385
setElementText,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
pub(super) fn script() -> &'static str {
2+
r####"
3+
// Reveal an offscreen target with minimal movement. Centering also scrolls
4+
// overflow:hidden ancestors, which can displace the surrounding app chrome.
5+
const scrollElementIntoView = (element) => {
6+
element.scrollIntoView({ behavior: "instant", block: "nearest", inline: "nearest" });
7+
};
8+
9+
// Positioning belongs to the caller; focus must not move it again, especially
10+
// after pointer coordinates have already been resolved.
11+
const focusWithoutScroll = (element) => {
12+
if (typeof element.focus === "function") {
13+
element.focus({ preventScroll: true });
14+
}
15+
};
16+
17+
// A large target can remain only partly visible after nearest scrolling.
18+
// Use its visible center rather than a point outside the viewport.
19+
const getInViewCenter = (element) => {
20+
const rect = element.getClientRects()[0] || element.getBoundingClientRect();
21+
const ownerWindow = element.ownerDocument.defaultView || window;
22+
return {
23+
x: (Math.max(0, rect.left) + Math.min(ownerWindow.innerWidth, rect.right)) / 2,
24+
y: (Math.max(0, rect.top) + Math.min(ownerWindow.innerHeight, rect.bottom)) / 2
25+
};
26+
};
27+
"####
28+
}

‎src/crates/adapters/webdriver/src/runtime/script/core/mod.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod alert;
22
mod context;
33
mod cookie;
44
mod execution;
5+
mod interaction;
56
mod locator;
67
mod runtime;
78
mod shadow;
@@ -10,13 +11,14 @@ mod visibility;
1011

1112
pub(super) fn head() -> String {
1213
format!(
13-
"{runtime}{store}{context}{locator}{shadow}{visibility}",
14+
"{runtime}{store}{context}{locator}{shadow}{visibility}{interaction}",
1415
runtime = runtime::script(),
1516
store = store::script(),
1617
context = context::script(),
1718
locator = locator::script(),
1819
shadow = shadow::script(),
19-
visibility = visibility::script()
20+
visibility = visibility::script(),
21+
interaction = interaction::script()
2022
)
2123
}
2224

‎src/crates/adapters/webdriver/src/runtime/script/input.rs‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ pub(super) fn script() -> &'static str {
7272
if (!element) {
7373
return;
7474
}
75+
if ("value" in element || element.isContentEditable) {
76+
scrollElementIntoView(element);
77+
focusWithoutScroll(element);
78+
}
7579
if ("value" in element) {
76-
element.focus();
7780
if (!dispatchBeforeInputEvent(element, "deleteContentBackward", null)) {
7881
return;
7982
}
@@ -82,7 +85,6 @@ pub(super) fn script() -> &'static str {
8285
return;
8386
}
8487
if (element.isContentEditable) {
85-
element.focus();
8688
element.textContent = "";
8789
emitInputEvents(element, "deleteContentBackward", null);
8890
}
@@ -112,7 +114,7 @@ pub(super) fn script() -> &'static str {
112114
return;
113115
}
114116
const selection = ownerWindow.getSelection();
115-
element.focus();
117+
focusWithoutScroll(element);
116118
if (selection && selection.rangeCount > 0) {
117119
selection.deleteFromDocument();
118120
selection.getRangeAt(0).insertNode(element.ownerDocument.createTextNode(text));

‎src/crates/adapters/webdriver/src/runtime/script/pointer/mouse.rs‎

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,16 @@ pub(super) fn script() -> &'static str {
3737
};
3838
}
3939
40-
if (origin && typeof origin === "object" && typeof origin[ELEMENT_KEY] === "string") {
41-
const element = getElement(origin[ELEMENT_KEY]);
40+
// Script arguments have already deserialized wire element references.
41+
if (isElementLike(origin) || (origin && typeof origin === "object" && typeof origin[ELEMENT_KEY] === "string")) {
42+
const element = isElementLike(origin) ? origin : getElement(origin[ELEMENT_KEY]);
4243
if (!element) {
4344
throw new Error("Element not found");
4445
}
45-
const rect = element.getBoundingClientRect();
46+
const { x, y } = getInViewCenter(element);
4647
return {
47-
x: rect.left + rect.width / 2 + (Number(action?.x) || 0),
48-
y: rect.top + rect.height / 2 + (Number(action?.y) || 0),
48+
x: x + (Number(action?.x) || 0),
49+
y: y + (Number(action?.y) || 0),
4950
target: element
5051
};
5152
}
@@ -108,13 +109,9 @@ pub(super) fn script() -> &'static str {
108109
if (!element) {
109110
throw new Error("Element not found");
110111
}
111-
element.scrollIntoView({ block: "center", inline: "center" });
112-
if (typeof element.focus === "function") {
113-
element.focus();
114-
}
115-
const rect = element.getBoundingClientRect();
116-
const x = rect.left + rect.width / 2;
117-
const y = rect.top + rect.height / 2;
112+
scrollElementIntoView(element);
113+
focusWithoutScroll(element);
114+
const { x, y } = getInViewCenter(element);
118115
updatePointerTarget(getFrameContext(), x, y, element);
119116
const runtime = ensureRuntimeState();
120117
const buttonMask = pointerButtonMask(button);

‎src/crates/adapters/webdriver/src/runtime/script/pointer/pointer_source.rs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ pub(super) fn script() -> &'static str {
3535
if (action.duration) {
3636
await sleep(action.duration);
3737
}
38-
if (typeof target.focus === "function") {
39-
target.focus();
40-
}
38+
focusWithoutScroll(target);
4139
runtime.pointer.buttons |= buttonMask;
4240
dispatchMouseEvent(target, "mousedown", runtime.pointer.x, runtime.pointer.y, button, runtime.pointer.buttons, frameContext);
4341
continue;

0 commit comments

Comments
 (0)