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:
undergroundwires
2023-12-11 05:24:27 +01:00
parent 916c9d62d9
commit a9851272ae
43 changed files with 1719 additions and 672 deletions

View File

@@ -0,0 +1,7 @@
export function formatAssertionMessage(lines: readonly string[]) {
return [ // Using many newlines so `vitest` output looks good
'\n---',
...lines,
'---\n\n',
].join('\n');
}

View File

@@ -0,0 +1,67 @@
export type EventName = keyof WindowEventMap;
export function createWindowEventSpies(restoreCallback: (restoreFunc: () => void) => void) {
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
const currentListeners = new Array<Parameters<typeof window.addEventListener>>();
const addEventListenerCalls = new Array<Parameters<typeof window.addEventListener>>();
const removeEventListenerCalls = new Array<Parameters<typeof window.removeEventListener>>();
window.addEventListener = (
...args: Parameters<typeof window.addEventListener>
): ReturnType<typeof window.addEventListener> => {
addEventListenerCalls.push(args);
currentListeners.push(args);
return originalAddEventListener.call(window, ...args);
};
window.removeEventListener = (
...args: Parameters<typeof window.removeEventListener>
): ReturnType<typeof window.removeEventListener> => {
removeEventListenerCalls.push(args);
const [type, listener] = args;
const registeredListener = findCurrentListener(type as EventName, listener);
if (registeredListener) {
const index = currentListeners.indexOf(registeredListener);
if (index > -1) {
currentListeners.splice(index, 1);
}
}
return originalRemoveEventListener.call(window, ...args);
};
function findCurrentListener(
type: EventName,
listener: EventListenerOrEventListenerObject,
): Parameters<typeof window.addEventListener> | undefined {
return currentListeners.find((args) => {
const [eventType, eventListener] = args;
return eventType === type && listener === eventListener;
});
}
restoreCallback(() => {
window.addEventListener = originalAddEventListener;
window.removeEventListener = originalRemoveEventListener;
});
return {
isAddEventCalled(eventType: EventName): boolean {
const call = addEventListenerCalls.find((args) => {
const [type] = args;
return type === eventType;
});
return call !== undefined;
},
isRemoveEventCalled(eventType: EventName) {
const call = removeEventListenerCalls.find((args) => {
const [type] = args;
return type === eventType;
});
return call !== undefined;
},
currentListeners,
};
}