Fix button inconsistencies and macOS layout shifts
This commit fixes layout shifts experienced in macOS Safari when hovering over top menu items. Instead of making text bold — which was causing layout shifts — the hover effect now changes the text color. This ensures a consistent UI across different browsers and platforms. Additionally, this commit fixes the styling of the privacy button located in the bottom right corner. Previously styled as an `<a>` element, it is now correctly represented as a `<button>`. Furthermore, the commit enhances HTML conformity and accessibility by correctly using `<button>` and `<a>` tags instead of relying on click interactions on `<span>` elements. This commit introduces `FlatButton` Vue component and a new `flat-button` mixin. These centralize button usage and link styles, aligning the hover/touch reactions of buttons across the application, thereby creating a more consistent user interface.
This commit is contained in:
167
tests/unit/presentation/components/Shared/FlatButton.spec.ts
Normal file
167
tests/unit/presentation/components/Shared/FlatButton.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import {
|
||||
describe, it, expect,
|
||||
} from 'vitest';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import FlatButton from '@/presentation/components/Shared/FlatButton.vue';
|
||||
import { IconName } from '@/presentation/components/Shared/Icon/IconName';
|
||||
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import { hasDirective } from '@/presentation/components/Scripts/View/Cards/NonCollapsingDirective';
|
||||
|
||||
const DOM_SELECTOR_LABEL = 'span';
|
||||
const DOM_SELECTOR_BUTTON = 'button';
|
||||
const DOM_CLASS_DISABLED_CLASS = 'disabled';
|
||||
|
||||
describe('FlatButton.vue', () => {
|
||||
describe('label', () => {
|
||||
it('renders label when provided', () => {
|
||||
// arrange
|
||||
const expectedLabel = 'expected label';
|
||||
|
||||
// act
|
||||
const wrapper = mountComponent({ labelPropValue: expectedLabel });
|
||||
|
||||
// assert
|
||||
const labelElement = wrapper.find(DOM_SELECTOR_LABEL);
|
||||
expect(labelElement.text()).to.equal(expectedLabel);
|
||||
});
|
||||
it('does not render label when not provided', () => {
|
||||
// arrange
|
||||
const absentLabelValue = undefined;
|
||||
|
||||
// act
|
||||
const wrapper = mountComponent({ labelPropValue: absentLabelValue });
|
||||
|
||||
// assert
|
||||
const labelElement = wrapper.find(DOM_SELECTOR_LABEL);
|
||||
expect(labelElement.exists()).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('icon', () => {
|
||||
it('renders icon when provided', () => {
|
||||
// arrange
|
||||
const expectedIcon: IconName = 'globe';
|
||||
|
||||
// act
|
||||
const wrapper = mountComponent({ iconPropValue: expectedIcon });
|
||||
|
||||
// assert
|
||||
expect(wrapper.findComponent(AppIcon).exists()).to.equal(true);
|
||||
});
|
||||
it('does not render icon when not provided', () => {
|
||||
// arrange
|
||||
const absentIconValue = undefined;
|
||||
|
||||
// act
|
||||
const wrapper = mountComponent({ iconPropValue: absentIconValue });
|
||||
|
||||
// assert
|
||||
expect(wrapper.findComponent(AppIcon).exists()).to.equal(false);
|
||||
});
|
||||
it('correctly binds given icon', () => {
|
||||
// arrange
|
||||
const expectedIcon: IconName = 'globe';
|
||||
|
||||
// act
|
||||
const wrapper = mountComponent({ iconPropValue: expectedIcon });
|
||||
|
||||
// assert
|
||||
const appIconComponent = wrapper.findComponent(AppIcon);
|
||||
expect(appIconComponent.props('icon')).toEqual(expectedIcon);
|
||||
});
|
||||
});
|
||||
describe('label + icon', () => {
|
||||
it('renders both label and icon when provided', () => {
|
||||
// arrange
|
||||
const expectedLabel = 'Test Label';
|
||||
const expectedIcon: IconName = 'globe';
|
||||
|
||||
// act
|
||||
const wrapper = mountComponent({
|
||||
labelPropValue: expectedLabel,
|
||||
iconPropValue: expectedIcon,
|
||||
});
|
||||
|
||||
// assert
|
||||
const labelElement = wrapper.find(DOM_SELECTOR_LABEL);
|
||||
expect(labelElement.text()).to.equal(expectedLabel);
|
||||
expect(wrapper.findComponent(AppIcon).exists()).to.equal(true);
|
||||
});
|
||||
});
|
||||
describe('disabled', () => {
|
||||
it('emits click event when enabled and clicked', async () => {
|
||||
// arrange
|
||||
const wrapper = mountComponent({ isDisabledPropValue: false });
|
||||
|
||||
// act
|
||||
await wrapper.find(DOM_SELECTOR_BUTTON).trigger('click');
|
||||
|
||||
// assert
|
||||
expect(wrapper.emitted().click).to.have.lengthOf(1, formatAssertionMessage([
|
||||
`Disabled prop value: ${wrapper.props('disabled')}`,
|
||||
`Emitted events: ${JSON.stringify(wrapper.emitted())}`,
|
||||
'Inner HTML:', wrapper.html(),
|
||||
]));
|
||||
});
|
||||
it('does not emit click event when disabled and clicked', async () => {
|
||||
// arrange
|
||||
const wrapper = mountComponent({ isDisabledPropValue: true });
|
||||
|
||||
// act
|
||||
await wrapper.find(DOM_SELECTOR_BUTTON).trigger('click');
|
||||
|
||||
// assert
|
||||
expect(wrapper.emitted().click ?? []).to.have.lengthOf(0, formatAssertionMessage([
|
||||
`Disabled prop value: ${wrapper.props('disabled')}`,
|
||||
'Inner HTML:', wrapper.html(),
|
||||
]));
|
||||
});
|
||||
it('applies disabled class when disabled', () => {
|
||||
// arrange & act
|
||||
const wrapper = mountComponent({ isDisabledPropValue: true });
|
||||
// assert
|
||||
const classes = wrapper.find(DOM_SELECTOR_BUTTON).classes();
|
||||
expect(classes).to.contain(DOM_CLASS_DISABLED_CLASS, formatAssertionMessage([
|
||||
`Disabled prop value: ${wrapper.props('disabled')}`,
|
||||
'Inner HTML:', wrapper.html(),
|
||||
]));
|
||||
});
|
||||
it('does not apply disabled class when enabled', () => {
|
||||
// arrange & act
|
||||
const wrapper = mountComponent({ isDisabledPropValue: false });
|
||||
// assert
|
||||
const classes = wrapper.find(DOM_SELECTOR_BUTTON).classes();
|
||||
expect(classes).not.contain(DOM_CLASS_DISABLED_CLASS, formatAssertionMessage([
|
||||
`Disabled prop value: ${wrapper.props('disabled')}`,
|
||||
'Inner HTML:', wrapper.html(),
|
||||
]));
|
||||
});
|
||||
});
|
||||
it('applies non-collapsing directive correctly', () => {
|
||||
// act & arrange
|
||||
const wrapper = mountComponent();
|
||||
|
||||
// assert
|
||||
const button = wrapper.find(DOM_SELECTOR_BUTTON);
|
||||
const isDirectiveApplied = hasDirective(button.element);
|
||||
expect(isDirectiveApplied).to.equal(true, formatAssertionMessage([
|
||||
`Attributes: ${JSON.stringify(button.attributes())}`,
|
||||
'Button HTML:', button.html(),
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
function mountComponent(options?: {
|
||||
readonly iconPropValue?: IconName,
|
||||
readonly labelPropValue?: string,
|
||||
readonly isDisabledPropValue?: boolean,
|
||||
readonly nonCollapsingDirective?: () => void,
|
||||
}) {
|
||||
return shallowMount(FlatButton, {
|
||||
props: {
|
||||
icon: options === undefined ? 'globe' : options?.iconPropValue,
|
||||
label: options === undefined ? 'stub-label' : options?.labelPropValue,
|
||||
disabled: options?.isDisabledPropValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user