Files
privacy.sexy/tests/e2e/modal-layout-shifts.cy.ts
undergroundwires a9851272ae 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.
2023-12-11 05:24:27 +01:00

88 lines
3.0 KiB
TypeScript

import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { ViewportTestScenarios } from './support/scenarios/viewport-test-scenarios';
describe('Modal interaction and layout stability', () => {
ViewportTestScenarios.forEach(({ // some shifts are observed only on extra small or large screens
name, width, height,
}) => {
it(name, () => {
cy.viewport(width, height);
cy.visit('/');
let metricsBeforeModal: ViewportMetrics | undefined;
captureViewportMetrics((metrics) => {
metricsBeforeModal = metrics;
});
cy
.contains('a', 'Privacy')
.click();
cy
.get('.modal-content')
.should('be.visible');
captureViewportMetrics((metrics) => {
const metricsAfterModal = metrics;
expect(metricsBeforeModal).to.deep.equal(metricsAfterModal, formatAssertionMessage([
`Expected (initial metrics before modal): ${JSON.stringify(metricsBeforeModal)}`,
`Actual (metrics after modal is opened): ${JSON.stringify(metricsAfterModal)}`,
]));
});
});
});
});
interface ViewportMetrics {
readonly x: number;
readonly y: number;
/*
Excluding height and width from the metrics to ensure test accuracy.
Height and width measurements can lead to false negatives due to layout shifts caused by
delayed loading of fonts and icons.
*/
}
function captureViewportMetrics(callback: (metrics: ViewportMetrics) => void): void {
cy.window().then((win) => {
cy.get('body')
.then((body) => {
const position = getElementViewportMetrics(body[0], win);
cy.log(`Captured metrics: ${JSON.stringify(position)}`);
callback(position);
});
});
}
function getElementViewportMetrics(element: HTMLElement, win: Window): ViewportMetrics {
const elementXRelativeToViewport = getElementXRelativeToViewport(element, win);
const elementYRelativeToViewport = getElementYRelativeToViewport(element, win);
return {
x: elementXRelativeToViewport,
y: elementYRelativeToViewport,
};
}
function getElementYRelativeToViewport(element: HTMLElement, win: Window): number {
const relativeTop = element.getBoundingClientRect().top;
const { position, top } = win.getComputedStyle(element);
const topValue = position === 'static' ? 0 : parseInt(top, 10);
if (Number.isNaN(topValue)) {
throw new Error(`Could not calculate Y position value from 'top': ${top}`);
}
const viewportRelativeY = relativeTop - topValue + win.scrollY;
return viewportRelativeY;
}
function getElementXRelativeToViewport(element: HTMLElement, win: Window): number {
const relativeLeft = element.getBoundingClientRect().left;
const { position, left } = win.getComputedStyle(element);
const leftValue = position === 'static' ? 0 : parseInt(left, 10);
if (Number.isNaN(leftValue)) {
throw new Error(`Could not calculate X position value from 'left': ${left}`);
}
const viewportRelativeX = relativeLeft - leftValue + win.scrollX;
return viewportRelativeX;
}