Fix horizontal layout shift after script selection

This commit resolves an issue causing horizontal UI layout shift when a
script is selected for the first time, and when all selected scripts are
deselected. This issue was only observed on Chromium-based browsers on
Linux environment when using macOS and Windows script collections.

The underlying cause was identified as the use of percentage-based
values for CSS margin and padding. To resolve this issue, these values
were updated to absolute measurements. This adjustment maintains layout
consistency across user interactions without compromising the
responsiveness.

The underlying cause was identified as the use of percentage-based values
for CSS margin and padding within certain elements. To resolve this issue,
these values were updated to absolute measurements. This adjustment
maintains layout consistency across user interactions without compromising
the responsiveness of the application.

Additionally, an end-to-end (E2E) test has been introduced to monitor
for future regressions of this layout shift bug, ensuring that the fix
remains effective over subsequent updates.
This commit is contained in:
undergroundwires
2024-04-02 12:17:20 +02:00
parent 557cea3f48
commit bc7e1faa1c
6 changed files with 153 additions and 66 deletions

View File

@@ -0,0 +1,75 @@
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
export function assertLayoutStability(selector: string, action: ()=> void): void {
// arrange
let initialMetrics: ViewportMetrics | undefined;
captureViewportMetrics(selector, (metrics) => {
initialMetrics = metrics;
});
// act
action();
// assert
captureViewportMetrics(selector, (metrics) => {
const finalMetrics = metrics;
expect(initialMetrics).to.deep.equal(finalMetrics, formatAssertionMessage([
`Expected (initial metrics before action): ${JSON.stringify(initialMetrics)}`,
`Actual (final metrics after action): ${JSON.stringify(finalMetrics)}`,
]));
});
}
function captureViewportMetrics(
selector: string,
callback: (metrics: ViewportMetrics) => void,
): void {
cy.window().then((win) => {
cy.get(selector)
.then((elements) => {
const element = elements[0];
const position = getElementViewportMetrics(element, win);
cy.log(`Captured metrics (\`${selector}\`): ${JSON.stringify(position)}`);
callback(position);
});
});
}
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 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;
}