Fix touch state not being activated in iOS Safari
This commit resolves the issue with the `:active` pseudo-class not
activating in mobile Safari on iOS devices. It introduces a workaround
specifically for mobile Safari on iOS/iPadOS to enable the `:active`
pseudo-class. This ensures a consistent and responsive user interface
in response to touch states on mobile Safari.
Other supporting changes:
- Introduce new test utility functions such as `createWindowEventSpies`
and `formatAssertionMessage` to improve code reusability and
maintainability.
- Improve browser detection:
- Add detection for iPadOS and Windows 10 Mobile.
- Add touch support detection to correctly determine iPadOS vs macOS.
- Fix misidentification of some Windows 10 Mobile platforms as Windows
Phone.
- Improve test coverage and refactor tests.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe } from 'vitest';
|
||||
import { describe, it } from 'vitest';
|
||||
import { createApp } from 'vue';
|
||||
import { ApplicationBootstrapper } from '@/presentation/bootstrapping/ApplicationBootstrapper';
|
||||
import { expectDoesNotThrowAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, it, afterEach } from 'vitest';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { MobileSafariActivePseudoClassEnabler } from '@/presentation/bootstrapping/Modules/MobileSafariActivePseudoClassEnabler';
|
||||
import { EventName, createWindowEventSpies } from '@tests/shared/Spies/WindowEventSpies';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { isTouchEnabledDevice } from '@/infrastructure/RuntimeEnvironment/TouchSupportDetection';
|
||||
import { MobileSafariDetectionTestCases } from './MobileSafariDetectionTestCases';
|
||||
|
||||
describe('MobileSafariActivePseudoClassEnabler', () => {
|
||||
describe('bootstrap', () => {
|
||||
MobileSafariDetectionTestCases.forEach(({
|
||||
description, userAgent, supportsTouch, expectedResult,
|
||||
}) => {
|
||||
it(description, () => {
|
||||
// arrange
|
||||
const expectedEvent: EventName = 'touchstart';
|
||||
patchUserAgent(userAgent, afterEach);
|
||||
const { isAddEventCalled, currentListeners } = createWindowEventSpies(afterEach);
|
||||
const patchedEnvironment = new ConstructibleRuntimeEnvironment(supportsTouch);
|
||||
const sut = new MobileSafariActivePseudoClassEnabler(patchedEnvironment);
|
||||
// act
|
||||
sut.bootstrap();
|
||||
// assert
|
||||
const isSet = isAddEventCalled(expectedEvent);
|
||||
expect(isSet).to.equal(expectedResult, formatAssertionMessage([
|
||||
`Expected result\t\t: ${expectedResult ? 'true (mobile Safari)' : 'false (not mobile Safari)'}`,
|
||||
`Actual result\t\t: ${isSet ? 'true (mobile Safari)' : 'false (not mobile Safari)'}`,
|
||||
`User agent\t\t: ${navigator.userAgent}`,
|
||||
`Touch supported\t\t: ${supportsTouch}`,
|
||||
`Current OS\t\t: ${patchedEnvironment.os === undefined ? 'unknown' : OperatingSystem[patchedEnvironment.os]}`,
|
||||
`Is desktop?\t\t: ${patchedEnvironment.isDesktop ? 'Yes (Desktop app)' : 'No (Browser)'}`,
|
||||
`Listeners (${currentListeners.length})\t\t: ${JSON.stringify(currentListeners)}`,
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function patchUserAgent(
|
||||
userAgent: string,
|
||||
restoreCallback: (restoreFunc: () => void) => void,
|
||||
) {
|
||||
const originalNavigator = window.navigator;
|
||||
const userAgentGetter = { get: () => userAgent };
|
||||
window.navigator = Object.create(navigator, {
|
||||
userAgent: userAgentGetter,
|
||||
});
|
||||
restoreCallback(() => {
|
||||
Object.assign(window, {
|
||||
navigator: originalNavigator,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTouchDetectorMock(
|
||||
isTouchEnabled: boolean,
|
||||
): typeof isTouchEnabledDevice {
|
||||
return () => isTouchEnabled;
|
||||
}
|
||||
|
||||
class ConstructibleRuntimeEnvironment extends RuntimeEnvironment {
|
||||
public constructor(isTouchEnabled: boolean) {
|
||||
super(window, undefined, undefined, getTouchDetectorMock(isTouchEnabled));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { determineTouchSupportOptions } from '@tests/integration/shared/TestCases/TouchSupportOptions';
|
||||
|
||||
interface PlatformTestCase {
|
||||
readonly description: string;
|
||||
readonly userAgent: string;
|
||||
readonly supportsTouch: boolean;
|
||||
readonly expectedResult: boolean;
|
||||
}
|
||||
|
||||
export const MobileSafariDetectionTestCases: ReadonlyArray<PlatformTestCase> = [
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Safari',
|
||||
expectedResult: true,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'Safari on iPad (≥ 13)',
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15', // same as macOS (desktop)
|
||||
supportsTouch: true,
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Safari on iPad (< 13)',
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
userAgent: 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B14 3 Safari/601.1',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Safari on iPhone',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Safari on iPod touch',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozila/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Geckto) Version/3.0 Mobile/3A101a Safari/419.3',
|
||||
// https://web.archive.org/web/20231112165804/https://www.cnet.com/tech/mobile/safari-for-ipod-touch-has-different-user-agent-string-may-not-go-directly-to-iphone-optimized-sites/null/
|
||||
},
|
||||
],
|
||||
}),
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Safari',
|
||||
expectedResult: false,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'Safari on macOS',
|
||||
operatingSystem: OperatingSystem.macOS,
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
|
||||
supportsTouch: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Chrome',
|
||||
expectedResult: false,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'macOS',
|
||||
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: 'iPad (iPadOS 17)',
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
userAgent: 'Mozilla/5.0 (iPad; CPU OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPhone (iOS 17)',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/119.0.6045.109 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPhone (iOS 12)',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/70.0.3538.75 Mobile/15E148 Safari/605.1',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPod Touch (iOS 12)',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPod; CPU iPhone OS 12_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/86.0.4240.93 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Firefox',
|
||||
expectedResult: false,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'macOS',
|
||||
operatingSystem: OperatingSystem.macOS,
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.1; rv:119.0) Gecko/20100101 Firefox/119.0',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPad (iPadOS 13)',
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
userAgent: 'Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/19.1b16203 Mobile/15E148 Safari/605.1.15',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPhone (iOS 17)',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/117.2 Mobile/15E148 Safari/605.1.15',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Edge',
|
||||
expectedResult: false,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'macOS',
|
||||
operatingSystem: OperatingSystem.macOS,
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPad (iPadOS 15)',
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
userAgent: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/96.0.1054.61 Version/15.0 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'iPhone (iOS 17)',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/118.0.2088.81 Version/17.0 Mobile/15E148 Safari/604.1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Opera',
|
||||
expectedResult: false,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'Opera Mini on iPhone',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Opera/9.80 (iPhone; Opera Mini/5.0.0176/764; U; en) Presto/2.4.15',
|
||||
// https://web.archive.org/web/20140221034354/http://my.opera.com/haavard/blog/2010/04/16/iphone-user-agent
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Opera Mini (Opera Turbo) on iPhone',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) OPiOS/8.0.0.78129 Mobile/11D201 Safari/9537.53',
|
||||
// https://web.archive.org/web/20231112164709/https://dev.opera.com/blog/opera-mini-8-for-ios/
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Opera on macOS',
|
||||
operatingSystem: OperatingSystem.macOS,
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 OPR/94.0.0.0',
|
||||
// https://web.archive.org/web/20231112164741/https://forums.opera.com/topic/59600/have-the-user-agent-browser-identification-match-with-the-mac-os-version-the-browser-is-running-on
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Opera on macOS (legacy)',
|
||||
operatingSystem: OperatingSystem.macOS,
|
||||
userAgent: 'Opera/9.80 (Macintosh; Intel Mac OS X 10.8; U; ru) Presto/2.10 Version/12.00',
|
||||
// https://web.archive.org/web/20231112164741/https://forums.opera.com/topic/59600/have-the-user-agent-browser-identification-match-with-the-mac-os-version-the-browser-is-running-on
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Opera Touch on iPhone',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) OPT/3.3.3 Mobile/15E148',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Opera Mini on iPhone',
|
||||
operatingSystem: OperatingSystem.iOS,
|
||||
userAgent: 'Opera/9.80 (iPhone; Opera Mini/14.0.0/37.8603; U; en) Presto/2.12.423 Version/12.16',
|
||||
},
|
||||
{
|
||||
deviceInfo: 'Opera Mini on iPad',
|
||||
operatingSystem: OperatingSystem.iPadOS,
|
||||
userAgent: 'Opera/9.80 (iPad; Opera Mini/7.0.5/191.320; U; id) Presto/2.12.423 Version/12.16',
|
||||
},
|
||||
],
|
||||
}),
|
||||
...createBrowserTestCases({
|
||||
browserName: 'Vivo Browser', // Runs only Vivo (Android) devices
|
||||
expectedResult: false,
|
||||
userAgents: [
|
||||
{
|
||||
deviceInfo: 'VivoBrowser on Android',
|
||||
operatingSystem: OperatingSystem.Android,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 10; V1990A; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36 VivoBrowser/10.3.10.0',
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
interface UserAgentTestScenario {
|
||||
readonly userAgent: string;
|
||||
readonly operatingSystem: OperatingSystem;
|
||||
readonly deviceInfo: string;
|
||||
readonly supportsTouch?: boolean;
|
||||
}
|
||||
|
||||
interface BrowserTestScenario {
|
||||
readonly browserName: string;
|
||||
readonly expectedResult: boolean;
|
||||
readonly userAgents: readonly UserAgentTestScenario[];
|
||||
}
|
||||
|
||||
function createBrowserTestCases(
|
||||
scenario: BrowserTestScenario,
|
||||
): PlatformTestCase[] {
|
||||
return scenario.userAgents.flatMap((agentInfo): readonly PlatformTestCase[] => {
|
||||
const touchCases = agentInfo.supportsTouch === undefined
|
||||
? determineTouchSupportOptions(agentInfo.operatingSystem)
|
||||
: [agentInfo.supportsTouch];
|
||||
return touchCases.map((hasTouch): PlatformTestCase => ({
|
||||
description: [
|
||||
scenario.expectedResult ? '[POSITIVE]' : '[NEGATIVE]',
|
||||
scenario.browserName,
|
||||
OperatingSystem[agentInfo.operatingSystem],
|
||||
agentInfo.deviceInfo,
|
||||
hasTouch === true ? '[TOUCH]' : '[NO TOUCH]',
|
||||
].join(' | '),
|
||||
userAgent: agentInfo.userAgent,
|
||||
supportsTouch: hasTouch,
|
||||
expectedResult: scenario.expectedResult,
|
||||
}));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user