Skip to content

Commit 010edb8

Browse files
committed
Move contenteditable arrow XCUITest to app-side harness
1 parent 23d793b commit 010edb8

2 files changed

Lines changed: 337 additions & 493 deletions

File tree

Sources/AppDelegate.swift

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
20772077
private var bonsplitTabDragUITestRecorder: DispatchSourceTimer?
20782078
private var gotoSplitUITestArrowRecorder: DispatchSourceTimer?
20792079
private var gotoSplitUITestInputSetupGeneration = 0
2080+
private var gotoSplitUITestContentEditableSetupGeneration = 0
20802081
private var gotoSplitUITestObservers: [NSObjectProtocol] = []
20812082
private var didSetupMultiWindowNotificationsUITest = false
20822083
private var didSetupDisplayResolutionUITestDiagnostics = false
@@ -7883,6 +7884,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
78837884
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_GOTO_SPLIT_ARROW_SETUP"] == "1" {
78847885
self.startGotoSplitUITestArrowRecorder(panelId: panel.id)
78857886
}
7887+
if ProcessInfo.processInfo.environment["CMUX_UI_TEST_GOTO_SPLIT_CONTENTEDITABLE_SETUP"] == "1" {
7888+
self.setupContentEditableForGotoSplitUITest(panel: panel)
7889+
}
78867890
return
78877891
}
78887892

@@ -7933,6 +7937,214 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
79337937
attempt()
79347938
}
79357939

7940+
private func setupContentEditableForGotoSplitUITest(panel: BrowserPanel) {
7941+
guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_GOTO_SPLIT_CONTENTEDITABLE_SETUP"] == "1" else {
7942+
return
7943+
}
7944+
7945+
gotoSplitUITestContentEditableSetupGeneration += 1
7946+
let generation = gotoSplitUITestContentEditableSetupGeneration
7947+
let deadline = Date().addingTimeInterval(8.0)
7948+
let script = """
7949+
(() => {
7950+
const snapshot = () => {
7951+
const active = document.activeElement;
7952+
return {
7953+
seeded: false,
7954+
editorId: "",
7955+
activeId: active && typeof active.id === "string" ? active.id : "",
7956+
readyState: String(document.readyState || ""),
7957+
secondaryCenterX: -1,
7958+
secondaryCenterY: -1,
7959+
editorCenterX: -1,
7960+
editorCenterY: -1
7961+
};
7962+
};
7963+
const seed = () => {
7964+
const parent = document.getElementById("cmux-ui-test-focus-container");
7965+
const secondary = document.getElementById("cmux-ui-test-focus-input-secondary");
7966+
if (!document.body || !parent || !secondary) {
7967+
return snapshot();
7968+
}
7969+
7970+
let editor = document.getElementById("cmux-ui-test-contenteditable");
7971+
if (!editor || !editor.tagName || editor.tagName.toLowerCase() !== "div") {
7972+
editor = document.createElement("div");
7973+
editor.id = "cmux-ui-test-contenteditable";
7974+
}
7975+
7976+
editor.setAttribute("contenteditable", "true");
7977+
editor.setAttribute("role", "textbox");
7978+
editor.setAttribute("aria-label", "cmux-ui-test-contenteditable");
7979+
editor.spellcheck = false;
7980+
editor.tabIndex = 0;
7981+
editor.innerHTML = "alpha<br>beta<br>gamma";
7982+
editor.style.display = "block";
7983+
editor.style.minHeight = "84px";
7984+
editor.style.padding = "8px 10px";
7985+
editor.style.border = "1px solid #5f6368";
7986+
editor.style.borderRadius = "6px";
7987+
editor.style.boxSizing = "border-box";
7988+
editor.style.fontSize = "14px";
7989+
editor.style.fontFamily = "system-ui, -apple-system, sans-serif";
7990+
editor.style.background = "white";
7991+
editor.style.color = "black";
7992+
editor.style.whiteSpace = "pre-wrap";
7993+
editor.style.outline = "none";
7994+
7995+
if (editor.parentElement !== parent) {
7996+
parent.appendChild(editor);
7997+
}
7998+
7999+
if (!window.__cmuxContentEditableArrowReport || typeof window.__cmuxContentEditableArrowReport !== "object") {
8000+
window.__cmuxContentEditableArrowReport = {
8001+
down: 0,
8002+
up: 0,
8003+
commandShiftDown: 0,
8004+
commandShiftUp: 0
8005+
};
8006+
}
8007+
8008+
if (!editor.__cmuxContentEditableArrowReportInstalled) {
8009+
editor.__cmuxContentEditableArrowReportInstalled = true;
8010+
editor.addEventListener("keydown", (event) => {
8011+
if (event.key === "ArrowDown") window.__cmuxContentEditableArrowReport.down += 1;
8012+
if (event.key === "ArrowUp") window.__cmuxContentEditableArrowReport.up += 1;
8013+
if (event.key === "ArrowDown" && event.metaKey && event.shiftKey) {
8014+
window.__cmuxContentEditableArrowReport.commandShiftDown += 1;
8015+
}
8016+
if (event.key === "ArrowUp" && event.metaKey && event.shiftKey) {
8017+
window.__cmuxContentEditableArrowReport.commandShiftUp += 1;
8018+
}
8019+
}, true);
8020+
}
8021+
8022+
editor.focus({ preventScroll: true });
8023+
const selection = window.getSelection();
8024+
if (selection) {
8025+
const range = document.createRange();
8026+
range.selectNodeContents(editor);
8027+
range.collapse(false);
8028+
selection.removeAllRanges();
8029+
selection.addRange(range);
8030+
}
8031+
8032+
const secondaryRect = secondary.getBoundingClientRect();
8033+
const editorRect = editor.getBoundingClientRect();
8034+
const active = document.activeElement;
8035+
return {
8036+
seeded: active === editor,
8037+
editorId: editor.id || "",
8038+
activeId: active && typeof active.id === "string" ? active.id : "",
8039+
readyState: String(document.readyState || ""),
8040+
secondaryCenterX: secondaryRect.left + (secondaryRect.width / 2),
8041+
secondaryCenterY: secondaryRect.top + (secondaryRect.height / 2),
8042+
editorCenterX: editorRect.left + (editorRect.width / 2),
8043+
editorCenterY: editorRect.top + (editorRect.height / 2)
8044+
};
8045+
};
8046+
const ready =
8047+
String(document.readyState || "") === "complete" &&
8048+
!!document.body &&
8049+
!!document.getElementById("cmux-ui-test-focus-container") &&
8050+
!!document.getElementById("cmux-ui-test-focus-input-secondary");
8051+
8052+
if (!ready) {
8053+
return snapshot();
8054+
}
8055+
8056+
try {
8057+
return seed();
8058+
} catch (_) {
8059+
return snapshot();
8060+
}
8061+
})();
8062+
"""
8063+
8064+
func attempt() {
8065+
guard gotoSplitUITestContentEditableSetupGeneration == generation else { return }
8066+
8067+
panel.webView.evaluateJavaScript(script) { [weak self, weak panel] result, _ in
8068+
guard let self,
8069+
self.gotoSplitUITestContentEditableSetupGeneration == generation,
8070+
let panel else { return }
8071+
8072+
let payload = result as? [String: Any]
8073+
let seeded = (payload?["seeded"] as? Bool) ?? false
8074+
let editorId = (payload?["editorId"] as? String) ?? ""
8075+
let activeId = (payload?["activeId"] as? String) ?? ""
8076+
let readyState = (payload?["readyState"] as? String) ?? ""
8077+
let secondaryCenterX = (payload?["secondaryCenterX"] as? NSNumber)?.doubleValue ?? -1
8078+
let secondaryCenterY = (payload?["secondaryCenterY"] as? NSNumber)?.doubleValue ?? -1
8079+
let editorCenterX = (payload?["editorCenterX"] as? NSNumber)?.doubleValue ?? -1
8080+
let editorCenterY = (payload?["editorCenterY"] as? NSNumber)?.doubleValue ?? -1
8081+
var editorClickOffsetX = -1.0
8082+
var editorClickOffsetY = -1.0
8083+
8084+
if let window = panel.webView.window {
8085+
let webFrame = panel.webView.convert(panel.webView.bounds, to: nil)
8086+
let contentHeight = Double(window.contentView?.bounds.height ?? 0)
8087+
if webFrame.width > 1,
8088+
webFrame.height > 1,
8089+
contentHeight > 1,
8090+
secondaryCenterX >= 0,
8091+
secondaryCenterY >= 0,
8092+
editorCenterX >= 0,
8093+
editorCenterY >= 0 {
8094+
let editorXInContent = Double(webFrame.minX) + editorCenterX
8095+
let editorYInContent = Double(webFrame.maxY) - editorCenterY
8096+
let titlebarHeight = max(0, Double(window.frame.height) - contentHeight)
8097+
editorClickOffsetX = editorXInContent
8098+
editorClickOffsetY = titlebarHeight + (contentHeight - editorYInContent)
8099+
}
8100+
}
8101+
8102+
if seeded,
8103+
!editorId.isEmpty,
8104+
activeId == editorId,
8105+
editorClickOffsetX > 0,
8106+
editorClickOffsetY > 0 {
8107+
self.writeGotoSplitTestData([
8108+
"webContentEditableSeeded": "true",
8109+
"webContentEditableElementId": editorId,
8110+
"webContentEditableActiveElementId": activeId,
8111+
"webContentEditableReadyState": readyState,
8112+
"webContentEditableClickOffsetX": "\(editorClickOffsetX)",
8113+
"webContentEditableClickOffsetY": "\(editorClickOffsetY)"
8114+
])
8115+
return
8116+
}
8117+
8118+
if Date() < deadline {
8119+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [weak self] in
8120+
guard let self,
8121+
self.gotoSplitUITestContentEditableSetupGeneration == generation else { return }
8122+
attempt()
8123+
}
8124+
return
8125+
}
8126+
8127+
self.writeGotoSplitTestData([
8128+
"webContentEditableSeeded": "false",
8129+
"webContentEditableElementId": editorId,
8130+
"webContentEditableActiveElementId": activeId,
8131+
"webContentEditableReadyState": readyState,
8132+
"webContentEditableClickOffsetX": "\(editorClickOffsetX)",
8133+
"webContentEditableClickOffsetY": "\(editorClickOffsetY)",
8134+
"setupError":
8135+
"Timed out focusing contenteditable for omnibar restore test " +
8136+
"seeded=\(seeded) editorId=\(editorId) activeId=\(activeId) " +
8137+
"readyState=\(readyState) secondaryCenterX=\(secondaryCenterX) " +
8138+
"secondaryCenterY=\(secondaryCenterY) editorCenterX=\(editorCenterX) " +
8139+
"editorCenterY=\(editorCenterY) editorClickOffsetX=\(editorClickOffsetX) " +
8140+
"editorClickOffsetY=\(editorClickOffsetY)"
8141+
])
8142+
}
8143+
}
8144+
8145+
attempt()
8146+
}
8147+
79368148
private func startGotoSplitUITestArrowRecorder(panelId: UUID) {
79378149
guard ProcessInfo.processInfo.environment["CMUX_UI_TEST_GOTO_SPLIT_ARROW_SETUP"] == "1" else { return }
79388150

@@ -7964,13 +8176,24 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
79648176
commandShiftDown: 0,
79658177
commandShiftUp: 0
79668178
};
8179+
const contentEditableReport = window.__cmuxContentEditableArrowReport || {
8180+
down: 0,
8181+
up: 0,
8182+
commandShiftDown: 0,
8183+
commandShiftUp: 0
8184+
};
79678185
const active = document.activeElement;
79688186
return {
79698187
installed: !!window.__cmuxArrowKeyReport,
79708188
down: Number(report.down || 0),
79718189
up: Number(report.up || 0),
79728190
commandShiftDown: Number(report.commandShiftDown || 0),
79738191
commandShiftUp: Number(report.commandShiftUp || 0),
8192+
contentEditableInstalled: !!window.__cmuxContentEditableArrowReport,
8193+
contentEditableDown: Number(contentEditableReport.down || 0),
8194+
contentEditableUp: Number(contentEditableReport.up || 0),
8195+
contentEditableCommandShiftDown: Number(contentEditableReport.commandShiftDown || 0),
8196+
contentEditableCommandShiftUp: Number(contentEditableReport.commandShiftUp || 0),
79748197
activeId: active && typeof active.id === "string" ? active.id : "",
79758198
selectionStart: active && typeof active.selectionStart === "number" ? active.selectionStart : null,
79768199
selectionEnd: active && typeof active.selectionEnd === "number" ? active.selectionEnd : null,
@@ -7994,6 +8217,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
79948217
let up = (payload["up"] as? NSNumber)?.intValue ?? 0
79958218
let commandShiftDown = (payload["commandShiftDown"] as? NSNumber)?.intValue ?? 0
79968219
let commandShiftUp = (payload["commandShiftUp"] as? NSNumber)?.intValue ?? 0
8220+
let contentEditableInstalled = (payload["contentEditableInstalled"] as? Bool) ?? false
8221+
let contentEditableDown = (payload["contentEditableDown"] as? NSNumber)?.intValue ?? 0
8222+
let contentEditableUp = (payload["contentEditableUp"] as? NSNumber)?.intValue ?? 0
8223+
let contentEditableCommandShiftDown = (payload["contentEditableCommandShiftDown"] as? NSNumber)?.intValue ?? 0
8224+
let contentEditableCommandShiftUp = (payload["contentEditableCommandShiftUp"] as? NSNumber)?.intValue ?? 0
79978225
let activeId = (payload["activeId"] as? String) ?? ""
79988226
let selectionStart = (payload["selectionStart"] as? NSNumber)?.intValue
79998227
let selectionEnd = (payload["selectionEnd"] as? NSNumber)?.intValue
@@ -8011,6 +8239,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
80118239
"browserArrowSelectionStart": selectionStart.map(String.init) ?? "",
80128240
"browserArrowSelectionEnd": selectionEnd.map(String.init) ?? "",
80138241
"browserArrowReadyState": readyState,
8242+
"browserContentEditableInstalled": contentEditableInstalled ? "true" : "false",
8243+
"browserContentEditableDownCount": "\(contentEditableDown)",
8244+
"browserContentEditableUpCount": "\(contentEditableUp)",
8245+
"browserContentEditableCommandShiftDownCount": "\(contentEditableCommandShiftDown)",
8246+
"browserContentEditableCommandShiftUpCount": "\(contentEditableCommandShiftUp)",
8247+
"browserContentEditableActiveElementId": activeId,
8248+
"browserContentEditableReadyState": readyState,
80148249
"browserArrowFirstResponderType": firstResponderType,
80158250
"browserArrowFirstResponderIsFieldEditor": firstResponderIsFieldEditor ? "true" : "false",
80168251
"browserArrowFirstResponderIsCmuxWebView": firstResponderIsCmuxWebView ? "true" : "false",

0 commit comments

Comments
 (0)