From 5d9741da4c811455c2f3062450503d2f00346d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Pu=C5=A1k=C3=A1=C4=8D?= Date: Tue, 13 Oct 2026 14:29:31 +0200 Subject: [PATCH] Remove the background audio keep-alive (App Review 2.5.4) App Review rejected 1.0 (build 4) because the app declares the audio background mode but nothing audible plays in the background. That is how the keep-alive worked: it played silence through AVAudioEngine so iPadOS wouldn't suspend the app during a game's queue or stream. - Remove UIBackgroundModes: [audio], BackgroundKeepAlive and BackgroundSessionKeeper, and the "Keep game running in background" setting. - Leaving the app now always pauses media, as the setting's Off did. - StreamPhase moves to ServiceProfile.swift; switching services still asks first when a game is running, and the health checks still read the phase. - docs/background-keep-alive.md records why it existed, how it worked and options for bringing it back in an allowed form; README updated. Build number stays 5, from the restore PR this is stacked on. Co-Authored-By: Claude Opus 5.5 --- Apps/PointerLocker/BackgroundKeepAlive.swift | 82 ---------------- .../BackgroundSessionKeeper.swift | 93 ------------------- .../PointerLocker/BrowserViewController.swift | 21 ++--- .../PointerLocker/Services/FigmaProfile.swift | 2 +- .../Services/ServiceCategory.swift | 4 +- .../Services/ServiceProfile.swift | 14 ++- Apps/PointerLocker/Settings.swift | 11 --- Apps/PointerLocker/SettingsView.swift | 7 -- README.md | 16 ++-- docs/background-keep-alive.md | 88 ++++++++++++++++++ project.yml | 3 - .../ServiceProfileTests.swift | 2 +- 12 files changed, 119 insertions(+), 224 deletions(-) delete mode 100644 Apps/PointerLocker/BackgroundKeepAlive.swift delete mode 100644 Apps/PointerLocker/BackgroundSessionKeeper.swift create mode 100644 docs/background-keep-alive.md diff --git a/Apps/PointerLocker/BackgroundKeepAlive.swift b/Apps/PointerLocker/BackgroundKeepAlive.swift deleted file mode 100644 index 079c3d5..0000000 --- a/Apps/PointerLocker/BackgroundKeepAlive.swift +++ /dev/null @@ -1,82 +0,0 @@ -import AVFoundation -import os - -/// Keeps the app running in the background for a limited time, so a stream's -/// session survives a quick app switch. -/// -/// With the `audio` background mode, iPadOS doesn't suspend an app while it -/// plays audio. The page's own audio isn't enough: WebKit treats a quiet -/// stream as silent and pauses it on entering the background, and the app is -/// then suspended within seconds. So play silence natively, mixed with other -/// apps' audio, until the time is up or the app comes back. -@MainActor -final class BackgroundKeepAlive { - private let log = Logger(subsystem: "sk.icebear.mouselook", category: "KeepAlive") - private var engine: AVAudioEngine? - private var timer: Timer? - - /// Keep alive for `duration` seconds from now; `onExpire` runs if the app - /// is still in the background when the time is up. Calling it again while - /// running only resets the time limit. - func start(for duration: TimeInterval, onExpire: @escaping () -> Void) { - timer?.invalidate() - timer = nil - guard duration > 0 else { - log.info("Keep-alive off: pausing media now") - stop(releaseAudioSession: true) - return onExpire() - } - if engine == nil { startSilence() } - - log.info("Keep-alive for \(duration, format: .fixed(precision: 0)) s") - timer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in - MainActor.assumeIsolated { - self?.log.info("Keep-alive expired") - self?.stop(releaseAudioSession: true) - onExpire() - } - } - } - - private func startSilence() { - let session = AVAudioSession.sharedInstance() - if session.category != .playAndRecord { - try? session.setCategory(.playback, options: [.mixWithOthers]) - } - try? session.setActive(true) - - let engine = AVAudioEngine() - let format = engine.outputNode.inputFormat(forBus: 0) - let silence = AVAudioSourceNode(format: format) { isSilence, _, _, buffers in - for buffer in UnsafeMutableAudioBufferListPointer(buffers) { - if let data = buffer.mData { memset(data, 0, Int(buffer.mDataByteSize)) } - } - isSilence.pointee = true - return noErr - } - engine.attach(silence) - engine.connect(silence, to: engine.mainMixerNode, format: format) - do { - try engine.start() - self.engine = engine - } catch { - // Without audio iPadOS suspends us shortly anyway; nothing else to do. - log.error("Keep-alive audio failed to start: \(error.localizedDescription)") - } - } - - /// Stop keeping alive. Back in the foreground the page's own audio shares - /// the session, so it is only released when the time ran out (and media - /// is paused), letting the system suspend us promptly. - func stop(releaseAudioSession: Bool = false) { - guard engine != nil || timer != nil else { return } - timer?.invalidate() - timer = nil - engine?.stop() - engine = nil - if releaseAudioSession { - try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) - } - log.info("Keep-alive stopped") - } -} diff --git a/Apps/PointerLocker/BackgroundSessionKeeper.swift b/Apps/PointerLocker/BackgroundSessionKeeper.swift deleted file mode 100644 index d2a92d9..0000000 --- a/Apps/PointerLocker/BackgroundSessionKeeper.swift +++ /dev/null @@ -1,93 +0,0 @@ -import os -import UIKit -import WebKit - -/// Keeps a streaming session alive across an app switch (see -/// BackgroundKeepAlive for how): while queued or loading until the game -/// starts (up to 30 minutes), and while streaming for the chosen time, then -/// pauses all media so the system can suspend the app as usual. Nothing -/// happens outside a session, or when the setting is Off. The active -/// ServiceProfile's sessionPhaseScript tells the phases apart. -@MainActor -final class BackgroundSessionKeeper { - enum StreamPhase: Int { - case none = 0 // library, sign-in, … - case starting = 1 // in the queue or loading the game - case playing = 2 // a stream is playing - } - - static let startingLimit: TimeInterval = 30 * 60 - static let pollInterval: TimeInterval = 5 - - private let log = Logger(subsystem: "sk.icebear.mouselook", category: "KeepAlive") - private weak var webView: WKWebView? - private let keepAlive = BackgroundKeepAlive() - private var streamWatch: Timer? - - init(webView: WKWebView) { - self.webView = webView - } - - func didEnterBackground() { - currentPhase { [weak self] phase in - let state = UIApplication.shared.applicationState - self?.log.info("Entered background: stream phase \(phase.rawValue), app state \(state.rawValue)") - guard let self, state == .background else { return } - switch phase { - case .none: return - case _ where Settings.backgroundKeepAlive == 0: - self.webView?.pauseAllMediaPlayback() - case .playing: self.keepAliveWhilePlaying() - case .starting: - self.keepAlive.start(for: Self.startingLimit) { [weak self] in self?.stopWatchingStream() } - self.watchForStreamStart() - } - } - } - - func willEnterForeground() { - stopWatchingStream() - keepAlive.stop() - } - - /// Which phase the page's session is in right now. - func currentPhase(_ completion: @escaping (StreamPhase) -> Void) { - guard let webView else { return completion(.none) } - webView.evaluateJavaScript(ServiceProfile.current.sessionPhaseScript) { [log] result, error in - if let error { log.error("Stream phase check failed: \(error.localizedDescription)") } - completion(StreamPhase(rawValue: result as? Int ?? 0) ?? .none) - } - } - - private func keepAliveWhilePlaying() { - stopWatchingStream() - keepAlive.start(for: Settings.backgroundKeepAlive) { [weak self] in - self?.webView?.pauseAllMediaPlayback() - } - } - - /// While queued or loading in the background, switch to the playing time - /// limit once the game starts, or stop if the session goes away. - private func watchForStreamStart() { - stopWatchingStream() - streamWatch = Timer.scheduledTimer(withTimeInterval: Self.pollInterval, repeats: true) { [weak self] _ in - MainActor.assumeIsolated { - self?.currentPhase { [weak self] phase in - guard let self, UIApplication.shared.applicationState == .background else { return } - switch phase { - case .starting: break - case .playing: self.keepAliveWhilePlaying() - case .none: - self.stopWatchingStream() - self.keepAlive.stop(releaseAudioSession: true) - } - } - } - } - } - - private func stopWatchingStream() { - streamWatch?.invalidate() - streamWatch = nil - } -} diff --git a/Apps/PointerLocker/BrowserViewController.swift b/Apps/PointerLocker/BrowserViewController.swift index 9a1d3f0..46baf92 100644 --- a/Apps/PointerLocker/BrowserViewController.swift +++ b/Apps/PointerLocker/BrowserViewController.swift @@ -12,8 +12,7 @@ import WebKit /// 3. Hold Escape, ⌘., three-finger tap, app switch, navigation or the system /// dropping the lock → page gets `pointerlockchange` and we release. /// -/// The menu lives in +Menu, popups/dialogs/permissions in +WebUI, and the -/// background keep-alive in BackgroundSessionKeeper. +/// The menu lives in +Menu, popups/dialogs/permissions in +WebUI. final class BrowserViewController: UIViewController { private(set) var webView: WKWebView! /// Set by RootViewController; Settings uses it to switch services. @@ -25,7 +24,6 @@ final class BrowserViewController: UIViewController { private var crashGuard = CrashLoopGuard() private var loadFailure: LoadFailure? private let network = NWPathMonitor() - private var sessionKeeper: BackgroundSessionKeeper! private var hudFeeder: DebugHUDFeeder! #if DEBUG private var debugBridge: DebugBridge? @@ -77,7 +75,6 @@ final class BrowserViewController: UIViewController { webView.evaluateJavaScript(script) { _, _ in completion() } } - sessionKeeper = BackgroundSessionKeeper(webView: webView) hudFeeder = DebugHUDFeeder(webView: webView, bridge: bridge) { [weak self] in (self?.pageWantsLock ?? false, self?.isSystemPointerLocked ?? false) } @@ -171,8 +168,11 @@ final class BrowserViewController: UIViewController { } /// Which phase the page's session is in (none / queued-loading / playing). - func sessionPhase(_ completion: @escaping (BackgroundSessionKeeper.StreamPhase) -> Void) { - sessionKeeper.currentPhase(completion) + func sessionPhase(_ completion: @escaping (StreamPhase) -> Void) { + guard let webView else { return completion(.none) } + webView.evaluateJavaScript(ServiceProfile.current.sessionPhaseScript) { result, _ in + completion(StreamPhase(rawValue: result as? Int ?? 0) ?? .none) + } } /// Before being replaced (service switch): stop listening to the mouse, @@ -181,7 +181,6 @@ final class BrowserViewController: UIViewController { forceUnlock() bridge.invalidate() hudFeeder.stop() - sessionKeeper.willEnterForeground() // stops any keep-alive health.resetSession() #if DEBUG debugBridge?.stop() @@ -248,11 +247,11 @@ final class BrowserViewController: UIViewController { observers.append(center.addObserver(forName: UIScene.willDeactivateNotification, object: nil, queue: .main) { [weak self] _ in self?.forceUnlock() }) + // iPadOS suspends the app soon after it leaves the screen, which ends + // a game stream (docs/background-keep-alive.md has the keep-alive this + // replaced). Pause media now so nothing is cut off mid-sound. observers.append(center.addObserver(forName: UIScene.didEnterBackgroundNotification, object: nil, queue: .main) { [weak self] _ in - MainActor.assumeIsolated { self?.sessionKeeper.didEnterBackground() } - }) - observers.append(center.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in - MainActor.assumeIsolated { self?.sessionKeeper.willEnterForeground() } + MainActor.assumeIsolated { self?.webView?.pauseAllMediaPlayback() } }) } diff --git a/Apps/PointerLocker/Services/FigmaProfile.swift b/Apps/PointerLocker/Services/FigmaProfile.swift index ad39257..863cbd5 100644 --- a/Apps/PointerLocker/Services/FigmaProfile.swift +++ b/Apps/PointerLocker/Services/FigmaProfile.swift @@ -14,7 +14,7 @@ extension ServiceProfile { // asks for the lock. Everything else it needs (WebGL 2, WebAssembly // SIMD, workers, OffscreenCanvas) is in WebKit. identity: .macChrome, - // No stream to keep alive; Figma saves as you go. + // No game session; Figma saves as you go. sessionPhaseScript: "0", tagline: "Collaborative design tool", artwork: ServiceArtwork( diff --git a/Apps/PointerLocker/Services/ServiceCategory.swift b/Apps/PointerLocker/Services/ServiceCategory.swift index b20d7e9..3a76825 100644 --- a/Apps/PointerLocker/Services/ServiceCategory.swift +++ b/Apps/PointerLocker/Services/ServiceCategory.swift @@ -33,8 +33,8 @@ enum ServiceCategory: CaseIterable { } } - /// Games stream a session that must survive app switches (keep-alive, - /// voice chat); tools save as you go. + /// Games stream a session (switching services asks first, voice chat); + /// tools save as you go. var hasGameSession: Bool { self == .play } /// The services in this world, as a comma-separated list for Welcome. diff --git a/Apps/PointerLocker/Services/ServiceProfile.swift b/Apps/PointerLocker/Services/ServiceProfile.swift index 4975eab..846534b 100644 --- a/Apps/PointerLocker/Services/ServiceProfile.swift +++ b/Apps/PointerLocker/Services/ServiceProfile.swift @@ -38,10 +38,18 @@ struct BrowserIdentity: Equatable { ) } +/// Where a service's session is, read from the page by `sessionPhaseScript`. +/// A running session makes switching services ask first. +enum StreamPhase: Int { + case none = 0 // library, sign-in, … + case starting = 1 // in the queue or loading the game + case playing = 2 // a stream is playing +} + /// Everything specific to one streaming service ("engine"): where it lives, /// which browser it needs to see, and how to tell that a session is running. -/// The rest of the app (pointer lock, fullscreen emulation, mouse input, -/// background keep-alive) is shared by all of them. +/// The rest of the app (pointer lock, fullscreen emulation, mouse input) is +/// shared by all of them. /// /// Adding a service: define a profile in its own file (see /// GeForceNowProfile.swift) and list it in `all`. @@ -51,7 +59,7 @@ struct ServiceProfile { let homeURL: URL let identity: BrowserIdentity /// JavaScript evaluating to the session phase: - /// 0 none, 1 queued or loading, 2 streaming (see BackgroundSessionKeeper). + /// 0 none, 1 queued or loading, 2 streaming (see StreamPhase). let sessionPhaseScript: String /// One line under the name on the service card. let tagline: String diff --git a/Apps/PointerLocker/Settings.swift b/Apps/PointerLocker/Settings.swift index 59d0acf..78460a9 100644 --- a/Apps/PointerLocker/Settings.swift +++ b/Apps/PointerLocker/Settings.swift @@ -69,17 +69,6 @@ enum Settings { set { defaults.set(newValue.rawValue, forKey: "microphone") } } - /// How long a stream keeps running after the app goes to the background - /// (e.g. to answer a message), in seconds; 0 pauses it right away. - static var backgroundKeepAlive: TimeInterval { - get { defaults.object(forKey: "backgroundKeepAlive") as? TimeInterval ?? 300 } - set { defaults.set(newValue, forKey: "backgroundKeepAlive") } - } - - static let backgroundKeepAlivePresets: [(title: String, seconds: TimeInterval)] = [ - ("Off", 0), ("1 minute", 60), ("5 minutes", 300), ("15 minutes", 900), - ] - /// Show the debug overlay (lock state, input, stream stats). static var debugHUD: Bool { get { defaults.bool(forKey: "debugHUD") } diff --git a/Apps/PointerLocker/SettingsView.swift b/Apps/PointerLocker/SettingsView.swift index 4745848..57a9d19 100644 --- a/Apps/PointerLocker/SettingsView.swift +++ b/Apps/PointerLocker/SettingsView.swift @@ -13,7 +13,6 @@ struct SettingsView: View { @State private var sensitivity = Settings.sensitivity @State private var invertY = Settings.invertY - @State private var keepAlive = Settings.backgroundKeepAlive @State private var microphone = Settings.microphone @State private var useIdentity = Settings.useServiceIdentity @State private var debugHUD = Settings.debugHUD @@ -45,16 +44,11 @@ struct SettingsView: View { } if service.category.hasGameSession { Section { - Picker("Keep game running in background", selection: $keepAlive) { - ForEach(Settings.backgroundKeepAlivePresets, id: \.seconds) { Text($0.title).tag($0.seconds) } - } Picker("Microphone", selection: $microphone) { ForEach(MicrophoneAccess.allCases, id: \.self) { Text($0.title).tag($0) } } } header: { Text("Game session") - } footer: { - Text("Keeping the game running lets you switch apps briefly without losing your session.") } } Section { @@ -110,7 +104,6 @@ struct SettingsView: View { } .onChange(of: sensitivity) { Settings.sensitivity = sensitivity } .onChange(of: invertY) { Settings.invertY = invertY } - .onChange(of: keepAlive) { Settings.backgroundKeepAlive = keepAlive } .onChange(of: microphone) { Settings.microphone = microphone } .onChange(of: useIdentity) { Settings.useServiceIdentity = useIdentity } .onChange(of: debugHUD) { Settings.debugHUD = debugHUD } diff --git a/README.md b/README.md index 26c3997..c987148 100644 --- a/README.md +++ b/README.md @@ -128,19 +128,16 @@ NOW, Figma): pick one to switch straight to it. Each service reopens the page yo were last on, such as the open Figma file; a running game asks before you leave. Below that are Back, Reload, Home, Open URL and **Settings…**: switch service (with the setup guide), show the setup guide again, mouse sensitivity, invert -Y-axis, background keep-alive, microphone, browser identity, the debug overlay, +Y-axis, microphone, browser identity, the debug overlay, service checks, and About (version, links to the website and this repository). The button hides while the pointer is locked. **Switching apps mid-game.** iPadOS suspends an app within seconds of leaving -it, which drops the stream and ends the session. Mouselook keeps itself -running in the background during a session. While you wait in the queue or the -game loads, it keeps running until the game starts, for up to 30 minutes. While -a stream plays, it keeps running for 5 minutes by default (⋯ ▸ Settings… ▸ Keep game -running in background: Off / 1 / 5 / 15 minutes). It does this by playing silence, -since WebKit pauses a quiet stream in the background. When the time is up it -pauses the stream so the system can suspend the app. GeForce NOW still needs a -tap on Resume when you come back. +it, which drops the stream, so leaving Mouselook mid-game can end the session. +Mouselook pauses media when you leave. Earlier builds kept the app running in +the background by playing silence; App Review doesn't allow that (guideline +2.5.4), so it was removed. [docs/background-keep-alive.md](docs/background-keep-alive.md) +has how it worked, in case it comes back in an allowed form. **Volume won't go to zero?** If you allowed the microphone, WebKit uses the voice-call audio mode, in which iPadOS keeps a minimum volume. Set ⋯ ▸ Settings… ▸ @@ -227,7 +224,6 @@ builds and tests the app. | `Resources/pointerlock-polyfill.js` | Pointer Lock and Fullscreen APIs in the page. It applies the profile's identity, including inside Web Workers, and turns native input into DOM events. | | `MouseBridge.swift` | Raw GCMouse input to the page, one batch per frame, never more than one in flight. | | `BrowserViewController*.swift` | The web view and lock state (+Menu, +WebUI for popups, dialogs and permissions). | -| `BackgroundSessionKeeper.swift`, `BackgroundKeepAlive.swift` | Keeping a session alive across app switches. | | `PageRecovery.swift`, `StatusOverlayView.swift` | Crashes, failed loads and going offline. | | `DebugHUDFeeder.swift`, `Resources/debug-hud.js`, `DebugBridge.swift` | The debug overlay, and the Mac-side JavaScript console (Debug builds only). | diff --git a/docs/background-keep-alive.md b/docs/background-keep-alive.md new file mode 100644 index 0000000..f50f4c5 --- /dev/null +++ b/docs/background-keep-alive.md @@ -0,0 +1,88 @@ +# Background keep-alive (removed in build 5) + +Builds 1–4 kept Mouselook running for a while after you switched away from a +game, so a GeForce NOW session survived a quick look at another app. App +Review rejected 1.0 (build 4) under guideline 2.5.4 on 13 October 2026, and +build 5 removed the feature. This note records why it existed, how it worked +and what we learned, in case it comes back in a form App Review accepts. + +## Why it was there + +iPadOS suspends an app a few seconds after it leaves the screen. For a cloud +game that drops the stream and can end the session; even with the +keep-alive, GeForce NOW needed a tap on Resume on return. The worst case was +losing your place in a queue of up to 30 minutes to answer a message. + +## How it worked + +Two files, both deleted in build 5: + +- `Apps/PointerLocker/BackgroundKeepAlive.swift` played **silence** natively: + an `AVAudioEngine` with an `AVAudioSourceNode` that filled every buffer with + zeros and set `isSilence`. The audio session was `.playback` with + `.mixWithOthers` (or left as `.playAndRecord` when the microphone was in + use). With `UIBackgroundModes: [audio]` in `project.yml`, iPadOS doesn't + suspend an app while its audio session plays, even silently. A timer + stopped it after a set time and released the audio session. +- `Apps/PointerLocker/BackgroundSessionKeeper.swift` decided when. On + `UIScene.didEnterBackgroundNotification` it read the page's session phase + through the service profile's `sessionPhaseScript`: + - **none** (library, sign-in): nothing; the app suspends as usual; + - **starting** (queued or loading): keep alive until the game starts, up to + 30 minutes, checking the phase every 5 s; + - **playing**: keep alive for the chosen time, then + `WKWebView.pauseAllMediaPlayback()` so the system could suspend the app. + + Coming back to the foreground stopped everything. + +Setting: ⋯ ▸ Settings… ▸ Game session ▸ "Keep game running in background": +Off / 1 / 5 / 15 minutes, default 5, stored as `backgroundKeepAlive` in +`UserDefaults`. Off paused media on leaving, as build 5 now always does. + +### Why silence and not the game's own audio + +WebKit treats a quiet stream as silent and pauses it when the app goes to the +background, and the app is then suspended within seconds. So the page's audio +alone didn't keep the app running, and the app had to hold the audio session +itself. That is exactly what 2.5.4 forbids: the reviewer heard nothing in the +background, because by design there was usually nothing to hear, above all +during the queue. + +## What testing showed + +- Simulator (27 September 2026): 90 s in the background with keep-alive, the + stream kept playing and the session continued on return. The simulator never + suspends backgrounded apps, so suspension after the time limit was never + confirmed there. +- Not measured: how long GeForce NOW holds a session when the app is + suspended, i.e. what users lose without the keep-alive. Worth testing on a + device before deciding to bring it back. + +## What stays in the app + +`StreamPhase` (in `ServiceProfile.swift`) and each profile's +`sessionPhaseScript`: switching services asks first when a game is running, +and the health checks read the phase too. + +## If we revisit it + +Options that could pass 2.5.4, none tested: + +- **Audible content only.** Keep the `audio` mode only while the game's own + audio is actually audible in the background, and never during the queue. + Needs a device test of whether WebKit keeps an audible WebRTC stream playing + in the background, and a Home Screen screen recording for App Review. +- **Picture in Picture** of the stream, if WebKit allows PiP for a MediaStream + video. Picture in Picture is a legitimate use of the same background mode. +- **Faster recovery instead of staying alive:** make coming back to a dropped + session quick (reload straight into GeForce NOW's resume flow). + +To get the old code back: + +```bash +git log --diff-filter=D --format=%h -- Apps/PointerLocker/BackgroundKeepAlive.swift +``` + +then `git show ^:Apps/PointerLocker/BackgroundKeepAlive.swift` (and the +same for `BackgroundSessionKeeper.swift`, `Settings.swift`, `SettingsView.swift` +and `project.yml`). diff --git a/project.yml b/project.yml index 3b7ade6..d1814bf 100644 --- a/project.yml +++ b/project.yml @@ -61,9 +61,6 @@ targets: - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown NSMicrophoneUsageDescription: For voice chat in games. The app asks before a site can use it. - # Keeps a stream alive for a while in the background (⋯ ▸ Settings… ▸ - # Keep game running in background); the app pauses media when that time is up. - UIBackgroundModes: [audio] # Only used by Debug builds (DebugBridge) to reach a developer Mac; the # "Remove debug-only Info.plist keys" phase deletes these four from # every other configuration. diff --git a/tests/PointerLockerTests/ServiceProfileTests.swift b/tests/PointerLockerTests/ServiceProfileTests.swift index 9f290cd..7cecda1 100644 --- a/tests/PointerLockerTests/ServiceProfileTests.swift +++ b/tests/PointerLockerTests/ServiceProfileTests.swift @@ -37,7 +37,7 @@ final class ServiceProfileTests: XCTestCase { XCTAssertTrue(identity.desktopContentMode) } - func testFigmaHasNoGameSessionToKeepAlive() { + func testFigmaHasNoGameSession() { XCTAssertEqual(ServiceProfile.figma.sessionPhaseScript, "0") }