diff --git a/.github/workflows/release-readiness.yml b/.github/workflows/release-readiness.yml index c5d7665..d0b967b 100644 --- a/.github/workflows/release-readiness.yml +++ b/.github/workflows/release-readiness.yml @@ -40,3 +40,36 @@ jobs: xcrun simctl list runtimes - run: ./Scripts/release-readiness.sh - run: ./Scripts/ipad-icloud-tests.sh + - name: Report XCTest results + if: always() + run: | + python3 - <<'PY' + import glob + import json + import subprocess + + for root in ("/tmp/scriptwidget-release-readiness", "/tmp/scriptwidget-ipad-icloud-tests"): + for path in sorted(glob.glob(f"{root}/*/Logs/Test/*.xcresult")): + print(f"XCTest result: {path}", flush=True) + result = subprocess.run( + ["xcrun", "xcresulttool", "get", "test-results", "summary", "--path", path], + capture_output=True, text=True, + ) + if result.returncode: + print(result.stderr, flush=True) + continue + summary = json.loads(result.stdout) + print(json.dumps({key: summary.get(key) for key in ( + "title", "result", "passedTests", "failedTests", "skippedTests", "testFailures" + )}, indent=2), flush=True) + PY + - name: Upload XCTest diagnostics + if: always() + uses: actions/upload-artifact@v7 + with: + name: xctest-results-${{ github.run_attempt }} + path: | + /tmp/scriptwidget-release-readiness/*/Logs/Test/ + /tmp/scriptwidget-ipad-icloud-tests/*/Logs/Test/ + if-no-files-found: warn + retention-days: 7 diff --git a/Scripts/macos-widget-link-tests.sh b/Scripts/macos-widget-link-tests.sh new file mode 100644 index 0000000..3b2b6ca --- /dev/null +++ b/Scripts/macos-widget-link-tests.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +TEST_DIR=$(mktemp -d /tmp/scriptwidget-link-tests.XXXXXX) +trap 'rm -rf "$TEST_DIR"' EXIT + +swiftc -module-cache-path "$TEST_DIR/module-cache" \ + "$ROOT/macOS/ScriptWidgetMac/App/AppDelegate.swift" \ + "$ROOT/Shared/ScriptWidgetRuntime/Widget/Runtime/DeepLinkDefine.swift" \ + "$ROOT/Tests/MacWidgetLinkTests/main.swift" \ + -o "$TEST_DIR/widget-link-tests" +"$TEST_DIR/widget-link-tests" diff --git a/Scripts/release-readiness.sh b/Scripts/release-readiness.sh index 4b246fc..b2eeb3a 100755 --- a/Scripts/release-readiness.sh +++ b/Scripts/release-readiness.sh @@ -13,6 +13,7 @@ git diff --exit-code -- \ macOS/ScriptWidgetMac/StudioEditor.bundle if [ "${SCRIPTWIDGET_SKIP_XCODE:-0}" != "1" ]; then + sh Scripts/macos-widget-link-tests.sh xcodebuild -quiet -project macOS/ScriptWidgetMac.xcodeproj -scheme ScriptWidgetRuntimeTests \ -destination 'platform=macOS' -derivedDataPath "$DERIVED_DATA/macos-tests" \ CODE_SIGNING_ALLOWED=NO test diff --git a/Tests/MacWidgetLinkTests/main.swift b/Tests/MacWidgetLinkTests/main.swift new file mode 100644 index 0000000..22cb368 --- /dev/null +++ b/Tests/MacWidgetLinkTests/main.swift @@ -0,0 +1,32 @@ +import AppKit +import Foundation + +// Isolate the app delegate from iCloud caching for this routing regression test. +final class StubScriptManager { + func precacheAllScripts() {} +} +let sharedScriptManager = StubScriptManager() + +let delegate = AppDelegate() +var openedURLs: [URL] = [] +delegate.openWidgetURL = { openedURLs.append($0) } + +let externalURLs = [ + URL(string: "https://xnu.app/scriptwidget")!, + URL(string: "scriptable:///run/Name")!, + URL(fileURLWithPath: "/tmp/widget-card.html"), + URL(string: "mailto:example@example.com")! +] +let internalURLs = [ + kDeepLinkDefaultURL, + URL(string: "WIDGET-DEEPLINK://default")!, + URL(string: "scriptwidget://open")!, + URL(string: "SCRIPTWIDGET://open")!, + URL(string: "relative/path")! +] + +delegate.application(NSApplication.shared, open: internalURLs + externalURLs) +precondition(openedURLs == externalURLs, "Forward external URLs in order and ignore internal/schemeless URLs") +delegate.application(NSApplication.shared, open: []) +precondition(openedURLs == externalURLs, "An empty URL batch should do nothing") +print("✓ macOS widget URL routing passed") diff --git a/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift b/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift index 6051f00..f414258 100644 --- a/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift +++ b/Tests/ScriptWidgetRuntimeTests/RuntimeExecutionTests.swift @@ -260,7 +260,16 @@ final class RuntimeExecutionTests: XCTestCase { } } - func testConcurrentRuntimesKeepEnvironmentStateIsolated() { + func testConcurrentRuntimesKeepEnvironmentStateIsolated() throws { + let source = "$render({$getenv(\"widget-param\")});" + // Exercise concurrent environment isolation without racing eight cold + // Babel compilations of identical source on constrained CI simulators. + // Each concurrent execution still gets its own package and JSContext. + let warmup = makeRuntime(environments: ["widget-size": "medium", "widget-param": "warmup"]) + .executeJSXSyncForWidget(source) + XCTAssertNil(warmup.1) + XCTAssertEqual(collectText(try XCTUnwrap(warmup.0)), "warmup") + let lock = NSLock() var rendered: [Int: String] = [:] var failures: [String] = [] @@ -268,9 +277,7 @@ final class RuntimeExecutionTests: XCTestCase { DispatchQueue.concurrentPerform(iterations: 8) { index in let token = "runtime-\(index)" let runtime = makeRuntime(environments: ["widget-size": "medium", "widget-param": token]) - let (element, error) = runtime.executeJSXSyncForWidget( - "$render({$getenv(\"widget-param\")});" - ) + let (element, error) = runtime.executeJSXSyncForWidget(source) lock.lock() defer { lock.unlock() } if let error { diff --git a/macOS/ScriptWidgetMac/App/AppDelegate.swift b/macOS/ScriptWidgetMac/App/AppDelegate.swift index faa64df..546c208 100644 --- a/macOS/ScriptWidgetMac/App/AppDelegate.swift +++ b/macOS/ScriptWidgetMac/App/AppDelegate.swift @@ -9,6 +9,19 @@ import Foundation import AppKit class AppDelegate: NSObject, NSApplicationDelegate { + var openWidgetURL: (URL) -> Void = { url in + NSWorkspace.shared.open(url) + } + + func application(_ application: NSApplication, open urls: [URL]) { + for url in urls { + guard let scheme = url.scheme?.lowercased(), + scheme != kDeepLinkDefaultScheme, + scheme != "scriptwidget" else { continue } + openWidgetURL(url) + } + } + func applicationDidFinishLaunching(_ notification: Notification) { print("did finish launching") // The embedded editor is served via a WKURLSchemeHandler — no diff --git a/macOS/ScriptWidgetMac/Create/CreateGuideView.swift b/macOS/ScriptWidgetMac/Create/CreateGuideView.swift index 80c350b..884417c 100644 --- a/macOS/ScriptWidgetMac/Create/CreateGuideView.swift +++ b/macOS/ScriptWidgetMac/Create/CreateGuideView.swift @@ -136,14 +136,10 @@ struct CreateGuideView: View { } } label: { HStack(spacing: 12) { - Image(systemName: "sparkles") - .font(.title) - .foregroundStyle(.white) + Image(systemName: "sparkle") + .font(.system(size: 32, weight: .light)) + .foregroundStyle(.primary) .frame(width: 44, height: 44) - .background(LinearGradient(colors: [.purple, .blue], - startPoint: .topLeading, - endPoint: .bottomTrailing)) - .clipShape(RoundedRectangle(cornerRadius: 10)) VStack(alignment: .leading, spacing: 2) { Text("Generate with AI").font(.headline) Text("Describe your widget and let the AI build it.")