Files
privacy.sexy/tests/e2e/modal-layout-shifts.cy.ts
undergroundwires e299d40fa1 Fix layout jumps/shifts and overflow on modals
This commit improves the user interface in modal display.

- Prevent layout shifts caused by background scrollbars when modals are
  active.
- Fix unintended overflow of modals on small screens, preventing parts
  of the modal from being cut off on the right side.
- Refactor DOM manipulation, enhancing modularity, reusability,
  extensibility, and separation of concerns.
- Centralize viewport test scenarios for different sizes in a single
  definition for E2E tests.
2023-11-19 23:51:25 +01:00

87 lines
2.9 KiB
TypeScript

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, [
`Expected (initial metrics before modal): ${JSON.stringify(metricsBeforeModal)}`,
`Actual (metrics after modal is opened): ${JSON.stringify(metricsAfterModal)}`,
].join('\n'));
});
});
});
});
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;
}