mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
✨ Add input controls and checkbox component
Introduced a new input controls library with a checkbox component. - ✨ **Feature**: Added input controls library with checkbox component - 🎨 **Style**: Updated checkbox component styles and structure - 🧪 **Test**: Added unit tests for checkbox and empty state components - 🛠️ **Refactor**: Improved checkbox component code and removed unused styles - 📚 **Docs**: Updated commit message guidelines in VSCode settings
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -23,7 +23,7 @@
|
||||
],
|
||||
"github.copilot.chat.commitMessageGeneration.instructions": [
|
||||
{
|
||||
"file": "docs/guidelines/commit-message.md"
|
||||
"file": ".github/commit-instructions.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.codeGeneration.instructions": [
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
@use "../../../libs/ui/buttons/src/buttons.scss";
|
||||
@use "../../../libs/ui/input-controls/src/input-controls.scss";
|
||||
@use "../../../libs/ui/progress-bar/src/lib/progress-bar.scss";
|
||||
|
||||
@@ -2,13 +2,47 @@
|
||||
|
||||
## Unit Testing Requirements
|
||||
|
||||
- Test files should end with `.spec.ts`.
|
||||
- Use Spectator for Component, Directive and Service tests.
|
||||
- Use Jest as the test runner.
|
||||
- Follow the Arrange-Act-Assert (AAA) pattern in tests.
|
||||
- Mock external dependencies to isolate the unit under test.
|
||||
- Test files should end with `.spec.ts`
|
||||
- Use Spectator for Component, Directive and Service tests
|
||||
- Use Jest as the test runner
|
||||
- Follow the Arrange-Act-Assert (AAA) pattern in tests
|
||||
- Mock external dependencies to isolate the unit under test
|
||||
- Mock child components to ensure true unit testing isolation
|
||||
|
||||
## Example Test Structure
|
||||
## Best Practices
|
||||
|
||||
### Component Testing
|
||||
|
||||
- Use `createComponentFactory` for standalone components
|
||||
- Use `createHostFactory` when testing components with templates
|
||||
- Mock child components using `ng-mocks`
|
||||
- Test component inputs, outputs, and lifecycle hooks
|
||||
- Verify DOM rendering and component behavior separately
|
||||
|
||||
### Mocking Child Components
|
||||
|
||||
Always mock child components to:
|
||||
|
||||
- Isolate the component under test
|
||||
- Prevent unintended side effects
|
||||
- Reduce test complexity
|
||||
- Improve test performance
|
||||
|
||||
```typescript
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { ChildComponent } from './child.component';
|
||||
|
||||
describe('ParentComponent', () => {
|
||||
const createComponent = createComponentFactory({
|
||||
component: ParentComponent,
|
||||
declarations: [MockComponent(ChildComponent)],
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Example Test Structures
|
||||
|
||||
### Basic Component Test
|
||||
|
||||
```typescript
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
@@ -27,9 +61,135 @@ describe('MyComponent', () => {
|
||||
});
|
||||
|
||||
it('should handle action correctly', () => {
|
||||
// Arrange
|
||||
spectator.setInput('inputProp', 'testValue');
|
||||
|
||||
// Act
|
||||
spectator.click('button');
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.outputProp).toBe('expectedValue');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Host Component Test with Child Components
|
||||
|
||||
```typescript
|
||||
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
|
||||
import { ParentComponent } from './parent.component';
|
||||
import { ChildComponent } from './child.component';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
|
||||
describe('ParentComponent', () => {
|
||||
let spectator: SpectatorHost<ParentComponent>;
|
||||
|
||||
const createHost = createHostFactory({
|
||||
component: ParentComponent,
|
||||
declarations: [MockComponent(ChildComponent)],
|
||||
template: `
|
||||
<app-parent
|
||||
[input]="inputValue"
|
||||
(output)="handleOutput($event)">
|
||||
</app-parent>`,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createHost(undefined, {
|
||||
hostProps: {
|
||||
inputValue: 'test',
|
||||
handleOutput: jest.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass input to child component', () => {
|
||||
// Arrange
|
||||
const childComponent = spectator.query(ChildComponent);
|
||||
|
||||
// Assert
|
||||
expect(childComponent.input).toBe('test');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Events and Outputs
|
||||
|
||||
```typescript
|
||||
it('should emit when button is clicked', () => {
|
||||
// Arrange
|
||||
const outputSpy = jest.fn();
|
||||
spectator.component.outputEvent.subscribe(outputSpy);
|
||||
|
||||
// Act
|
||||
spectator.click('button');
|
||||
|
||||
// Assert
|
||||
expect(outputSpy).toHaveBeenCalledWith(expectedValue);
|
||||
});
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Query Elements
|
||||
|
||||
```typescript
|
||||
// By CSS selector
|
||||
const element = spectator.query('.class-name');
|
||||
|
||||
// By directive/component
|
||||
const child = spectator.query(ChildComponent);
|
||||
|
||||
// Multiple elements
|
||||
const elements = spectator.queryAll('.item');
|
||||
```
|
||||
|
||||
### Trigger Events
|
||||
|
||||
```typescript
|
||||
// Click events
|
||||
spectator.click('.button');
|
||||
spectator.click(buttonElement);
|
||||
|
||||
// Input events
|
||||
spectator.typeInElement('value', 'input');
|
||||
|
||||
// Custom events
|
||||
spectator.triggerEventHandler(MyComponent, 'eventName', eventValue);
|
||||
```
|
||||
|
||||
### Test Async Operations
|
||||
|
||||
```typescript
|
||||
it('should handle async operations', async () => {
|
||||
// Arrange
|
||||
const response = { data: 'test' };
|
||||
service.getData.mockResolvedValue(response);
|
||||
|
||||
// Act
|
||||
await spectator.component.loadData();
|
||||
|
||||
// Assert
|
||||
expect(spectator.component.data).toEqual(response);
|
||||
});
|
||||
```
|
||||
|
||||
## Tips and Tricks
|
||||
|
||||
1. **Debugging Tests**
|
||||
|
||||
- Use `spectator.debug()` to log the current DOM state
|
||||
- Use `console.log` sparingly and remove before committing
|
||||
- Set breakpoints in your IDE for step-by-step debugging
|
||||
|
||||
2. **Common Pitfalls**
|
||||
|
||||
- Don't test implementation details
|
||||
- Avoid testing third-party libraries
|
||||
- Don't test multiple concerns in a single test
|
||||
- Remember to clean up subscriptions
|
||||
|
||||
3. **Performance**
|
||||
- Mock heavy dependencies
|
||||
- Keep test setup minimal
|
||||
- Use `beforeAll` for expensive operations shared across tests
|
||||
|
||||
29
libs/ui/empty-state/src/lib/empty-state.component.spec.ts
Normal file
29
libs/ui/empty-state/src/lib/empty-state.component.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
import { EmptyStateComponent } from './empty-state.component';
|
||||
|
||||
describe('EmptyStateComponent', () => {
|
||||
let spectator: Spectator<EmptyStateComponent>;
|
||||
const createComponent = createComponentFactory(EmptyStateComponent);
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent({
|
||||
props: {
|
||||
title: 'Test Title',
|
||||
description: 'Test Description',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the component', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set title and description inputs correctly', () => {
|
||||
expect(spectator.component.title()).toEqual('Test Title');
|
||||
expect(spectator.component.description()).toEqual('Test Description');
|
||||
});
|
||||
|
||||
it('should apply the host class "ui-empty-state"', () => {
|
||||
expect(spectator.element.classList.contains('ui-empty-state')).toBe(true);
|
||||
});
|
||||
});
|
||||
1
libs/ui/input-controls/src/input-controls.scss
Normal file
1
libs/ui/input-controls/src/input-controls.scss
Normal file
@@ -0,0 +1 @@
|
||||
@use "./lib/checkbox/checkbox";
|
||||
@@ -2,11 +2,11 @@
|
||||
@apply relative inline-flex p-3 items-center justify-center rounded-lg bg-isa-white size-6 border border-solid border-isa-neutral-900;
|
||||
font-size: 1.5rem;
|
||||
|
||||
ng-icon {
|
||||
.ui-checkbox__icon {
|
||||
@apply invisible min-w-6 size-6 text-isa-white;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -16,22 +16,22 @@
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
&:has(input[type='checkbox']:checked) {
|
||||
&:has(input[type="checkbox"]:checked) {
|
||||
@apply bg-isa-neutral-900 text-isa-white;
|
||||
|
||||
ng-icon {
|
||||
.ui-checkbox__icon {
|
||||
@apply visible;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input[type='checkbox']:disabled) {
|
||||
&:has(input[type="checkbox"]:disabled) {
|
||||
@apply bg-isa-neutral-400 border-isa-neutral-400 cursor-default;
|
||||
|
||||
&:has(input[type='checkbox']:checked) {
|
||||
&:has(input[type="checkbox"]:checked) {
|
||||
@apply bg-isa-neutral-400 border-isa-neutral-400;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type="checkbox"] {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,11 @@
|
||||
|
||||
@apply rounded-full bg-isa-neutral-300 size-12;
|
||||
|
||||
ng-icon {
|
||||
.ui-checkbox__icon {
|
||||
@apply invisible size-6 text-isa-neutral-100;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type="checkbox"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -65,10 +65,10 @@
|
||||
@apply bg-isa-neutral-400;
|
||||
}
|
||||
|
||||
&:has(input[type='checkbox']:checked) {
|
||||
&:has(input[type="checkbox"]:checked) {
|
||||
@apply bg-isa-neutral-700;
|
||||
|
||||
ng-icon {
|
||||
.ui-checkbox__icon {
|
||||
@apply visible;
|
||||
}
|
||||
|
||||
@@ -77,14 +77,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input[type='checkbox']:disabled) {
|
||||
&:has(input[type="checkbox"]:disabled) {
|
||||
@apply bg-isa-neutral-400 cursor-default;
|
||||
|
||||
&:has(input[type='checkbox']:checked) {
|
||||
&:has(input[type="checkbox"]:checked) {
|
||||
@apply bg-isa-neutral-700;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
input[type="checkbox"] {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
<ng-content select="input[type=checkbox]"></ng-content> <ng-icon name="isaActionCheck"></ng-icon>
|
||||
<ng-content select="input[type=checkbox]"></ng-content>
|
||||
<ng-icon class="ui-checkbox__icon" name="isaActionCheck"></ng-icon>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
|
||||
import { CheckboxComponent, CheckboxAppearance } from './checkbox.component';
|
||||
import { MockComponent } from 'ng-mocks';
|
||||
import { NgIconComponent } from '@ng-icons/core';
|
||||
|
||||
describe('CheckboxComponent', () => {
|
||||
let spectator: SpectatorHost<CheckboxComponent>;
|
||||
const createHost = createHostFactory({
|
||||
component: CheckboxComponent,
|
||||
declarations: [MockComponent(NgIconComponent)],
|
||||
template: `<ui-checkbox [appearance]="appearance"><input type="checkbox" />></ui-checkbox>`,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createHost(undefined, {
|
||||
hostProps: {
|
||||
appearance: CheckboxAppearance.Checkbox,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(spectator.component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should use default appearance as Checkbox', () => {
|
||||
// Assert default appearance value and computed class
|
||||
expect(spectator.component.appearance()).toEqual(CheckboxAppearance.Checkbox);
|
||||
expect(spectator.component.appearanceClass()).toEqual('ui-checkbox__checkbox');
|
||||
});
|
||||
|
||||
it('should compute bullet class when appearance is set to Bullet', () => {
|
||||
// Act: update appearance signal to Bullet
|
||||
spectator.setHostInput('appearance', CheckboxAppearance.Bullet);
|
||||
|
||||
// Assert updated appearance & computed class
|
||||
expect(spectator.component.appearance()).toEqual(CheckboxAppearance.Bullet);
|
||||
expect(spectator.component.appearanceClass()).toEqual('ui-checkbox__bullet');
|
||||
});
|
||||
|
||||
it('should render checkbox input element', () => {
|
||||
// Arrange & Act
|
||||
const checkbox = spectator.query('input[type="checkbox"]');
|
||||
|
||||
// Assert
|
||||
expect(checkbox).toBeTruthy();
|
||||
expect(checkbox).toHaveAttribute('type', 'checkbox');
|
||||
});
|
||||
});
|
||||
@@ -13,13 +13,11 @@ export const CheckboxAppearance = {
|
||||
Checkbox: 'checkbox',
|
||||
} as const;
|
||||
|
||||
export type CheckboxAppearance =
|
||||
(typeof CheckboxAppearance)[keyof typeof CheckboxAppearance];
|
||||
export type CheckboxAppearance = (typeof CheckboxAppearance)[keyof typeof CheckboxAppearance];
|
||||
|
||||
@Component({
|
||||
selector: 'ui-checkbox',
|
||||
templateUrl: './checkbox.component.html',
|
||||
styleUrls: ['./checkbox.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [NgIconComponent],
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -101,6 +101,7 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"karma-junit-reporter": "~2.0.1",
|
||||
"ng-mocks": "^14.13.4",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "20.4.6",
|
||||
"postcss": "^8.5.3",
|
||||
@@ -23807,6 +23808,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ng-mocks": {
|
||||
"version": "14.13.4",
|
||||
"resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.13.4.tgz",
|
||||
"integrity": "sha512-OFpzcx9vzeMqpVaBaukH8gvHRhor8iAkc8pOWakSPwxD3DuoHyDrjb/odgjI3Jq+Iaerqb3js1I4Sluu+0rLSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/help-me-mom"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19",
|
||||
"@angular/core": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19",
|
||||
"@angular/forms": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19",
|
||||
"@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19"
|
||||
}
|
||||
},
|
||||
"node_modules/ng-swagger-gen": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ng-swagger-gen/-/ng-swagger-gen-2.3.1.tgz",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"karma-junit-reporter": "~2.0.1",
|
||||
"ng-mocks": "^14.13.4",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "20.4.6",
|
||||
"postcss": "^8.5.3",
|
||||
|
||||
Reference in New Issue
Block a user