Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ Run the model suite without that bridge using `python3 scripts/test-swift-model-
It builds `LogseqModelTests` and launches Swift Testing directly.
Run the native UI suite in a simulator app host with `python3 scripts/test-swift-ui-suite.py`
after building the simulator core. This also invokes Swift Testing directly.
For recording Live Activities, build with `LOGSEQ_IOS_ENABLE_TESTING=1
scripts/build-mobile-ios-simulator.sh`, then run
`python3 scripts/test-ios-recording-live-activity.py`. It checks the system's
archived lock-screen and Dynamic Island layouts, including timer dimensions.
For iPhone Duo, build with Xcode 27.1 or later. Custom navigation controls follow
the system toolbar axis, including the vertical bar on the outer display.
Collapsed photo, audio recording, search and capture actions stay in a shared
Expand Down
9 changes: 7 additions & 2 deletions apple/Sources/Logseq/RecordingActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ struct RecordingActivityTimer: View {
var body: some View {
Group {
if state.isRecording {
Text(timerInterval: attributes.timerRange, countsDown: false)
// A live timer has no finite ideal width in WidgetKit's
// archived renderer. Let static text size the row instead.
Text("00:00")
.hidden()
.overlay(alignment: .trailing) {
Text(timerInterval: attributes.timerRange, countsDown: false, showsHours: false)
}
} else {
Text(String(format: "%02d:%02d", state.elapsedSeconds / 60, state.elapsedSeconds % 60))
}
}
.monospacedDigit()
.fixedSize()
}
}

Expand Down
11 changes: 11 additions & 0 deletions apple/Tests/UI/recording-live-activity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ import UIKit
guard let active = Activity<RecordingActivityAttributes>.activities.first(where: { $0.attributes.startedAt >= started }) else {
preconditionFailure("The recording Live Activity was not registered with the widget extension")
}
print("SYSTEM_ACTIVITY_ID=\(active.id)")
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
if ProcessInfo.processInfo.environment["LOGSEQ_ACTIVITY_SYSTEM_RENDER"] == "1" {
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "Recording activity rendering")
// Simulator-only probe: show the system-hosted activity rather
// than relying solely on an in-process SwiftUI image preview.
_ = UIApplication.shared.perform(NSSelectorFromString("suspend"))
}
defer {
if backgroundTask != .invalid { UIApplication.shared.endBackgroundTask(backgroundTask) }
}
defer { live.end() }
live.update(elapsedSeconds: 2, isRecording: true, transcript: "First words")
live.update(elapsedSeconds: 3, isRecording: true, transcript: "Latest words")
Expand Down
74 changes: 74 additions & 0 deletions scripts/test-ios-recording-live-activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Check real WidgetKit archived layout, which an ImageRenderer preview cannot validate."""
import math
import os
from pathlib import Path
import re
import signal
import subprocess
import sys

ROOT = Path(__file__).resolve().parents[1]


def validate_render(text, activity_id):
entries = re.split(r"(?m)(?=^\d{4}-\d{2}-\d{2} \d{2}:)", text)
rendered = [entry for entry in entries
if f"[activity:{activity_id}:" in entry
and "Rendering view: AnyView(_ArchivedView" in entry]
for presentation in ("listItem", "dynamicIsland-compactTrailing"):
if not any(f"-{presentation}] Rendering view:" in entry for entry in rendered):
raise AssertionError(f"The system did not render the recording {presentation}")
for entry in rendered:
frame = re.search(r"\(frame \(([^)]+)\)\)", entry)
if frame is None:
raise AssertionError("The system-rendered activity has no layout frame")
values = frame[1].replace(";", " ").split()
if not all(math.isfinite(float(value)) for value in values):
raise AssertionError(f"Invalid system activity layout: {frame[0]}")
Comment on lines +23 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Timer visibility remains untested

validate_render rejects nonfinite frames but accepts zero-sized ones. The check can pass even when the timer occupies no visible space.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

return len(rendered)


def main():
simulator = os.environ.get("LOGSEQ_IOS_SIMULATOR", "booted")
artifacts = ROOT / "_build/recording-lockscreen-review"
artifacts.mkdir(parents=True, exist_ok=True)
render_log = artifacts / "system-render.log"
check_log = artifacts / "system-check.log"
environment = os.environ.copy()
environment.update({
"LOGSEQ_RUNTIME_CHECK_WIDGETS": "1",
"SIMCTL_CHILD_LOGSEQ_ACTIVITY_SYSTEM_RENDER": "1",
})
with render_log.open("w") as output:
logger = subprocess.Popen([
"xcrun", "simctl", "spawn", simulator, "log", "stream", "--level", "debug",
"--style", "compact", "--predicate", 'process == "WidgetRenderer_Activities"',
], stdout=output, stderr=subprocess.STDOUT)
try:
result = subprocess.run([
sys.executable, str(ROOT / "apple/Tests/UI/run-simulator-runtime-check.py"),
str(ROOT / "apple/Tests/UI/recording-live-activity.swift"), "CheckRecordingLiveActivity",
], env=environment, capture_output=True, text=True, timeout=90)
check_log.write_text(result.stdout + result.stderr)
if result.returncode:
raise SystemExit(f"Live Activity runtime check failed; see {check_log}")
finally:
logger.send_signal(signal.SIGINT)
try:
logger.wait(timeout=5)
except subprocess.TimeoutExpired:
logger.kill()
logger.wait()
activity = re.search(r"SYSTEM_ACTIVITY_ID=([^\s]+)", check_log.read_text())
if activity is None:
raise SystemExit(f"The runtime check did not return an activity ID; see {check_log}")
try:
count = validate_render(render_log.read_text(), activity[1])
except AssertionError as error:
raise SystemExit(f"{error}; see {render_log}") from None
print(f"PASS: {count} system-rendered recording views have finite layout frames, including the lock-screen card and compact timer")


if __name__ == "__main__":
main()
Loading