Almedia.Initialize works inside the Unity Editor by falling back to a deterministic mock bridge that simulates the native layer. The mock fires OnStatusChanged / OnNotificationsReceived on a timer, so the UI can be exercised without a device build. It also simulates the screen lifecycle pair for every screen, modeling the webview linking strategy - starting linking from the popup’s CTA fires OnScreenPresented/OnScreenDismissed around the simulated flow so pause/resume wiring is exercisable in the editor, whereas production configured with the system-browser linking strategy fires no pair for linking.
Static state and event subscribers are cleared on domain reload (RuntimeInitializeOnLoadMethod with SubsystemRegistration timing). This means:
- Manual unsubscription in
OnDestroyis not required to avoid leaking handlers across play-mode entry and exit - domain reload clears them. Within a single play session you must still unsubscribe, though: aMonoBehaviourthat subscribes to anAlmediaevent and is then destroyed (scene unload,Destroy(gameObject)) stays registered. The next callback fires on the destroyed object and throwsMissingReferenceException, which the bridge swallows toOnLog- it does not surface throughOnErrorOccurred. Unsubscribe inOnDisable/OnDestroyfor any component shorter-lived than the SDK. - Even with domain reload disabled in Edit → Project Settings → Editor → Enter Play Mode Options, subscriptions should still happen in
Awake, because the SDK explicitly resets its own state on the same lifecycle hook.
Driving non-happy paths
EditorMock auto-simulate covers the happy path only:Eligible after Initialize, then Linked + OnLinkCompleted and a first progress snapshot after linking starts, then three mock notifications and a new snapshot when FetchNotifications is called. To exercise the rest of the surface - NotAvailable / Blocked / Disabled statuses, every AlmediaErrorCode, empty or oversized notification batches, native log forwarding - reach for AlmediaEditorMock. It lets host editor code and play-mode tests drive the SDK into any state in one line, so UI branches keyed off those signals become Editor-testable without a device build.
The public surface lives in AlmediaSDK.Editor.Testing and consists of these static methods:
EmitStatus(AlmediaStatus)- delivers a status as if native had reported it;Almedia.StatusandOnStatusChangedreflect it immediately. Build the status with its constructor:EmitStatus(new AlmediaStatus.Linked(false, false))is a linked player with nothing to open, andEmitStatus(new AlmediaStatus.NotAvailable("holdout"))is a holdout player.EmitError(AlmediaErrorCode, string)- delivers an error;OnErrorOccurredfires with the matching code and message.EmitLinkCompleted()- firesOnLinkCompletedwith the current UTC timestamp.EmitNotifications(params AlmediaNotification[])- firesOnNotificationsReceived. Pass no arguments for an empty batch; pass many to test scrolling / paging.EmitInGameRewardGrant(string id, params AlmediaInGameReward[])- firesOnInGameRewardGrantRequested. The id is optional (an overload generates one); pass the same id twice to reproduce an at-least-once redelivery. A call with no rewards is dropped by the SDK as malformed.EmitProgress(AlmediaProgress)- applies a snapshot.Progressholds it andOnProgressUpdatedfires.nullmodels the native SDK clearing the snapshot. A nullUsernamemodels a snapshot that clears the previous username.EmitTaskCompleted(string id, AlmediaCompletedTask)/EmitBalanceChanged(string id, AlmediaRewardPoints balance, AlmediaRewardPoints change)- fire the two progress events. The id is optional. Pass the same id twice to reproduce a replay.EmitScreenPresented(AlmediaScreen)- firesOnScreenPresentedfor the given screen, without a real webview.EmitScreenDismissed(AlmediaScreen, AlmediaInAppScreenResult)- firesOnScreenDismissedfor the given screen and result.EmitNativeLog(AlmediaLogLevel, string)- delivers a forwarded log line through the same path the iOS/Android plugins use.CancelPending()- stops any pending auto-simulate coroutine started before manual mode flipped.
Initialize, linking start and FetchNotifications becomes a no-op so the test owns the scenario). The first call to any AlmediaEditorMock method flips manual mode for the rest of the play session and cancels any in-flight canned coroutine, so an EmitStatus(new AlmediaStatus.Blocked()) issued right after Almedia.Initialize cannot be clobbered by the delayed Eligible. Manual mode resets on domain reload (entering Play mode, recompiling) - there is no public toggle to leave it manually.
Pre-init contract. Calling any Emit* method before Almedia.Initialize(...) throws InvalidOperationException. The throw is deliberate: it surfaces test ordering bugs at test-author time instead of swallowing them as a silent no-op.
Stripping. AlmediaEditorMock lives in the AlmediaLink.Editor assembly definition with includePlatforms:["Editor"]. It is not compiled for iOS or Android player targets at the assembly level - host code that references it must be wrapped in #if UNITY_EDITOR, and the references will fail to compile on a player target rather than crash at runtime.
A minimal play-mode test that verifies UI hides on Blocked: