Fix card list UI layout shifts (jumps) on load
This commit fixes layout shifts that occur on card list part of the page when the page is initially loaded. - Resolve issue where card list starts with minimal width, leading to jumps in UI until correct width is calculated on medium and big screens. - Dispose of existing `ResizeObserver` properly before creating a new one. This prevents leaks and incorrect width calculations if `containerElement` changes. - Throttle resize events to minimize width/height calculation changes, enhancing performance and reducing the chances for layout shifts. Supporting CI/CD improvements: - Enable artifact upload in CI/CD even if E2E tests fail. - Distinguish uploaded artifacts by operating system for clarity.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Ref, nextTick, ref } from 'vue';
|
||||
import CardList from '@/presentation/components/Scripts/View/Cards/CardList.vue';
|
||||
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
|
||||
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
|
||||
import { InjectionKeys } from '@/presentation/injectionSymbols';
|
||||
import { createSizeObserverStub } from '@tests/unit/shared/Stubs/SizeObserverStub';
|
||||
|
||||
const DOM_SELECTOR_CARDS = '.cards';
|
||||
|
||||
describe('CardList.vue', () => {
|
||||
describe('rendering cards based on width', () => {
|
||||
it('renders cards when a valid width is provided', async () => {
|
||||
// arrange
|
||||
const expectedCardsExistence = true;
|
||||
const width = ref(0);
|
||||
// act
|
||||
const wrapper = mountComponent({
|
||||
widthRef: width,
|
||||
});
|
||||
width.value = 800;
|
||||
await nextTick();
|
||||
// assert
|
||||
const actual = wrapper.find(DOM_SELECTOR_CARDS).exists();
|
||||
expect(actual).to.equal(expectedCardsExistence, wrapper.html());
|
||||
});
|
||||
it('does not render cards when width is not set', async () => {
|
||||
// arrange
|
||||
const expectedCardsExistence = false;
|
||||
const width = ref(0);
|
||||
const wrapper = mountComponent({
|
||||
widthRef: width,
|
||||
});
|
||||
// act
|
||||
await nextTick();
|
||||
// assert
|
||||
const actual = wrapper.find(DOM_SELECTOR_CARDS).exists();
|
||||
expect(actual).to.equal(expectedCardsExistence, wrapper.html());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mountComponent(options?: {
|
||||
readonly useCollectionState?: ReturnType<typeof useCollectionState>,
|
||||
readonly widthRef?: Readonly<Ref<number>>,
|
||||
}) {
|
||||
const {
|
||||
name: sizeObserverName,
|
||||
component: sizeObserverStub,
|
||||
} = createSizeObserverStub(options?.widthRef);
|
||||
return shallowMount(CardList, {
|
||||
global: {
|
||||
provide: {
|
||||
[InjectionKeys.useCollectionState.key]:
|
||||
() => options?.useCollectionState ?? new UseCollectionStateStub().get(),
|
||||
},
|
||||
stubs: {
|
||||
[sizeObserverName]: sizeObserverStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
||||
return this.collection.os;
|
||||
}
|
||||
|
||||
public collection: ICategoryCollection = new CategoryCollectionStub();
|
||||
public collection: ICategoryCollection = new CategoryCollectionStub().withSomeActions();
|
||||
|
||||
public selection: IUserSelection = new UserSelectionStub([]);
|
||||
|
||||
|
||||
@@ -21,6 +21,13 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
|
||||
public readonly actions = new Array<ICategory>();
|
||||
|
||||
public withSomeActions(): this {
|
||||
this.withAction(new CategoryStub(1));
|
||||
this.withAction(new CategoryStub(2));
|
||||
this.withAction(new CategoryStub(3));
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAction(category: ICategory): this {
|
||||
this.actions.push(category);
|
||||
return this;
|
||||
|
||||
27
tests/unit/shared/Stubs/SizeObserverStub.ts
Normal file
27
tests/unit/shared/Stubs/SizeObserverStub.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
const COMPONENT_SIZE_OBSERVER_NAME = 'SizeObserver';
|
||||
|
||||
export function createSizeObserverStub(
|
||||
widthRef: Readonly<Ref<number>> = ref(500),
|
||||
) {
|
||||
const component = defineComponent({
|
||||
name: COMPONENT_SIZE_OBSERVER_NAME,
|
||||
template: `<div id="${COMPONENT_SIZE_OBSERVER_NAME}-stub"><slot /></div>`,
|
||||
emits: {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
widthChanged: (newWidth: number) => true,
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
},
|
||||
setup: (_, { emit }) => {
|
||||
watch(widthRef, (newValue) => {
|
||||
emit('widthChanged', newValue);
|
||||
});
|
||||
},
|
||||
});
|
||||
return {
|
||||
name: COMPONENT_SIZE_OBSERVER_NAME,
|
||||
component,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user