mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
feat: update button component with spinner support and improve test coverage
This commit is contained in:
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -55,6 +55,7 @@ You are a mentor with a dual approach: when I make a mistake or my work needs im
|
||||
|
||||
- Testing Framework Jest
|
||||
- Spectator should be used for Unit tests
|
||||
- When using Spectator use spectator.setInput('<input>','<value>') to set the Component inputs
|
||||
- Unit tests should be included for all components and services
|
||||
- Use the Angular TestBed configuration
|
||||
- Include error case testing
|
||||
@@ -66,6 +67,7 @@ You are a mentor with a dual approach: when I make a mistake or my work needs im
|
||||
- Use strict TypeScript configurations
|
||||
- Follow Angular style guide naming conventions
|
||||
- Follow the project's guidelines in `/docs/guidelines.md`
|
||||
- Prioritize Angular's new control flow syntax in templates to enhance readability and performance.
|
||||
- Organize imports in groups:
|
||||
1. Angular core imports
|
||||
2. Third-party libraries
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
border-radius: 6.25rem;
|
||||
}
|
||||
|
||||
.ui-button__spinner {
|
||||
@apply animate-spin;
|
||||
}
|
||||
|
||||
.ui-button__small {
|
||||
min-width: 10rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
@@ -51,6 +55,10 @@
|
||||
&.disabled {
|
||||
@apply bg-isa-neutral-400;
|
||||
}
|
||||
|
||||
.ui-button__spinner {
|
||||
@apply text-isa-white;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-button__secondary {
|
||||
@@ -69,7 +77,7 @@
|
||||
@apply border-isa-neutral-400 text-isa-neutral-400 bg-isa-white;
|
||||
}
|
||||
|
||||
ng-icon {
|
||||
.ui-button__spinner {
|
||||
@apply text-isa-secondary-600;
|
||||
}
|
||||
}
|
||||
@@ -111,7 +119,7 @@
|
||||
@apply bg-isa-neutral-200 text-isa-neutral-500;
|
||||
}
|
||||
|
||||
ng-icon {
|
||||
.ui-button__spinner {
|
||||
@apply text-isa-neutral-600;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@if (!pending()) {
|
||||
<ng-content></ng-content>
|
||||
} @else {
|
||||
<ng-icon size="1.5rem" class="animate-spin text-isa-white" name="isaLoading"></ng-icon>
|
||||
<ng-icon class="ui-button__spinner" size="1.5rem" name="isaLoading"></ng-icon>
|
||||
}
|
||||
|
||||
@@ -1,71 +1,45 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { ButtonComponent } from './button.component';
|
||||
import { NgIconComponent } from '@ng-icons/core';
|
||||
import { ButtonColor, ButtonSize } from './types';
|
||||
|
||||
describe('ButtonComponent', () => {
|
||||
let spectator: Spectator<ButtonComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: ButtonComponent,
|
||||
imports: [NgIconComponent],
|
||||
});
|
||||
const createComponent = createComponentFactory(ButtonComponent);
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
it('should create the button component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set default input values', () => {
|
||||
expect(spectator.component.size()).toBe('medium');
|
||||
expect(spectator.component.color()).toBe('primary');
|
||||
expect(spectator.component.pending()).toBe(false);
|
||||
expect(spectator.component.tabIndex()).toBe(0);
|
||||
it('should have default classes and tabIndex set', () => {
|
||||
const host = spectator.element;
|
||||
expect(host.classList).toContain('ui-button');
|
||||
expect(host.classList).toContain('ui-button__medium');
|
||||
expect(host.classList).toContain('ui-button__primary');
|
||||
expect(host.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
|
||||
it('should apply correct size classes', () => {
|
||||
const sizes: ButtonSize[] = ['small', 'medium', 'large'];
|
||||
it('should update size class when size input changes', () => {
|
||||
// Update the size signal from its default value ('medium') to 'large'
|
||||
spectator.setInput('size', 'large');
|
||||
spectator.detectChanges();
|
||||
|
||||
sizes.forEach((size) => {
|
||||
spectator.setInput('size', size);
|
||||
expect(spectator.element).toHaveClass(`ui-button__${size}`);
|
||||
});
|
||||
const host = spectator.element;
|
||||
expect(host.classList).toContain('ui-button__large');
|
||||
// Ensure previous size class is no longer present
|
||||
expect(host.classList).not.toContain('ui-button__medium');
|
||||
});
|
||||
|
||||
it('should apply correct color classes', () => {
|
||||
const colors: ButtonColor[] = ['primary', 'secondary', 'tertiary'];
|
||||
it('should update color class when color input changes', () => {
|
||||
// Update the color signal from its default value ('primary') to 'secondary'
|
||||
spectator.setInput('color', 'secondary');
|
||||
spectator.detectChanges();
|
||||
|
||||
colors.forEach((color) => {
|
||||
spectator.setInput('color', color);
|
||||
expect(spectator.element).toHaveClass(`ui-button__${color}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update tabIndex in DOM', () => {
|
||||
spectator.setInput('tabIndex', -1);
|
||||
expect(spectator.element.tabIndex).toBe(-1);
|
||||
});
|
||||
|
||||
it('should show loading icon when pending', () => {
|
||||
spectator.setInput('pending', true);
|
||||
const loadingIcon = spectator.query('ng-icon[name="isaLoading"]');
|
||||
expect(loadingIcon).toExist();
|
||||
});
|
||||
|
||||
it('should have base button class', () => {
|
||||
expect(spectator.element).toHaveClass('ui-button');
|
||||
});
|
||||
|
||||
it('should combine all classes correctly', () => {
|
||||
spectator.setInput({
|
||||
size: 'large',
|
||||
color: 'secondary',
|
||||
});
|
||||
|
||||
expect(spectator.element).toHaveClass('ui-button');
|
||||
expect(spectator.element).toHaveClass('ui-button__large');
|
||||
expect(spectator.element).toHaveClass('ui-button__secondary');
|
||||
const host = spectator.element;
|
||||
expect(host.classList).toContain('ui-button__secondary');
|
||||
// Ensure previous color class is no longer present
|
||||
expect(host.classList).not.toContain('ui-button__primary');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,23 @@ import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { ButtonColor, ButtonSize } from './types';
|
||||
import { isaLoading } from '@isa/icons';
|
||||
|
||||
/**
|
||||
* A UI button component that allows customization of size, color, and state.
|
||||
*
|
||||
* The component uses reactive signals to manage its properties, allowing dynamic
|
||||
* updates to its CSS classes without manual DOM manipulation.
|
||||
*
|
||||
* @property size - The size of the button (e.g., 'small', 'medium', 'large'). The default is 'medium'.
|
||||
* @property sizeClass - A computed CSS class based on the current size, formatted as 'ui-button__{size}'.
|
||||
* @property color - The color theme of the button (e.g., 'primary', 'secondary'). The default is 'primary'.
|
||||
* @property colorClass - A computed CSS class based on the current color, formatted as 'ui-button__{color}'.
|
||||
* @property pending - A boolean flag indicating whether the button is in a loading or pending state.
|
||||
* @property tabIndex - The tab index for the button, used to control keyboard navigation order.
|
||||
*
|
||||
* @remarks
|
||||
* This component relies on reactive input and computed signals to update its state and classes,
|
||||
* ensuring that transformations are automatically reflected in the UI.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ui-button, [uiButton]',
|
||||
templateUrl: './button.component.html',
|
||||
|
||||
46
libs/ui/buttons/src/lib/icon-button.component.spec.ts
Normal file
46
libs/ui/buttons/src/lib/icon-button.component.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { IconButtonComponent } from './icon-button.component';
|
||||
|
||||
describe('IconButtonComponent', () => {
|
||||
let spectator: Spectator<IconButtonComponent>;
|
||||
let component: IconButtonComponent;
|
||||
|
||||
const createComponent = createComponentFactory({
|
||||
component: IconButtonComponent,
|
||||
imports: [], // additional imports if needed
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
component = spectator.component;
|
||||
});
|
||||
|
||||
test('should have default size and computed sizeClass', () => {
|
||||
expect(component.size()).toBe('medium');
|
||||
expect(component.sizeClass()).toBe('ui-icon-button__medium');
|
||||
});
|
||||
|
||||
test('should update size and computed sizeClass when size changes', () => {
|
||||
spectator.setInput('size', 'small');
|
||||
spectator.detectChanges();
|
||||
expect(component.size()).toBe('small');
|
||||
expect(component.sizeClass()).toBe('ui-icon-button__small');
|
||||
});
|
||||
|
||||
test('should have default color and computed colorClass', () => {
|
||||
expect(component.color()).toBe('primary');
|
||||
expect(component.colorClass()).toBe('ui-icon-button__primary');
|
||||
});
|
||||
|
||||
test('should update color and computed colorClass when color changes', () => {
|
||||
spectator.setInput('color', 'secondary');
|
||||
spectator.detectChanges();
|
||||
expect(component.color()).toBe('secondary');
|
||||
expect(component.colorClass()).toBe('ui-icon-button__secondary');
|
||||
});
|
||||
|
||||
test('should have default pending and tabIndex values', () => {
|
||||
expect(component.pending()).toBe(false);
|
||||
expect(component.tabIndex()).toBe(0);
|
||||
});
|
||||
});
|
||||
36
libs/ui/buttons/src/lib/info-button.component.spec.ts
Normal file
36
libs/ui/buttons/src/lib/info-button.component.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { InfoButtonComponent } from './info-button.component';
|
||||
|
||||
describe('InfoButtonComponent', () => {
|
||||
let spectator: Spectator<InfoButtonComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: InfoButtonComponent,
|
||||
shallow: true,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have the default "ui-info-button" class when pending is false', () => {
|
||||
spectator.setInput('pending', false);
|
||||
spectator.detectChanges();
|
||||
|
||||
const hostClasses = spectator.element.classList;
|
||||
expect(hostClasses).toContain('ui-info-button');
|
||||
expect(hostClasses).not.toContain('ui-info-button--pending');
|
||||
});
|
||||
|
||||
it('should add the "ui-info-button--pending" class when pending is true', () => {
|
||||
spectator.setInput('pending', true);
|
||||
spectator.detectChanges();
|
||||
|
||||
const hostClasses = spectator.element.classList;
|
||||
expect(hostClasses).toContain('ui-info-button');
|
||||
expect(hostClasses).toContain('ui-info-button--pending');
|
||||
});
|
||||
});
|
||||
44
libs/ui/buttons/src/lib/text-button.component.spec.ts
Normal file
44
libs/ui/buttons/src/lib/text-button.component.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { TextButtonComponent } from './text-button.component';
|
||||
|
||||
describe('TextButtonComponent', () => {
|
||||
let spectator: Spectator<TextButtonComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: TextButtonComponent,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have default classes for size and color', () => {
|
||||
const hostElement = spectator.element;
|
||||
// Expected default inputs: size 'medium', color 'normal'
|
||||
expect(hostElement.classList).toContain('ui-text-button');
|
||||
expect(hostElement.classList).toContain('ui-text-button__medium');
|
||||
expect(hostElement.classList).toContain('ui-text-button__normal');
|
||||
});
|
||||
|
||||
it('should update classes when inputs change', () => {
|
||||
spectator.setInput('size', 'large');
|
||||
spectator.setInput('color', 'primary');
|
||||
spectator.detectChanges();
|
||||
|
||||
const hostElement = spectator.element;
|
||||
expect(hostElement.classList).toContain('ui-text-button__large');
|
||||
expect(hostElement.classList).toContain('ui-text-button__primary');
|
||||
});
|
||||
|
||||
it('should correctly set the tabindex attribute', () => {
|
||||
const hostElement = spectator.element;
|
||||
expect(hostElement.getAttribute('tabindex')).toEqual('0');
|
||||
|
||||
spectator.setInput('tabIndex', 5);
|
||||
spectator.detectChanges();
|
||||
expect(hostElement.getAttribute('tabindex')).toEqual('5');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user