diff --git a/packages/core/src/SystemInfo.ts b/packages/core/src/SystemInfo.ts index 7c2cb25dd..15f1d044a 100644 --- a/packages/core/src/SystemInfo.ts +++ b/packages/core/src/SystemInfo.ts @@ -1,8 +1,8 @@ +import { AssetPromise } from "./asset/AssetPromise"; import { GLCapabilityType } from "./base/Constant"; import { Engine } from "./Engine"; import { Platform } from "./Platform"; import { TextureFormat } from "./texture"; -import { AssetPromise } from "./asset/AssetPromise"; /** * Access operating system, platform and hardware information. @@ -28,6 +28,16 @@ export class SystemInfo { return window.devicePixelRatio; } + private static _parseAppleMobileOSVersion(userAgent: string, osPrefix: string): string { + // Since iOS 26, Safari freezes UA OS version at 18.6, so Version/xx is more reliable + // Use Version/ if available, otherwise fallback to OS version + let v = userAgent.match(/Version\/(\d+)(?:\.(\d+))?(?:\.(\d+))?/); + if (v) return `${osPrefix} ${v[1]}.${v[2] || 0}.${v[3] || 0}`; + + v = userAgent.match(/OS (\d+)_(\d+)(?:_(\d+))?/); + return v ? `${osPrefix} ${v[1]}.${v[2]}.${v[3] || 0}` : osPrefix; + } + /** * @internal */ @@ -53,12 +63,10 @@ export class SystemInfo { let v: RegExpMatchArray; switch (SystemInfo.platform) { case Platform.IPhone: - v = userAgent.match(/OS (\d+)_?(\d+)?_?(\d+)?/); - this.operatingSystem = v ? `iPhone OS ${v[1]}.${v[2] || 0}.${v[3] || 0}` : "iPhone OS"; + this.operatingSystem = this._parseAppleMobileOSVersion(userAgent, "iPhone OS"); break; case Platform.IPad: - v = userAgent.match(/OS (\d+)_?(\d+)?_?(\d+)?/); - this.operatingSystem = v ? `iPad OS ${v[1]}.${v[2] || 0}.${v[3] || 0}` : "iPad OS"; + this.operatingSystem = this._parseAppleMobileOSVersion(userAgent, "iPad OS"); break; case Platform.Android: v = userAgent.match(/Android (\d+).?(\d+)?.?(\d+)?/); diff --git a/tests/src/core/base/SystemInfo.test.ts b/tests/src/core/base/SystemInfo.test.ts new file mode 100644 index 000000000..3f57a9bbf --- /dev/null +++ b/tests/src/core/base/SystemInfo.test.ts @@ -0,0 +1,136 @@ +import { Platform, SystemInfo } from "@galacean/engine-core"; +import { describe, expect, it, beforeEach, afterEach } from "vitest"; + +// Cast to access internal methods +const SystemInfoInternal = SystemInfo as typeof SystemInfo & { + _initialize(): void; +}; + +describe("SystemInfo", () => { + let originalUserAgent: string; + + beforeEach(() => { + originalUserAgent = navigator.userAgent; + }); + + afterEach(() => { + // Restore original userAgent + Object.defineProperty(navigator, "userAgent", { + value: originalUserAgent, + configurable: true + }); + // Reset SystemInfo state + SystemInfo.platform = Platform.Unknown; + SystemInfo.operatingSystem = ""; + }); + + const mockUserAgent = (ua: string) => { + Object.defineProperty(navigator, "userAgent", { + value: ua, + configurable: true + }); + }; + + describe("_parseAppleMobileOSVersion for iPhone", () => { + // iOS 26+ Safari: UA freezes OS version at 18.6, use Version/xx to infer real iOS version + it("Safari on iOS 26 (UA frozen at 18.6)", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 26.0.0"); + }); + + it("Safari on iOS 26.1.2 (UA frozen at 18.6)", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.1.2 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 26.1.2"); + }); + + // Chrome on iOS: OS version is accurate + it("Chrome on iOS 26", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/138.0.7204.119 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 26.0.0"); + }); + + // Firefox on iOS: OS version is accurate + it("Firefox on iOS 26", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/140.2 Mobile/15E148 Safari/605.1.15" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 26.0.0"); + }); + + // Edge on iOS: Use Version/ (may lose patch version, but acceptable) + it("Edge on iOS 26", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 EdgiOS/46.3.30 Mobile/15E148 Safari/605.1.15" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 26.0.0"); + }); + + // Opera on iOS: OS version is accurate + it("Opera on iOS 26", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 OPiOS/16.0.14 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 26.0.0"); + }); + + // Legacy Safari (before iOS 26): OS version is accurate + it("Safari on iOS 18.4", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 18.4.0"); + }); + + // WebView or in-app browser: No Version/, fallback to OS version + it("WebView on iOS (no Version/)", () => { + mockUserAgent( + "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPhone); + expect(SystemInfo.operatingSystem).to.eq("iPhone OS 18.6.0"); + }); + }); + + describe("_parseAppleMobileOSVersion for iPad", () => { + // iPad Safari with frozen UA + it("Safari on iPadOS 26 (UA frozen at 18.6)", () => { + mockUserAgent( + "Mozilla/5.0 (iPad; CPU OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPad); + expect(SystemInfo.operatingSystem).to.eq("iPad OS 26.0.0"); + }); + + // Chrome on iPad + it("Chrome on iPadOS 26", () => { + mockUserAgent( + "Mozilla/5.0 (iPad; CPU OS 26_0_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/138.0.7204.156 Mobile/15E148 Safari/604.1" + ); + SystemInfoInternal._initialize(); + expect(SystemInfo.platform).to.eq(Platform.IPad); + expect(SystemInfo.operatingSystem).to.eq("iPad OS 26.0.0"); + }); + }); +});