mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
1135 lines
52 KiB
Swift
1135 lines
52 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
private actor DashboardRouteAuthGate {
|
|
private var token: String?
|
|
private var ready = false
|
|
private var probeCount = 0
|
|
|
|
init(token: String?) {
|
|
self.token = token
|
|
}
|
|
|
|
func authToken() -> String? {
|
|
self.ready ? self.token : nil
|
|
}
|
|
|
|
func probe() {
|
|
self.ready = true
|
|
self.probeCount += 1
|
|
}
|
|
|
|
func replaceToken(_ token: String?) {
|
|
self.token = token
|
|
}
|
|
|
|
func probes() -> Int {
|
|
self.probeCount
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class DashboardBrowserImportGate {
|
|
var isOnboarded = false
|
|
private(set) var requestCount = 0
|
|
|
|
func request() -> Bool {
|
|
self.requestCount += 1
|
|
return self.isOnboarded
|
|
}
|
|
}
|
|
|
|
private final class DashboardWindowGestureSpy: NSWindow {
|
|
private(set) var dragCount = 0
|
|
private(set) var zoomCount = 0
|
|
|
|
override func performDrag(with _: NSEvent) {
|
|
self.dragCount += 1
|
|
}
|
|
|
|
override func performZoom(_: Any?) {
|
|
self.zoomCount += 1
|
|
}
|
|
}
|
|
|
|
@Suite(.serialized)
|
|
@MainActor
|
|
struct DashboardWindowSmokeTests {
|
|
@Test func `dashboard frame routes single click to drag and double click to zoom`() throws {
|
|
let window = DashboardWindowGestureSpy(
|
|
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
|
|
styleMask: [.titled, .resizable],
|
|
backing: .buffered,
|
|
defer: false)
|
|
let dragRegion = DashboardWindowDragRegionView(
|
|
frame: NSRect(x: 0, y: 0, width: 300, height: 12))
|
|
window.contentView = dragRegion
|
|
let mouseDownEvent: (Int) -> NSEvent? = { clickCount in
|
|
NSEvent.mouseEvent(
|
|
with: .leftMouseDown,
|
|
location: NSPoint(x: 100, y: 6),
|
|
modifierFlags: [],
|
|
timestamp: 0,
|
|
windowNumber: window.windowNumber,
|
|
context: nil,
|
|
eventNumber: clickCount,
|
|
clickCount: clickCount,
|
|
pressure: 1)
|
|
}
|
|
|
|
dragRegion.mouseDown(with: try #require(mouseDownEvent(1)))
|
|
|
|
#expect(window.dragCount == 1)
|
|
#expect(window.zoomCount == 0)
|
|
|
|
dragRegion.mouseDown(with: try #require(mouseDownEvent(2)))
|
|
|
|
#expect(window.dragCount == 1)
|
|
#expect(window.zoomCount == 1)
|
|
}
|
|
|
|
@Test func `dashboard window controller shows and closes`() throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:18789/control/#token=device-token"))
|
|
let windowAutosaveName = "OpenClawDashboardWindow-Test-\(UUID().uuidString)"
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:18789/control/",
|
|
token: "device-token",
|
|
password: nil),
|
|
windowAutosaveName: windowAutosaveName)
|
|
controller.show()
|
|
#expect(controller.window?.styleMask.contains(.titled) == true)
|
|
#expect(controller.window?.styleMask.contains(.closable) == true)
|
|
#expect(controller.window?.isRestorable == false)
|
|
#expect(controller.window?.contentViewController != nil)
|
|
#expect(controller.window?.standardWindowButton(.closeButton) != nil)
|
|
// The empty unified toolbar is what grows the titlebar to 52pt so the
|
|
// traffic lights center against the web titlebar row; without it they
|
|
// hug the top edge and misalign with the hosted web buttons.
|
|
#expect(controller.window?.toolbar != nil)
|
|
#expect(controller.window?.toolbarStyle == .unified)
|
|
// The toolbar only exists to size the titlebar, so View > Hide Toolbar
|
|
// (⌥⌘T) must be refused; otherwise hiding it desyncs the 52pt web inset.
|
|
controller.window?.toggleToolbarShown(nil)
|
|
#expect(controller.window?.toolbar?.isVisible == true)
|
|
#expect((controller.window?.frame.width ?? 0) >= DashboardWindowLayout.windowMinSize.width)
|
|
#expect((controller.window?.frame.height ?? 0) >= DashboardWindowLayout.windowMinSize.height)
|
|
#expect(controller.window?.frameAutosaveName == windowAutosaveName)
|
|
controller.closeDashboard()
|
|
}
|
|
|
|
@Test func `dashboard context menu removes browser items and collapses separators`() {
|
|
let hiddenIdentifiers = [
|
|
"WKMenuItemIdentifierReload",
|
|
"WKMenuItemIdentifierOpenLinkInNewWindow",
|
|
"WKMenuItemIdentifierOpenImageInNewWindow",
|
|
"WKMenuItemIdentifierOpenMediaInNewWindow",
|
|
"WKMenuItemIdentifierOpenFrameInNewWindow",
|
|
"WKMenuItemIdentifierDownloadLinkedFile",
|
|
"WKMenuItemIdentifierDownloadImage",
|
|
"WKMenuItemIdentifierDownloadMedia",
|
|
]
|
|
let hiddenItems = hiddenIdentifiers.map { identifier in
|
|
let item = NSMenuItem(title: identifier, action: nil, keyEquivalent: "")
|
|
item.identifier = NSUserInterfaceItemIdentifier(identifier)
|
|
return item
|
|
}
|
|
let copy = NSMenuItem(title: "Copy", action: nil, keyEquivalent: "")
|
|
let inspect = NSMenuItem(title: "Inspect Element", action: nil, keyEquivalent: "")
|
|
let filtered = DashboardWebView.filteredContextMenuItems([
|
|
.separator(),
|
|
hiddenItems[0],
|
|
.separator(),
|
|
copy,
|
|
.separator(),
|
|
.separator(),
|
|
hiddenItems[1],
|
|
hiddenItems[2],
|
|
hiddenItems[3],
|
|
hiddenItems[4],
|
|
hiddenItems[5],
|
|
hiddenItems[6],
|
|
hiddenItems[7],
|
|
.separator(),
|
|
inspect,
|
|
.separator(),
|
|
])
|
|
|
|
#expect(filtered.map(\.title) == ["Copy", "", "Inspect Element"])
|
|
#expect(filtered[1].isSeparatorItem)
|
|
#expect(!filtered.contains { hiddenIdentifiers.contains($0.identifier?.rawValue ?? "") })
|
|
}
|
|
|
|
@Test func `dashboard reload decision preserves live same URL content`() throws {
|
|
let current = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let replacement = try #require(URL(string: "http://127.0.0.1:18790/control/"))
|
|
let auth = DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:18789/control/",
|
|
token: nil,
|
|
password: "secret")
|
|
let rotatedAuth = DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:18789/control/",
|
|
token: nil,
|
|
password: "rotated")
|
|
|
|
#expect(!DashboardWindowController.shouldReloadDashboard(
|
|
currentURL: current,
|
|
newURL: current,
|
|
currentAuth: auth,
|
|
newAuth: auth,
|
|
hasUsableDocument: true,
|
|
isShowingFailurePage: false))
|
|
#expect(DashboardWindowController.shouldReloadDashboard(
|
|
currentURL: current,
|
|
newURL: current,
|
|
currentAuth: auth,
|
|
newAuth: auth,
|
|
hasUsableDocument: false,
|
|
isShowingFailurePage: false))
|
|
#expect(DashboardWindowController.shouldReloadDashboard(
|
|
currentURL: current,
|
|
newURL: replacement,
|
|
currentAuth: auth,
|
|
newAuth: auth,
|
|
hasUsableDocument: true,
|
|
isShowingFailurePage: false))
|
|
// Password-only auth keeps the URL identical; rotation must reload.
|
|
#expect(DashboardWindowController.shouldReloadDashboard(
|
|
currentURL: current,
|
|
newURL: current,
|
|
currentAuth: auth,
|
|
newAuth: rotatedAuth,
|
|
hasUsableDocument: true,
|
|
isShowingFailurePage: false))
|
|
// An in-flight failure page is never a usable document to keep.
|
|
#expect(DashboardWindowController.shouldReloadDashboard(
|
|
currentURL: current,
|
|
newURL: current,
|
|
currentAuth: auth,
|
|
newAuth: auth,
|
|
hasUsableDocument: true,
|
|
isShowingFailurePage: true))
|
|
}
|
|
|
|
@Test func `dashboard native command queues before page load`() throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
|
|
controller.dispatchNativeCommand(.newSession)
|
|
controller.dispatchNativeCommand(.commandPalette)
|
|
controller.dispatchNativeCommand(.commandPalette)
|
|
|
|
#expect(controller._testPendingNativeCommands == [.newSession, .commandPalette, .commandPalette])
|
|
|
|
// A terminal failure drops moment-bound intent instead of replaying it
|
|
// after a later recovery reload.
|
|
controller.showFailure(title: "Dashboard unavailable", message: "offline")
|
|
#expect(controller._testPendingNativeCommands.isEmpty)
|
|
}
|
|
|
|
@Test func `dashboard navigation stays on same endpoint`() throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let staleEndpoint = try #require(URL(string: "http://127.0.0.1:18790/control/chat"))
|
|
#expect(try DashboardWindowController.shouldAllowNavigation(
|
|
to: #require(URL(string: "http://127.0.0.1:18789/control/chat")),
|
|
dashboardURL: dashboard,
|
|
isMainFrame: true))
|
|
#expect(try !DashboardWindowController.shouldAllowNavigation(
|
|
to: #require(URL(string: "https://docs.openclaw.ai/")),
|
|
dashboardURL: dashboard,
|
|
isMainFrame: true))
|
|
#expect(!DashboardWindowController.shouldAllowNavigation(
|
|
to: staleEndpoint,
|
|
dashboardURL: dashboard,
|
|
isMainFrame: true))
|
|
#expect(!DashboardWindowController.shouldOpenExternalDashboardNavigation(
|
|
staleEndpoint,
|
|
navigationType: .backForward,
|
|
buttonNumber: 1))
|
|
}
|
|
|
|
@Test func `dashboard permits only trusted ClickClack discussion subframes`() throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let channel = try #require(URL(string: "http://127.0.0.1:18890/embed/channel/T01/C01"))
|
|
let thread = try #require(URL(string: "http://127.0.0.1:18890/embed/thread/T01/M01"))
|
|
let hostnameAlias = try #require(URL(string: "http://localhost:18890/embed/channel/T01/C01"))
|
|
let ipv6Alias = try #require(URL(string: "http://[::1]:18890/embed/thread/T01/M01"))
|
|
let credentialedFrame = try #require(URL(string: "http://user:pass@localhost:18890/embed/channel/T01/C01"))
|
|
let unrelatedPath = try #require(URL(string: "http://127.0.0.1:18890/admin"))
|
|
let externalFrame = try #require(URL(string: "https://clickclack.example/embed/channel/T01/C01"))
|
|
let externalHTTPFrame = try #require(URL(string: "http://clickclack.example/embed/thread/T01/M01"))
|
|
let localFile = try #require(URL(string: "file:///tmp/discussion.html"))
|
|
|
|
#expect(DashboardWindowController.shouldAllowNavigation(
|
|
to: channel, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(DashboardWindowController.shouldAllowNavigation(
|
|
to: thread, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(DashboardWindowController.shouldAllowNavigation(
|
|
to: hostnameAlias, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(DashboardWindowController.shouldAllowNavigation(
|
|
to: ipv6Alias, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(DashboardWindowController.shouldAllowNavigation(
|
|
to: externalFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(DashboardWindowController.shouldAllowNavigation(
|
|
to: externalHTTPFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(!DashboardWindowController.shouldAllowNavigation(
|
|
to: channel, dashboardURL: dashboard, isMainFrame: true))
|
|
#expect(!DashboardWindowController.shouldAllowNavigation(
|
|
to: credentialedFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(!DashboardWindowController.shouldAllowNavigation(
|
|
to: unrelatedPath, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
#expect(!DashboardWindowController.shouldAllowNavigation(
|
|
to: externalFrame, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: false))
|
|
#expect(!DashboardWindowController.shouldAllowNavigation(
|
|
to: localFile, dashboardURL: dashboard, isMainFrame: false, isTrustedDashboardSource: true))
|
|
}
|
|
|
|
@Test func `dashboard navigation shortcuts target the focused browser`() throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
#expect(controller._testNavigationWebViewIdentity == controller._testDashboardWebViewIdentity)
|
|
|
|
try controller._testOpenLinkBrowser(#require(URL(string: "https://docs.openclaw.ai/")))
|
|
let linkWebView = try #require(controller._testLinkBrowserWebViewIdentity)
|
|
#expect(controller._testFocusLinkBrowser())
|
|
#expect(controller._testNavigationWebViewIdentity == linkWebView)
|
|
}
|
|
|
|
@Test func `browser import offer retries until the first completed inline browser request`() async throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
var requestCount = 0
|
|
var firstRequestContinuation: CheckedContinuation<Bool, Never>?
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil),
|
|
requestBrowserProfileImportOffer: { _ in
|
|
requestCount += 1
|
|
if requestCount == 1 {
|
|
return await withCheckedContinuation { continuation in
|
|
firstRequestContinuation = continuation
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
defer { controller.closeDashboard() }
|
|
|
|
controller.show()
|
|
#expect(requestCount == 0)
|
|
|
|
let link = try #require(URL(string: "https://docs.openclaw.ai/"))
|
|
controller._testOpenLinkBrowser(link)
|
|
controller.update(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
#expect(requestCount == 0)
|
|
|
|
controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true)
|
|
for _ in 0..<200 where firstRequestContinuation == nil {
|
|
await Task.yield()
|
|
}
|
|
#expect(requestCount == 1)
|
|
|
|
controller.update(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
firstRequestContinuation?.resume(returning: false)
|
|
firstRequestContinuation = nil
|
|
for _ in 0..<200 where requestCount == 1 {
|
|
await Task.yield()
|
|
}
|
|
#expect(requestCount == 2)
|
|
|
|
controller._testCloseLinkBrowser()
|
|
controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true)
|
|
for _ in 0..<10 {
|
|
await Task.yield()
|
|
}
|
|
#expect(requestCount == 2)
|
|
}
|
|
|
|
@Test func `browser import offer retries when onboarding completes with browser open`() async throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let gate = DashboardBrowserImportGate()
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil),
|
|
requestBrowserProfileImportOffer: { _ in gate.request() })
|
|
defer { controller.closeDashboard() }
|
|
let manager = DashboardManager._testMake()
|
|
manager._testSetController(controller)
|
|
|
|
let link = try #require(URL(string: "https://docs.openclaw.ai/"))
|
|
controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true)
|
|
for _ in 0..<200 where gate.requestCount == 0 {
|
|
await Task.yield()
|
|
}
|
|
#expect(gate.requestCount == 1)
|
|
|
|
gate.isOnboarded = true
|
|
manager.handleOnboardingCompletion()
|
|
for _ in 0..<200 where gate.requestCount == 1 {
|
|
await Task.yield()
|
|
}
|
|
#expect(gate.requestCount == 2)
|
|
|
|
manager.handleOnboardingCompletion()
|
|
for _ in 0..<10 {
|
|
await Task.yield()
|
|
}
|
|
#expect(gate.requestCount == 2)
|
|
}
|
|
|
|
@Test func `closing inline browser invalidates an in-flight import offer`() async throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
var requestCount = 0
|
|
var firstRequestContinuation: CheckedContinuation<Void, Never>?
|
|
var firstRequestApplied: Bool?
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil),
|
|
requestBrowserProfileImportOffer: { shouldApply in
|
|
requestCount += 1
|
|
if requestCount == 1 {
|
|
await withCheckedContinuation { continuation in
|
|
firstRequestContinuation = continuation
|
|
}
|
|
firstRequestApplied = shouldApply()
|
|
return firstRequestApplied == true
|
|
}
|
|
return shouldApply()
|
|
})
|
|
defer { controller.closeDashboard() }
|
|
|
|
let link = try #require(URL(string: "https://docs.openclaw.ai/"))
|
|
controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true)
|
|
for _ in 0..<200 where firstRequestContinuation == nil {
|
|
await Task.yield()
|
|
}
|
|
#expect(requestCount == 1)
|
|
|
|
controller._testCloseLinkBrowser()
|
|
firstRequestContinuation?.resume()
|
|
for _ in 0..<200 where firstRequestApplied == nil {
|
|
await Task.yield()
|
|
}
|
|
#expect(firstRequestApplied == false)
|
|
|
|
controller._testOpenLinkBrowser(link, requestBrowserProfileImportOffer: true)
|
|
for _ in 0..<200 where requestCount == 1 {
|
|
await Task.yield()
|
|
}
|
|
#expect(requestCount == 2)
|
|
}
|
|
|
|
@Test func `dashboard parses only bounded native link requests`() throws {
|
|
let request = DashboardWindowController.linkRequest(from: [
|
|
"type": "open-link",
|
|
"url": "https://docs.openclaw.ai/platforms/macos",
|
|
"target": "inline",
|
|
])
|
|
#expect(try request == DashboardLinkRequest(
|
|
url: #require(URL(string: "https://docs.openclaw.ai/platforms/macos")),
|
|
target: .inline))
|
|
|
|
#expect(DashboardWindowController.linkRequest(from: [
|
|
"type": "open-link",
|
|
"url": "file:///tmp/private",
|
|
"target": "inline",
|
|
]) == nil)
|
|
#expect(DashboardWindowController.linkRequest(from: [
|
|
"type": "open-link",
|
|
"url": "https://docs.openclaw.ai/",
|
|
"target": "unknown",
|
|
]) == nil)
|
|
#expect(DashboardWindowController.linkRequest(from: [
|
|
"type": "other",
|
|
"url": "https://docs.openclaw.ai/",
|
|
"target": "external",
|
|
]) == nil)
|
|
#expect(try DashboardWindowController.linkRequest(from: [
|
|
"type": "open-link",
|
|
"url": "mailto:hello@example.com",
|
|
"target": "external",
|
|
]) == DashboardLinkRequest(
|
|
url: #require(URL(string: "mailto:hello@example.com")),
|
|
target: .external))
|
|
#expect(DashboardWindowController.linkRequest(from: [
|
|
"type": "open-link",
|
|
"url": "mailto:hello@example.com",
|
|
"target": "inline",
|
|
]) == nil)
|
|
#expect(DashboardWindowController.linkRequest(from: [
|
|
"type": "open-link",
|
|
"url": "https:hostless",
|
|
"target": "external",
|
|
]) == nil)
|
|
}
|
|
|
|
@Test func `dashboard accepts only typed window drag requests`() {
|
|
#expect(DashboardWindowController.isWindowDragRequest(["type": "window-drag"]))
|
|
#expect(!DashboardWindowController.isWindowDragRequest(["type": "open-link"]))
|
|
#expect(!DashboardWindowController.isWindowDragRequest(["type": 1]))
|
|
#expect(!DashboardWindowController.isWindowDragRequest("window-drag"))
|
|
}
|
|
|
|
@Test func `dashboard trusts only its main control path for link messages`() throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let trusted = try #require(URL(string: "http://127.0.0.1:18789/control/chat"))
|
|
let wrongPath = try #require(URL(string: "http://127.0.0.1:18789/control-room"))
|
|
let wrongPort = try #require(URL(string: "http://127.0.0.1:18790/control/"))
|
|
#expect(DashboardWindowController.isTrustedLinkSource(trusted, dashboardURL: dashboard))
|
|
#expect(!DashboardWindowController.isTrustedLinkSource(wrongPath, dashboardURL: dashboard))
|
|
#expect(!DashboardWindowController.isTrustedLinkSource(wrongPort, dashboardURL: dashboard))
|
|
#expect(!DashboardWindowController.isTrustedLinkSource(nil, dashboardURL: dashboard))
|
|
#expect(DashboardWindowController.shouldAllowEditorURLLaunch(
|
|
from: trusted,
|
|
isMainFrame: true,
|
|
dashboardURL: dashboard))
|
|
#expect(!DashboardWindowController.shouldAllowEditorURLLaunch(
|
|
from: wrongPath,
|
|
isMainFrame: true,
|
|
dashboardURL: dashboard))
|
|
#expect(!DashboardWindowController.shouldAllowEditorURLLaunch(
|
|
from: trusted,
|
|
isMainFrame: false,
|
|
dashboardURL: dashboard))
|
|
}
|
|
|
|
@Test func `dashboard link browser tabs preserve isolation and lifecycle`() throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
#expect(controller._testLinkBrowserIsCollapsed)
|
|
#expect(controller._testLinkBrowserTabCount == 0)
|
|
#expect(controller._testLinkBrowserActiveTabIndex == nil)
|
|
#expect(controller._testLinkBrowserWebViewIdentity == nil)
|
|
#expect(controller._testLinkBrowserDataStore === controller._testDashboardDataStore)
|
|
#expect(!controller._testCanOpenWindowsAutomatically)
|
|
#expect(controller._testAllWebViewsEnableTabNavigation)
|
|
#expect(controller._testLinkBrowserNavigationObservationCount == 0)
|
|
#expect(controller._testLinkBrowserTabBarIsHidden)
|
|
#expect(controller._testLinkBrowserTabBarHeight == 0)
|
|
#expect(controller._testLinkBrowserToolbarHeight == DashboardWindowLayout.linkBrowserToolbarHeight)
|
|
|
|
let urlA = try #require(URL(string: "http://127.0.0.1:1/a"))
|
|
let urlB = try #require(URL(string: "http://127.0.0.1:1/b"))
|
|
controller._testOpenLinkBrowser(urlA)
|
|
#expect(controller._testLinkBrowserTabCount == 1)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlA])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 0)
|
|
#expect(!controller._testLinkBrowserIsCollapsed)
|
|
#expect(controller._testLinkBrowserRepresentedURL == urlA)
|
|
#expect(!controller._testCanOpenWindowsAutomatically)
|
|
#expect(controller._testAllWebViewsEnableTabNavigation)
|
|
#expect(controller._testLinkBrowserTabBarIsHidden)
|
|
#expect(controller._testLinkBrowserTabBarHeight == 0)
|
|
#expect(controller._testLinkBrowserToolbarHeight == DashboardWindowLayout.linkBrowserToolbarHeight)
|
|
|
|
controller._testOpenLinkBrowser(urlB)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlA, urlB])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 1)
|
|
#expect(!controller._testLinkBrowserTabBarIsHidden)
|
|
#expect(controller._testLinkBrowserTabBarHeight == DashboardWindowLayout.linkBrowserTabBarHeight)
|
|
#expect(
|
|
controller._testLinkBrowserToolbarHeight ==
|
|
DashboardWindowLayout.linkBrowserToolbarWithTabsHeight)
|
|
controller._testOpenLinkBrowser(urlA)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlA, urlB])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 0)
|
|
|
|
controller._testLinkBrowserOpenInNewTab(urlA)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlA, urlB, urlA])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 2)
|
|
controller._testLinkBrowserSelectTab(at: 1)
|
|
controller._testLinkBrowserCloseTab(at: 1)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlA, urlA])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 1)
|
|
controller._testLinkBrowserCloseTab(at: 0)
|
|
#expect(controller._testLinkBrowserTabBarIsHidden)
|
|
#expect(controller._testLinkBrowserTabBarHeight == 0)
|
|
#expect(controller._testLinkBrowserToolbarHeight == DashboardWindowLayout.linkBrowserToolbarHeight)
|
|
controller._testLinkBrowserCloseTab(at: 0)
|
|
#expect(controller._testLinkBrowserIsCollapsed)
|
|
#expect(controller._testLinkBrowserTabCount == 0)
|
|
#expect(controller._testLinkBrowserRepresentedURL == nil)
|
|
|
|
controller._testOpenLinkBrowser(urlB)
|
|
#expect(!controller._testLinkBrowserIsCollapsed)
|
|
#expect(controller._testLinkBrowserTabCount == 1)
|
|
#expect(controller._testLinkBrowserWebViewURL == nil)
|
|
#expect(controller._testLinkBrowserHistoryIsEmpty)
|
|
#expect(controller._testLinkBrowserDelegatesAreInstalled)
|
|
#expect(controller._testLinkBrowserWebViewIsInstalled)
|
|
#expect(controller._testLinkBrowserNavigationObservationCount == 4)
|
|
#expect(controller._testLinkBrowserDataStore === controller._testDashboardDataStore)
|
|
controller._testLinkBrowserOpenInNewTab(urlA)
|
|
#expect(controller._testLinkBrowserTabCount == 2)
|
|
controller._testCloseLinkBrowser()
|
|
#expect(controller._testLinkBrowserIsCollapsed)
|
|
#expect(controller._testLinkBrowserTabCount == 0)
|
|
#expect(controller._testLinkBrowserRepresentedURL == nil)
|
|
}
|
|
|
|
@Test func `dashboard link browser opens half width and remembers resizable pane width`() throws {
|
|
#expect(!DashboardWindowLayout.dividerMoved(from: nil, to: 100))
|
|
#expect(!DashboardWindowLayout.dividerMoved(from: 100, to: 100))
|
|
#expect(DashboardWindowLayout.dividerMoved(from: 100, to: 101))
|
|
#expect(DashboardWindowLayout.linkBrowserWidth(
|
|
splitWidth: 1241,
|
|
dividerThickness: 1,
|
|
persistedWidth: nil) == 620)
|
|
#expect(DashboardWindowLayout.linkBrowserWidth(
|
|
splitWidth: 1241,
|
|
dividerThickness: 1,
|
|
persistedWidth: 400) == 400)
|
|
|
|
let defaults = UserDefaults.standard
|
|
let key = DashboardWindowLayout.linkBrowserWidthDefaultsKey
|
|
let originalValue = defaults.object(forKey: key)
|
|
defaults.removeObject(forKey: key)
|
|
defer {
|
|
if let originalValue {
|
|
defaults.set(originalValue, forKey: key)
|
|
} else {
|
|
defaults.removeObject(forKey: key)
|
|
}
|
|
}
|
|
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let link = try #require(URL(string: "http://127.0.0.1:1/half-width"))
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
controller.show()
|
|
defer { controller.closeDashboard() }
|
|
controller.window?.setContentSize(DashboardWindowLayout.windowSize)
|
|
|
|
controller._testOpenLinkBrowser(link)
|
|
let openedSplitWidth = controller._testLinkBrowserSplitWidth
|
|
let dividerThickness = controller._testLinkBrowserDividerThickness
|
|
let openedLinkBrowserWidth = controller._testLinkBrowserWidth
|
|
let expectedWidth = DashboardWindowLayout.linkBrowserWidth(
|
|
splitWidth: openedSplitWidth,
|
|
dividerThickness: dividerThickness,
|
|
persistedWidth: nil)
|
|
#expect(abs(openedLinkBrowserWidth - expectedWidth) < 1)
|
|
#expect(
|
|
openedSplitWidth - dividerThickness - openedLinkBrowserWidth >=
|
|
DashboardWindowLayout.mainBrowserMinWidth)
|
|
#expect(controller._testLinkBrowserMaximumThickness == NSSplitViewItem.unspecifiedDimension)
|
|
|
|
defaults.set(Double(openedLinkBrowserWidth + 37), forKey: key)
|
|
controller._testCompleteLinkBrowserDividerDrag()
|
|
let resizedWidth = controller._testLinkBrowserWidth
|
|
#expect(abs(CGFloat(defaults.double(forKey: key)) - resizedWidth) < 1)
|
|
#expect(abs(CGFloat(defaults.double(forKey: key)) - openedLinkBrowserWidth - 37) >= 1)
|
|
|
|
controller._testCloseLinkBrowser()
|
|
controller.window?.setContentSize(DashboardWindowLayout.windowMinSize)
|
|
controller._testOpenLinkBrowser(link)
|
|
let compactExpectedWidth = DashboardWindowLayout.linkBrowserWidth(
|
|
splitWidth: controller._testLinkBrowserSplitWidth,
|
|
dividerThickness: controller._testLinkBrowserDividerThickness,
|
|
persistedWidth: resizedWidth)
|
|
#expect(abs(controller._testLinkBrowserWidth - compactExpectedWidth) < 1)
|
|
#expect(abs(CGFloat(defaults.double(forKey: key)) - resizedWidth) < 1)
|
|
|
|
controller._testCloseLinkBrowser()
|
|
controller.window?.setContentSize(DashboardWindowLayout.windowSize)
|
|
controller._testOpenLinkBrowser(link)
|
|
let restoredExpectedWidth = DashboardWindowLayout.linkBrowserWidth(
|
|
splitWidth: controller._testLinkBrowserSplitWidth,
|
|
dividerThickness: controller._testLinkBrowserDividerThickness,
|
|
persistedWidth: resizedWidth)
|
|
#expect(abs(controller._testLinkBrowserWidth - restoredExpectedWidth) < 1)
|
|
}
|
|
|
|
@Test func `dashboard link browser reorders and closes other tabs`() throws {
|
|
let dashboard = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: dashboard,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
let urlA = try #require(URL(string: "https://127.0.0.1:1/a"))
|
|
let urlB = try #require(URL(string: "https://127.0.0.1:1/b"))
|
|
let urlC = try #require(URL(string: "https://127.0.0.1:1/c"))
|
|
controller._testOpenLinkBrowser(urlA)
|
|
controller._testOpenLinkBrowser(urlB)
|
|
controller._testOpenLinkBrowser(urlC)
|
|
controller._testLinkBrowserSelectTab(at: 1)
|
|
let activeIdentity = controller._testLinkBrowserWebViewIdentity
|
|
|
|
controller._testLinkBrowserMoveTab(from: 0, to: 2)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlB, urlC, urlA])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 0)
|
|
#expect(controller._testLinkBrowserWebViewIdentity == activeIdentity)
|
|
|
|
let menu = try #require(controller._testLinkBrowserContextMenu(forTabAt: 2))
|
|
#expect(menu.items.map(\.title) == [
|
|
"Open in Default Browser",
|
|
"Copy Link",
|
|
"Reload",
|
|
"",
|
|
"Close Tab",
|
|
"Close Other Tabs",
|
|
])
|
|
#expect(menu.items[0].isEnabled)
|
|
#expect(menu.items[1].isEnabled)
|
|
#expect(menu.items[2].isEnabled)
|
|
#expect(menu.items[4].isEnabled)
|
|
#expect(menu.items[5].isEnabled)
|
|
menu.performActionForItem(at: 5)
|
|
#expect(controller._testLinkBrowserTabURLs == [urlA])
|
|
#expect(controller._testLinkBrowserActiveTabIndex == 0)
|
|
}
|
|
|
|
@Test func `dashboard link browser calculates final drag insertion indexes`() {
|
|
let midpoints: [CGFloat] = [50, 150, 250]
|
|
let cases: [(currentIndex: Int, locationX: CGFloat, targetIndex: Int?, order: [Int])] = [
|
|
(0, 100, nil, [0, 1, 2]),
|
|
(0, 150, 1, [1, 0, 2]),
|
|
(0, 200, 1, [1, 0, 2]),
|
|
(0, 300, 2, [1, 2, 0]),
|
|
(2, 100, 1, [0, 2, 1]),
|
|
(2, 0, 0, [2, 0, 1]),
|
|
(1, 150, nil, [0, 1, 2]),
|
|
]
|
|
for testCase in cases {
|
|
let targetIndex = DashboardLinkBrowserTabBar.dropIndex(
|
|
currentIndex: testCase.currentIndex,
|
|
itemMidpoints: midpoints,
|
|
locationX: testCase.locationX)
|
|
#expect(targetIndex == testCase.targetIndex)
|
|
|
|
var order = Array(midpoints.indices)
|
|
if let targetIndex {
|
|
let moved = order.remove(at: testCase.currentIndex)
|
|
order.insert(moved, at: targetIndex)
|
|
}
|
|
#expect(order == testCase.order)
|
|
}
|
|
}
|
|
|
|
@Test func `dashboard link browser retires initial URL after later navigation`() throws {
|
|
let view = DashboardLinkBrowserView(websiteDataStore: .default())
|
|
defer { view.closeBrowser() }
|
|
let requestedURL = try #require(URL(string: "http://127.0.0.1:1/short"))
|
|
let currentURL = try #require(URL(string: "http://127.0.0.1:1/final"))
|
|
view.open(requestedURL)
|
|
let webView = try #require(view._testActiveWebView)
|
|
let initialNavigation = NSObject()
|
|
view._testStartNavigation(initialNavigation, in: webView)
|
|
view.navigationWillStart(currentURL, in: webView)
|
|
|
|
view.open(requestedURL)
|
|
#expect(view._testTabCount == 1)
|
|
#expect(view._testActiveWebView === webView)
|
|
|
|
view.open(currentURL)
|
|
#expect(view._testTabCount == 1)
|
|
#expect(view._testActiveWebView === webView)
|
|
|
|
view._testFinishNavigation(initialNavigation, at: currentURL, in: webView)
|
|
view.open(requestedURL)
|
|
#expect(view._testTabCount == 1)
|
|
#expect(view._testActiveWebView === webView)
|
|
|
|
view._testStartNavigation(NSObject(), in: webView)
|
|
view.navigationWillStart(currentURL, in: webView)
|
|
view.open(requestedURL)
|
|
#expect(view._testTabCount == 2)
|
|
#expect(view._testActiveWebView !== webView)
|
|
}
|
|
|
|
@Test func `dashboard link browser retires initial URL when navigation is replaced`() throws {
|
|
let view = DashboardLinkBrowserView(websiteDataStore: .default())
|
|
defer { view.closeBrowser() }
|
|
let requestedURL = try #require(URL(string: "http://127.0.0.1:1/short"))
|
|
let redirectURL = try #require(URL(string: "http://127.0.0.1:1/redirect"))
|
|
let replacementURL = try #require(URL(string: "http://127.0.0.1:1/replacement"))
|
|
view.open(requestedURL)
|
|
let webView = try #require(view._testActiveWebView)
|
|
view._testStartNavigation(NSObject(), in: webView)
|
|
view.navigationWillStart(redirectURL, in: webView)
|
|
view._testStartNavigation(NSObject(), in: webView)
|
|
view.navigationWillStart(replacementURL, in: webView)
|
|
|
|
view.open(requestedURL)
|
|
|
|
#expect(view._testTabCount == 2)
|
|
#expect(view._testActiveWebView !== webView)
|
|
}
|
|
|
|
@Test func `dashboard link browser retires initial URL when redirected navigation fails`() throws {
|
|
let view = DashboardLinkBrowserView(websiteDataStore: .default())
|
|
defer { view.closeBrowser() }
|
|
let requestedURL = try #require(URL(string: "http://127.0.0.1:1/short"))
|
|
let redirectURL = try #require(URL(string: "http://127.0.0.1:1/redirect"))
|
|
view.open(requestedURL)
|
|
let webView = try #require(view._testActiveWebView)
|
|
view._testStartNavigation(NSObject(), in: webView)
|
|
view.navigationWillStart(redirectURL, in: webView)
|
|
view.navigationDidFail(for: webView)
|
|
|
|
view.open(requestedURL)
|
|
|
|
#expect(view._testTabCount == 2)
|
|
#expect(view._testActiveWebView !== webView)
|
|
}
|
|
|
|
@Test func `dashboard link browser prefers current URL over initial alias`() throws {
|
|
let view = DashboardLinkBrowserView(websiteDataStore: .default())
|
|
defer { view.closeBrowser() }
|
|
let requestedURL = try #require(URL(string: "http://127.0.0.1:1/short"))
|
|
let currentURL = try #require(URL(string: "http://127.0.0.1:1/final"))
|
|
view.open(requestedURL)
|
|
let redirectedWebView = try #require(view._testActiveWebView)
|
|
view.navigationWillStart(currentURL, in: redirectedWebView)
|
|
view._testOpenInNewTab(requestedURL)
|
|
let currentWebView = try #require(view._testActiveWebView)
|
|
view._testSelectTab(at: 0)
|
|
|
|
view.open(requestedURL)
|
|
|
|
#expect(view._testTabCount == 2)
|
|
#expect(view._testActiveWebView === currentWebView)
|
|
}
|
|
|
|
@Test func `dashboard link browser menu disables URL actions for blank tab`() throws {
|
|
let view = DashboardLinkBrowserView(websiteDataStore: .default())
|
|
let url = try #require(URL(string: "http://127.0.0.1:1/blank"))
|
|
view.open(url)
|
|
let webView = try #require(view._testActiveWebView)
|
|
#expect(webView.url == nil)
|
|
view.navigationURLDidChange(for: webView)
|
|
let menu = try #require(view._testContextMenu(forTabAt: 0))
|
|
#expect(!menu.items[0].isEnabled)
|
|
#expect(!menu.items[1].isEnabled)
|
|
#expect(!menu.items[2].isEnabled)
|
|
#expect(menu.items[4].isEnabled)
|
|
#expect(!menu.items[5].isEnabled)
|
|
view.closeBrowser()
|
|
}
|
|
|
|
@Test func `dashboard new windows route by source browser`() throws {
|
|
let url = try #require(URL(string: "https://127.0.0.1:1/new"))
|
|
let fileURL = try #require(URL(string: "file:///tmp/private"))
|
|
#expect(DashboardWindowController.newWindowAction(
|
|
for: url,
|
|
sourceIsLinkBrowser: true) == .openTab(url))
|
|
#expect(DashboardWindowController.newWindowAction(
|
|
for: url,
|
|
sourceIsLinkBrowser: false) == .openExternal(url))
|
|
#expect(DashboardWindowController.newWindowAction(
|
|
for: fileURL,
|
|
sourceIsLinkBrowser: true) == .ignore)
|
|
#expect(DashboardWindowController.newWindowAction(
|
|
for: nil,
|
|
sourceIsLinkBrowser: false) == .ignore)
|
|
}
|
|
|
|
@Test func `sidebar browser reserves auxiliary schemes for subframes`() throws {
|
|
let webURL = try #require(URL(string: "https://github.com/openclaw/openclaw"))
|
|
let blankURL = try #require(URL(string: "about:blank"))
|
|
let fileURL = try #require(URL(string: "file:///tmp/private"))
|
|
let mailURL = try #require(URL(string: "mailto:hello@example.com"))
|
|
#expect(DashboardWindowController.shouldAllowBrowserNavigation(to: webURL, isMainFrame: true))
|
|
#expect(DashboardWindowController.shouldAllowBrowserNavigation(to: webURL, isMainFrame: false))
|
|
#expect(!DashboardWindowController.shouldAllowBrowserNavigation(to: blankURL, isMainFrame: true))
|
|
#expect(DashboardWindowController.shouldAllowBrowserNavigation(to: blankURL, isMainFrame: false))
|
|
#expect(!DashboardWindowController.shouldAllowBrowserNavigation(to: fileURL, isMainFrame: false))
|
|
#expect(!DashboardWindowController.shouldAllowBrowserNavigation(to: mailURL, isMainFrame: false))
|
|
}
|
|
|
|
@Test func `external pointer fallback rejects synthetic link activation`() throws {
|
|
let webURL = try #require(URL(string: "https://docs.openclaw.ai/"))
|
|
let mailURL = try #require(URL(string: "mailto:hello@example.com"))
|
|
#expect(DashboardWindowController.shouldOpenExternalDashboardNavigation(
|
|
webURL,
|
|
navigationType: .linkActivated,
|
|
buttonNumber: 1))
|
|
#expect(DashboardWindowController.shouldOpenExternalDashboardNavigation(
|
|
mailURL,
|
|
navigationType: .linkActivated,
|
|
buttonNumber: 1))
|
|
#expect(!DashboardWindowController.shouldOpenExternalDashboardNavigation(
|
|
webURL,
|
|
navigationType: .linkActivated,
|
|
buttonNumber: 0))
|
|
#expect(!DashboardWindowController.shouldOpenExternalDashboardNavigation(
|
|
mailURL,
|
|
navigationType: .other,
|
|
buttonNumber: 1))
|
|
|
|
#expect(DashboardWindowController.targetlessNavigationAction(
|
|
for: webURL,
|
|
navigationType: .linkActivated,
|
|
buttonNumber: 1,
|
|
allowEditorURLs: false) == .allow)
|
|
#expect(DashboardWindowController.targetlessNavigationAction(
|
|
for: mailURL,
|
|
navigationType: .linkActivated,
|
|
buttonNumber: 1,
|
|
allowEditorURLs: false) == .openExternal)
|
|
#expect(DashboardWindowController.targetlessNavigationAction(
|
|
for: mailURL,
|
|
navigationType: .linkActivated,
|
|
buttonNumber: 0,
|
|
allowEditorURLs: false) == .cancel)
|
|
|
|
let editorURL = try #require(URL(string: "vscode://file/workspace/src/foo.ts"))
|
|
#expect(DashboardWindowController.targetlessNavigationAction(
|
|
for: editorURL,
|
|
navigationType: .other,
|
|
buttonNumber: 0,
|
|
allowEditorURLs: true) == .openExternal)
|
|
#expect(DashboardWindowController.targetlessNavigationAction(
|
|
for: editorURL,
|
|
navigationType: .other,
|
|
buttonNumber: 0,
|
|
allowEditorURLs: false) == .cancel)
|
|
}
|
|
|
|
@Test func `dashboard origin brackets ipv6 literals`() throws {
|
|
let url = try #require(URL(string: "http://[fd12:3456:789a::1]:18789/control/"))
|
|
#expect(DashboardWindowController.originString(for: url) == "http://[fd12:3456:789a::1]:18789")
|
|
}
|
|
|
|
@Test func `dashboard log string strips token fragment`() throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:18789/control/#token=sekret")) // pragma: allowlist secret
|
|
#expect(dashboardLogString(for: url) == "http://127.0.0.1:18789/control/")
|
|
}
|
|
|
|
@Test func `dashboard native chrome clears both desktop sidebars`() throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
let chromeScript = try #require(controller._testUserScripts.first {
|
|
$0.source.contains("openclaw-native-macos-chrome")
|
|
})
|
|
|
|
// Narrow widths are styled by the Control UI's own compact drawer-row
|
|
// rules (layout.mobile.css); only the desktop sidebar surfaces need
|
|
// native padding injected here.
|
|
#expect(chromeScript.source.contains(".sidebar-shell"))
|
|
#expect(chromeScript.source.contains(".settings-sidebar__header"))
|
|
#expect(chromeScript.source.contains("min-width: 700px"))
|
|
// Keep the injected titlebar height in lockstep with the 52pt unified
|
|
// toolbar in makeWindow(); the two must match for the traffic lights and
|
|
// the hosted web buttons to share one vertical center.
|
|
#expect(chromeScript.source.contains("--openclaw-native-titlebar-height: 52px"))
|
|
#expect(!chromeScript.source.contains("max-width: 1100px"))
|
|
#expect(chromeScript.source.contains("openclaw-native-web-chrome"))
|
|
#expect(!chromeScript.source.contains("openclaw-native-nav"))
|
|
#expect(chromeScript.injectionTime == .atDocumentEnd)
|
|
#expect(chromeScript.isForMainFrameOnly)
|
|
}
|
|
|
|
@Test func `dashboard advertises web titlebar chrome before document load`() throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
let capabilityScript = try #require(controller._testUserScripts.first {
|
|
$0.source.contains("__OPENCLAW_NATIVE_WEB_CHROME__")
|
|
})
|
|
|
|
#expect(capabilityScript.injectionTime == .atDocumentStart)
|
|
#expect(capabilityScript.isForMainFrameOnly)
|
|
#expect(controller.window?.titlebarAccessoryViewControllers.isEmpty == true)
|
|
#expect(controller._testAllowsBackForwardGestures)
|
|
}
|
|
|
|
@Test func `dashboard javascript confirm alert maps actions`() {
|
|
let alert = DashboardWindowController._testJavaScriptConfirmAlert(
|
|
message: "Delete 1 session?",
|
|
host: "127.0.0.1")
|
|
|
|
#expect(alert.messageText == "OpenClaw Dashboard")
|
|
#expect(alert.informativeText.contains("127.0.0.1 is asking:"))
|
|
#expect(alert.informativeText.contains("Delete 1 session?"))
|
|
#expect(alert.buttons.map(\.title) == ["OK", "Cancel"])
|
|
#expect(DashboardWindowController._testJavaScriptConfirmResult(
|
|
for: .alertFirstButtonReturn))
|
|
#expect(!DashboardWindowController._testJavaScriptConfirmResult(
|
|
for: .alertSecondButtonReturn))
|
|
#expect(!DashboardWindowController._testJavaScriptConfirmResult(for: .cancel))
|
|
}
|
|
|
|
@Test func `dashboard failure state opens in dashboard window`() throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:18789/control/"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(gatewayUrl: nil, token: nil, password: nil))
|
|
controller.showFailure(
|
|
title: "Dashboard unavailable",
|
|
message: "Remote control tunnel failed",
|
|
detail: "Reset the remote tunnel and try again.")
|
|
#expect(controller.window?.isVisible == true)
|
|
#expect(controller.window?.styleMask.contains(.closable) == true)
|
|
#expect(!controller.canDeliverNativeCommands)
|
|
controller.closeDashboard()
|
|
}
|
|
|
|
private func makeShownController() throws -> DashboardWindowController {
|
|
let url = try #require(URL(string: "http://127.0.0.1:60001/#token=device-token"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:60001/",
|
|
token: "device-token",
|
|
password: nil))
|
|
controller.show()
|
|
return controller
|
|
}
|
|
|
|
@Test func `dashboard follows ready endpoint to a new tunnel port`() async throws {
|
|
let controller = try makeShownController()
|
|
defer { controller.closeDashboard() }
|
|
let manager = DashboardManager._testMake()
|
|
manager._testSetController(controller)
|
|
|
|
try await manager.handleEndpointState(.ready(
|
|
mode: .remote,
|
|
url: #require(URL(string: "ws://127.0.0.1:60002")),
|
|
token: "device-token",
|
|
password: nil))
|
|
|
|
#expect(controller.currentURL.absoluteString == "http://127.0.0.1:60002/#token=device-token")
|
|
let authScripts = controller._testUserScripts
|
|
.filter { $0.source.contains("__OPENCLAW_NATIVE_CONTROL_AUTH__") }
|
|
#expect(authScripts.count == 1)
|
|
// JSONSerialization escapes "/" so match on host:port, not the full origin.
|
|
#expect(authScripts.first?.source.contains("127.0.0.1:60002") == true)
|
|
#expect(authScripts.first?.source.contains("60001") == false)
|
|
}
|
|
|
|
@Test func `dashboard retires its web view while endpoint is unavailable`() async throws {
|
|
let controller = try makeShownController()
|
|
defer { controller.closeDashboard() }
|
|
let manager = DashboardManager._testMake()
|
|
manager._testSetController(controller)
|
|
let scriptsBefore = controller._testUserScripts
|
|
|
|
try await manager.handleEndpointState(.ready(
|
|
mode: .remote,
|
|
url: #require(URL(string: "ws://127.0.0.1:60001")),
|
|
token: "device-token",
|
|
password: nil))
|
|
await manager.handleEndpointState(.connecting(mode: .remote, detail: "Connecting…"))
|
|
await manager.handleEndpointState(.unavailable(mode: .remote, reason: "tunnel down"))
|
|
|
|
let replacement = try #require(manager._testController())
|
|
#expect(replacement !== controller)
|
|
#expect(replacement.currentURL == URL(string: "about:blank"))
|
|
#expect(!controller.isWindowOpen)
|
|
#expect(!replacement._testUserScripts.elementsEqual(scriptsBefore) { $0 === $1 })
|
|
}
|
|
|
|
@Test func `same URL route revision recreates dashboard without prior token`() async throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:60001/#token=route-a-device-token"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:60001/",
|
|
token: "route-a-device-token",
|
|
password: nil))
|
|
controller.show()
|
|
let authGate = DashboardRouteAuthGate(token: "route-a-device-token")
|
|
let manager = DashboardManager._testMake(
|
|
authTokenProvider: { _ in await authGate.authToken() },
|
|
routeProbe: { await authGate.probe() })
|
|
manager._testSetController(controller)
|
|
defer { manager._testController()?.closeDashboard() }
|
|
let socketURL = try #require(URL(string: "ws://127.0.0.1:60001"))
|
|
|
|
await manager.handleEndpointState(.ready(
|
|
mode: .remote,
|
|
url: socketURL,
|
|
token: nil,
|
|
password: nil,
|
|
routeRevision: 1))
|
|
let routeAController = try #require(manager._testController())
|
|
#expect(routeAController !== controller)
|
|
#expect(await authGate.probes() == 1)
|
|
|
|
await authGate.replaceToken("route-b-device-token")
|
|
await manager.handleEndpointState(.ready(
|
|
mode: .remote,
|
|
url: socketURL,
|
|
token: nil,
|
|
password: nil,
|
|
routeRevision: 2))
|
|
|
|
let routeBController = try #require(manager._testController())
|
|
#expect(routeBController !== routeAController)
|
|
#expect(!routeAController.isWindowOpen)
|
|
#expect(routeBController.currentURL.absoluteString ==
|
|
"http://127.0.0.1:60001/#token=route-b-device-token")
|
|
let scripts = routeBController._testUserScripts
|
|
.filter { $0.source.contains("__OPENCLAW_NATIVE_CONTROL_AUTH__") }
|
|
#expect(scripts.count == 1)
|
|
#expect(scripts[0].source.contains("route-b-device-token"))
|
|
#expect(!scripts[0].source.contains("route-a-device-token"))
|
|
}
|
|
|
|
@Test func `route change without fresh credential blanks prior dashboard`() async throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:60001/#token=route-a-device-token"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:60001/",
|
|
token: "route-a-device-token",
|
|
password: nil))
|
|
controller.show()
|
|
let manager = DashboardManager._testMake(
|
|
authTokenProvider: { _ in nil },
|
|
routeProbe: {})
|
|
manager._testSetController(controller)
|
|
defer { manager._testController()?.closeDashboard() }
|
|
|
|
try await manager.handleEndpointState(.ready(
|
|
mode: .remote,
|
|
url: #require(URL(string: "ws://127.0.0.1:60001")),
|
|
token: nil,
|
|
password: nil,
|
|
routeRevision: 2))
|
|
|
|
let replacement = try #require(manager._testController())
|
|
#expect(replacement !== controller)
|
|
#expect(!controller.isWindowOpen)
|
|
#expect(replacement.currentURL == URL(string: "about:blank"))
|
|
let scripts = replacement._testUserScripts
|
|
.filter { $0.source.contains("__OPENCLAW_NATIVE_CONTROL_AUTH__") }
|
|
#expect(!scripts.contains { $0.source.contains("route-a-device-token") })
|
|
}
|
|
|
|
@Test func `dashboard ignores endpoint changes while window is closed`() async throws {
|
|
let url = try #require(URL(string: "http://127.0.0.1:60001/#token=device-token"))
|
|
let controller = DashboardWindowController(
|
|
url: url,
|
|
auth: DashboardWindowAuth(
|
|
gatewayUrl: "ws://127.0.0.1:60001/",
|
|
token: "device-token",
|
|
password: nil))
|
|
let manager = DashboardManager._testMake()
|
|
manager._testSetController(controller)
|
|
|
|
try await manager.handleEndpointState(.ready(
|
|
mode: .remote,
|
|
url: #require(URL(string: "ws://127.0.0.1:60002")),
|
|
token: "device-token",
|
|
password: nil))
|
|
|
|
#expect(controller.currentURL == url)
|
|
}
|
|
}
|