diff --git a/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserConditions.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserConditions.ts index 26eda7fc..02608cd7 100644 --- a/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserConditions.ts +++ b/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserConditions.ts @@ -57,6 +57,7 @@ export const BrowserConditions: readonly BrowserCondition[] = [ { operatingSystem: OperatingSystem.iPadOS, existingPartsInSameUserAgent: ['Macintosh'], // Reported by Safari on iPads running ≥ iPadOS 13 + notExistingPartsInUserAgent: ['Electron'], // Electron supports only macOS, not iPadOS touchSupport: TouchSupportExpectation.MustExist, // Safari same user agent as desktop macOS }, { diff --git a/src/infrastructure/RuntimeEnvironment/TouchSupportDetection.ts b/src/infrastructure/RuntimeEnvironment/TouchSupportDetection.ts index a60f3549..b3581b7e 100644 --- a/src/infrastructure/RuntimeEnvironment/TouchSupportDetection.ts +++ b/src/infrastructure/RuntimeEnvironment/TouchSupportDetection.ts @@ -10,43 +10,48 @@ export interface BrowserTouchSupportAccessor { navigatorMaxTouchPoints: () => number | undefined; windowMatchMediaMatches: (query: string) => boolean; documentOntouchend: () => undefined | unknown; - windowTouchEvent: () => undefined | unknown; } +/* + Touch support checks are inconsistent across different browsers and OS. + `✅` and `❌` indicate correct and incorrect detections, respectively. +*/ const TouchSupportChecks: ReadonlyArray<(accessor: BrowserTouchSupportAccessor) => boolean> = [ /* - ✅ Mobile: Chrome, Safari, Firefox on iOS and Android - ❌ Touch-enabled Windows laptop: Chrome - (Chromium has removed ontouch* events on desktop since Chrome 70+.) - ❌ Touch-enabled Windows laptop: Firefox + Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox + Touch-enabled Windows laptop: ❌ Chrome (reports no touch), ❌ Firefox (reports no touch) + Chromium has removed ontouch* events on desktop since Chrome 70+ + Non-touch macOS: ✅ Firefox, ✅ Safari, ✅ Chromium */ (accessor) => accessor.documentOntouchend() !== undefined, /* - ✅ Mobile: Chrome, Safari, Firefox on iOS and Android - ✅ Touch-enabled Windows laptop: Chrome - ❌ Touch-enabled Windows laptop: Firefox + Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox + Touch-enabled Windows laptop: ✅ Chrome, ❌ Firefox (reports no touch) + Non-touch macOS: ✅ Firefox, ✅ Safari, ✅ Chromium */ (accessor) => { const maxTouchPoints = accessor.navigatorMaxTouchPoints(); return maxTouchPoints !== undefined && maxTouchPoints > 0; }, /* - ✅ Mobile: Chrome, Safari, Firefox on iOS and Android - ✅ Touch-enabled Windows laptop: Chrome - ❌ Touch-enabled Windows laptop: Firefox + Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox + Touch-enabled Windows laptop: ✅ Chrome, ❌ Firefox (reports no touch) + Non-touch macOS: ✅ Firefox, ✅ Safari, ✅ Chromium */ (accessor) => accessor.windowMatchMediaMatches('(any-pointer: coarse)'), + /* - ✅ Mobile: Chrome, Safari, Firefox on iOS and Android - ✅ Touch-enabled Windows laptop: Chrome - ❌ Touch-enabled Windows laptop: Firefox + Do not check window.TouchEvent === undefined, as it incorrectly + reports touch support on Chromium macOS even though there is no + touch support. + Mobile (iOS & Android): ✅ Chrome, ✅ Safari, ✅ Firefox + Touch-enabled Windows laptop: ✅ Chrome, ❌ Firefox (reports no touch) + Non-touch macOS: ✅ Firefox, ✅ Safari, ❌ Chromium (reports touch) */ - (accessor) => accessor.windowTouchEvent() !== undefined, ]; const GlobalTouchSupportAccessor: BrowserTouchSupportAccessor = { navigatorMaxTouchPoints: () => navigator.maxTouchPoints, windowMatchMediaMatches: (query: string) => window.matchMedia(query)?.matches, documentOntouchend: () => document.ontouchend, - windowTouchEvent: () => window.TouchEvent, } as const; diff --git a/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts b/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts index c4c670e0..71fb6492 100644 --- a/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts +++ b/tests/integration/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsTestCases.ts @@ -37,6 +37,8 @@ export const BrowserOsTestCases: ReadonlyArray = [ 'Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1', // UC Browser: 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 UBrowser/6.0.1308.1016 Safari/537.36', + // Electron: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', ], }), ...createTests({ @@ -56,6 +58,8 @@ export const BrowserOsTestCases: ReadonlyArray = [ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.82 Safari/537.36 OPR/29.0.1795.41 (Edition beta)', // Edge: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0', + // Electron: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', ], }), ...createTests({ @@ -68,6 +72,8 @@ export const BrowserOsTestCases: ReadonlyArray = [ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', // Edge: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188', + // Electron: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', ], }), ...createTests({ diff --git a/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts b/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts index 8c1acbc8..58f6a0b1 100644 --- a/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts +++ b/tests/integration/presentation/bootstrapping/Modules/MobileSafariDetectionTestCases.ts @@ -58,6 +58,11 @@ export const MobileSafariDetectionTestCases: ReadonlyArray = [ operatingSystem: OperatingSystem.macOS, userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', }, + { + deviceInfo: 'macOS (Electron)', + operatingSystem: OperatingSystem.macOS, + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.5993.54 Electron/27.0.0 Safari/537.36', + }, { deviceInfo: 'iPad (iPadOS 17)', operatingSystem: OperatingSystem.iPadOS, diff --git a/tests/unit/infrastructure/RuntimeEnvironment/TouchSupportDetection.spec.ts b/tests/unit/infrastructure/RuntimeEnvironment/TouchSupportDetection.spec.ts index c2e8c379..778f267e 100644 --- a/tests/unit/infrastructure/RuntimeEnvironment/TouchSupportDetection.spec.ts +++ b/tests/unit/infrastructure/RuntimeEnvironment/TouchSupportDetection.spec.ts @@ -32,11 +32,6 @@ describe('TouchSupportDetection', () => { }), expectedTouch: true, }, - { - description: 'detects touch capability with defined window.TouchEvent', - expectedTouch: true, - accessor: createMockAccessor({ windowTouchEvent: () => class {} }), - }, ]; testScenarios.forEach(({ description, accessor, expectedTouch }) => { it(`${description} - returns ${expectedTouch}`, () => { @@ -56,7 +51,6 @@ function createMockAccessor( navigatorMaxTouchPoints: () => undefined, windowMatchMediaMatches: () => false, documentOntouchend: () => undefined, - windowTouchEvent: () => undefined, }; return { ...defaultTouchSupport,