Merge branch 'refactor/convert-buildable-libs-to-non-buildable' into develop

Resolved conflicts:
- .gitignore: Added .claude to ignored files
- nx.json: Kept HEAD version with extra eslint.config.js exclusion
- package.json: Merged dependencies, updated vitest to v3.1.1 for compatibility
- eslint config files: Fixed merge conflicts and accepted conversion from .mjs to .js
- Removed deleted files from refactor branch
- Regenerated package-lock.json with --legacy-peer-deps

Build and tests pass successfully.
This commit is contained in:
Lorenz Hilpert
2025-06-30 20:52:05 +02:00
37 changed files with 1166 additions and 4391 deletions

View File

@@ -1,12 +0,0 @@
{
"permissions": {
"allow": [
"mcp__context7__resolve-library-id",
"mcp__context7__get-library-docs",
"Bash(npx nx test:*)",
"Bash(npm test)",
"Bash(npm run test:*)"
],
"deny": []
}
}

View File

@@ -4,7 +4,7 @@ applyTo: '**'
// This file is automatically generated by Nx Console
You are in an nx workspace using Nx 21.2.0 and npm as the package manager.
You are in an nx workspace using Nx 21.2.1 and npm as the package manager.
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:

3
.gitignore vendored
View File

@@ -58,6 +58,7 @@ libs/swagger/src/lib/*
.nx/cache
.nx/workspace-data
.angular
.claude
storybook-static
@@ -68,4 +69,4 @@ storybook-static
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*
vitest.config.*.timestamp*

View File

@@ -1,40 +1,40 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -37,4 +37,4 @@ module.exports = [
'@angular-eslint/prefer-standalone': 'off',
},
},
];
];

View File

@@ -1,7 +0,0 @@
# remission-shared
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remission-shared` to execute the unit tests.

View File

@@ -1,54 +0,0 @@
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../../eslint.config.js');
module.exports = [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs}'],
},
],
},
languageOptions: {
parser: require('jsonc-eslint-parser'),
},
},
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'remi',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'remi',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/prefer-standalone': 'off',
},
},
];

View File

@@ -1,21 +0,0 @@
export default {
displayName: 'remission-shared',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/remission/shared',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@@ -1,9 +0,0 @@
{
"name": "@isa/remission/shared",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^20.0.0",
"@angular/core": "^20.0.0"
},
"sideEffects": false
}

View File

@@ -1,3 +1,7 @@
# @isa/remission/shared/product-info
# remission-shared-product
Secondary entry point of `@isa/remission/shared`. It can be used by importing from `@isa/remission/shared/product-info`.
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remission-shared-product` to execute the unit tests.

View File

@@ -1,5 +0,0 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "remission-shared-select-remission-quantity-and-reason-dialog",
"name": "remission-shared-product",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/remission/shared/select-remission-quantity-and-reason-dialog/src",
"sourceRoot": "libs/remission/shared/product/src",
"prefix": "remi",
"projectType": "library",
"tags": [],
@@ -10,7 +10,7 @@
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../../coverage/libs/remission/shared/select-remission-quantity-and-reason-dialog"
"reportsDirectory": "../../../../coverage/libs/remission/shared/product"
}
},
"lint": {

View File

@@ -1,65 +1,329 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import {
ProductInfoComponent,
ProductInfoItem,
} from './product-info.component';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { MockDirectives } from 'ng-mocks';
describe('ProductInfoComponent', () => {
let spectator: Spectator<ProductInfoComponent>;
const createComponent = createComponentFactory({
component: ProductInfoComponent,
overrideComponents: [
[
ProductInfoComponent,
{
remove: {
imports: [ProductImageDirective, ProductRouterLinkDirective],
},
add: {
imports: MockDirectives(
ProductImageDirective,
ProductRouterLinkDirective,
),
},
},
],
],
});
const mockItem: ProductInfoItem = {
product: {
ean: '1234567890123',
name: 'Test Product',
contributors: '',
format: '',
formatDetail: '',
},
retailPrice: {
value: { value: 19.99, currency: 'EUR' },
},
};
beforeEach(() => {
spectator = createComponent({ props: { item: mockItem } });
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should set data-what and data-which attributes', () => {
const host = spectator.element;
expect(host).toHaveAttribute('data-what', 'product-info');
expect(host).toHaveAttribute('data-which', 'remission-product');
});
it('should set data-ean attribute from item.product.ean', () => {
const host = spectator.element;
expect(host).toHaveAttribute('data-ean', mockItem.product.ean);
});
// Add more tests for template rendering if template is available
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductInfoComponent, ProductInfoItem } from './product-info.component';
import { MockComponents, MockDirectives } from 'ng-mocks';
import { ProductFormatComponent } from '@isa/shared/product-foramt';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { By } from '@angular/platform-browser';
describe('ProductInfoComponent', () => {
let component: ProductInfoComponent;
let fixture: ComponentFixture<ProductInfoComponent>;
const mockProductItem: ProductInfoItem = {
product: {
id: 1,
name: 'Test Product',
ean: '1234567890123',
contributors: 'Test Contributors',
format: 'Test Format',
formatDetail: 'Test Format Detail',
},
retailPrice: {
value: {
value: 19.99,
currencySymbol: '€',
},
},
} as ProductInfoItem;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductInfoComponent],
})
.overrideComponent(ProductInfoComponent, {
remove: {
imports: [ProductFormatComponent, ProductImageDirective, ProductRouterLinkDirective],
},
add: {
imports: [
MockComponents(ProductFormatComponent),
MockDirectives(ProductImageDirective, ProductRouterLinkDirective),
],
},
})
.compileComponents();
fixture = TestBed.createComponent(ProductInfoComponent);
component = fixture.componentInstance;
});
describe('Component Setup and Initialization', () => {
it('should create', () => {
fixture.componentRef.setInput('item', mockProductItem);
expect(component).toBeTruthy();
});
it('should have default orientation as horizontal', () => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.detectChanges();
expect(component.orientation()).toBe('horizontal');
});
it('should accept vertical orientation', () => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.componentRef.setInput('orientation', 'vertical');
fixture.detectChanges();
expect(component.orientation()).toBe('vertical');
});
it('should have correct CSS classes', () => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.detectChanges();
expect(component.classList).toEqual([
'grid',
'grid-cols-[3.5rem,1fr]',
'gap-6',
'text-isa-neutral-900',
]);
});
it('should set host attributes correctly', () => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.detectChanges();
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-what')).toBe('product-info');
expect(hostElement.getAttribute('data-which')).toBe('remission-product');
expect(hostElement.getAttribute('data-ean')).toBe('1234567890123');
});
});
describe('Template Rendering', () => {
beforeEach(() => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.detectChanges();
});
it('should display product image with correct attributes', () => {
const productImage = fixture.debugElement.query(By.css('img'));
expect(productImage).toBeTruthy();
expect(productImage.nativeElement.getAttribute('data-what')).toBe('product-image');
expect(productImage.nativeElement.alt).toBe('Test Product');
});
it('should display product contributors', () => {
const contributorsElement = fixture.debugElement.query(
By.css('[data-what="product-contributors"]')
);
expect(contributorsElement).toBeTruthy();
expect(contributorsElement.nativeElement.textContent.trim()).toBe('Test Contributors');
});
it('should display product name', () => {
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameElement).toBeTruthy();
expect(nameElement.nativeElement.textContent.trim()).toBe('Test Product');
});
it('should display formatted price', () => {
const priceElement = fixture.debugElement.query(
By.css('[data-what="product-price"]')
);
expect(priceElement).toBeTruthy();
expect(priceElement.nativeElement.textContent.trim()).toBe('€19.99');
});
it('should display product EAN', () => {
const eanElement = fixture.debugElement.query(
By.css('[data-what="product-ean"]')
);
expect(eanElement).toBeTruthy();
expect(eanElement.nativeElement.textContent.trim()).toBe('1234567890123');
});
it('should render product format component with correct inputs', () => {
const formatComponent = fixture.debugElement.query(
By.css('shared-product-format')
);
expect(formatComponent).toBeTruthy();
expect(formatComponent.nativeElement.getAttribute('data-what')).toBe('product-format');
});
});
describe('Orientation Behavior', () => {
it('should apply horizontal layout classes when orientation is horizontal', () => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.componentRef.setInput('orientation', 'horizontal');
fixture.detectChanges();
const layoutDiv = fixture.debugElement.query(
By.css('.grid.grid-cols-\\[minmax\\(20rem\\,1fr\\)\\,auto\\]')
);
expect(layoutDiv).toBeTruthy();
expect(layoutDiv.nativeElement.classList.contains('gap-6')).toBe(true);
});
it('should apply vertical layout classes when orientation is vertical', () => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.componentRef.setInput('orientation', 'vertical');
fixture.detectChanges();
const layoutDiv = fixture.debugElement.query(By.css('.grid-flow-row'));
expect(layoutDiv).toBeTruthy();
expect(layoutDiv.nativeElement.classList.contains('gap-2')).toBe(true);
});
});
describe('Input Validation', () => {
it('should handle item input changes', () => {
const newItem: ProductInfoItem = {
...mockProductItem,
product: {
...mockProductItem.product,
name: 'Updated Product Name',
ean: '9876543210987',
},
};
fixture.componentRef.setInput('item', mockProductItem);
fixture.detectChanges();
fixture.componentRef.setInput('item', newItem);
fixture.detectChanges();
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameElement.nativeElement.textContent.trim()).toBe('Updated Product Name');
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-ean')).toBe('9876543210987');
});
it('should handle different currency symbols', () => {
const itemWithDifferentCurrency: ProductInfoItem = {
...mockProductItem,
retailPrice: {
value: {
value: 25.50,
currencySymbol: '$',
},
},
};
fixture.componentRef.setInput('item', itemWithDifferentCurrency);
fixture.detectChanges();
const priceElement = fixture.debugElement.query(
By.css('[data-what="product-price"]')
);
expect(priceElement.nativeElement.textContent.trim()).toBe('$25.50');
});
it('should handle zero price', () => {
const itemWithZeroPrice: ProductInfoItem = {
...mockProductItem,
retailPrice: {
value: {
value: 0,
currencySymbol: '€',
},
},
};
fixture.componentRef.setInput('item', itemWithZeroPrice);
fixture.detectChanges();
const priceElement = fixture.debugElement.query(
By.css('[data-what="product-price"]')
);
expect(priceElement.nativeElement.textContent.trim()).toBe('€0.00');
});
});
describe('Component Structure', () => {
beforeEach(() => {
fixture.componentRef.setInput('item', mockProductItem);
fixture.detectChanges();
});
it('should have proper grid structure', () => {
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.classList.contains('grid')).toBe(true);
expect(hostElement.classList.contains('grid-cols-[3.5rem,1fr]')).toBe(true);
expect(hostElement.classList.contains('gap-6')).toBe(true);
});
it('should maintain data attributes for testing', () => {
const dataWhatElements = fixture.debugElement.queryAll(
By.css('[data-what]')
);
expect(dataWhatElements.length).toBeGreaterThan(0);
const expectedDataWhatValues = [
'product-image',
'product-contributors',
'product-name',
'product-price',
'product-format',
'product-ean',
];
expectedDataWhatValues.forEach(value => {
const element = fixture.debugElement.query(
By.css(`[data-what="${value}"]`)
);
expect(element).toBeTruthy();
});
});
});
describe('Edge Cases', () => {
it('should handle empty product name', () => {
const itemWithEmptyName: ProductInfoItem = {
...mockProductItem,
product: {
...mockProductItem.product,
name: '',
},
};
fixture.componentRef.setInput('item', itemWithEmptyName);
fixture.detectChanges();
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameElement.nativeElement.textContent.trim()).toBe('');
});
it('should handle empty contributors', () => {
const itemWithEmptyContributors: ProductInfoItem = {
...mockProductItem,
product: {
...mockProductItem.product,
contributors: '',
},
};
fixture.componentRef.setInput('item', itemWithEmptyContributors);
fixture.detectChanges();
const contributorsElement = fixture.debugElement.query(
By.css('[data-what="product-contributors"]')
);
expect(contributorsElement.nativeElement.textContent.trim()).toBe('');
});
it('should handle long product names gracefully', () => {
const itemWithLongName: ProductInfoItem = {
...mockProductItem,
product: {
...mockProductItem.product,
name: 'This is a very long product name that might cause layout issues if not handled properly',
},
};
fixture.componentRef.setInput('item', itemWithLongName);
fixture.detectChanges();
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
);
expect(nameElement.nativeElement.textContent.trim()).toContain('This is a very long product name');
});
});
});

View File

@@ -1,67 +1,376 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ProductStockInfoComponent } from './product-stock-info.component';
describe('ProductStockInfoComponent', () => {
let spectator: Spectator<ProductStockInfoComponent>;
const createComponent = createComponentFactory(ProductStockInfoComponent);
beforeEach(() => {
spectator = createComponent();
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should display the current stock', () => {
spectator.setInput('stock', 42);
spectator.detectChanges();
const value = spectator.query(
'[data-what="stock-value"][data-which="current-stock"]',
);
expect(value).toHaveText('42x');
});
it('should display the remit amount', () => {
spectator.setInput('stockToRemit', 7);
spectator.detectChanges();
const value = spectator.query(
'[data-what="stock-value"][data-which="remit-amount"]',
);
expect(value).toHaveText('7x');
});
it('should display the remaining stock (targetStock)', () => {
spectator.setInput('stock', 20);
spectator.setInput('stockToRemit', 5);
spectator.detectChanges();
const value = spectator.query(
'[data-what="stock-value"][data-which="remaining-stock"]',
);
expect(value).toHaveText('15x');
});
it('should display the zob value', () => {
spectator.component.zob.set(99);
spectator.detectChanges();
const value = spectator.query(
'[data-what="stock-value"][data-which="zob"]',
);
expect(value).toHaveText('99x');
});
it('should render all labels with correct e2e attributes', () => {
const labels = [
{ which: 'current-stock', text: 'Aktueller Bestand' },
{ which: 'remit-amount', text: 'Remi Menge' },
{ which: 'remaining-stock', text: 'Übriger Bestand' },
{ which: 'zob', text: 'ZOB' },
];
labels.forEach(({ which, text }) => {
const label = spectator.query(
`[data-what="stock-label"][data-which="${which}"]`,
);
expect(label).toHaveText(text);
});
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductStockInfoComponent } from './product-stock-info.component';
import { By } from '@angular/platform-browser';
describe('ProductStockInfoComponent', () => {
let component: ProductStockInfoComponent;
let fixture: ComponentFixture<ProductStockInfoComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ProductStockInfoComponent],
}).compileComponents();
fixture = TestBed.createComponent(ProductStockInfoComponent);
component = fixture.componentInstance;
});
describe('Component Setup and Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have default stock value of 0', () => {
fixture.detectChanges();
expect(component.stock()).toBe(0);
});
it('should have default stockToRemit value of 0', () => {
fixture.detectChanges();
expect(component.stockToRemit()).toBe(0);
});
it('should have default zob signal value of 0', () => {
fixture.detectChanges();
expect(component.zob()).toBe(0);
});
it('should calculate targetStock correctly with default values', () => {
fixture.detectChanges();
expect(component.targetStock()).toBe(0);
});
});
describe('Input Signal Behavior', () => {
it('should accept and update stock input', () => {
fixture.componentRef.setInput('stock', 100);
fixture.detectChanges();
expect(component.stock()).toBe(100);
});
it('should accept and update stockToRemit input', () => {
fixture.componentRef.setInput('stockToRemit', 25);
fixture.detectChanges();
expect(component.stockToRemit()).toBe(25);
});
it('should update both inputs simultaneously', () => {
fixture.componentRef.setInput('stock', 150);
fixture.componentRef.setInput('stockToRemit', 40);
fixture.detectChanges();
expect(component.stock()).toBe(150);
expect(component.stockToRemit()).toBe(40);
});
});
describe('Computed Signal Behavior', () => {
it('should calculate targetStock correctly', () => {
fixture.componentRef.setInput('stock', 100);
fixture.componentRef.setInput('stockToRemit', 30);
fixture.detectChanges();
expect(component.targetStock()).toBe(70);
});
it('should recalculate targetStock when stock changes', () => {
fixture.componentRef.setInput('stock', 50);
fixture.componentRef.setInput('stockToRemit', 10);
fixture.detectChanges();
expect(component.targetStock()).toBe(40);
fixture.componentRef.setInput('stock', 80);
fixture.detectChanges();
expect(component.targetStock()).toBe(70);
});
it('should recalculate targetStock when stockToRemit changes', () => {
fixture.componentRef.setInput('stock', 100);
fixture.componentRef.setInput('stockToRemit', 20);
fixture.detectChanges();
expect(component.targetStock()).toBe(80);
fixture.componentRef.setInput('stockToRemit', 35);
fixture.detectChanges();
expect(component.targetStock()).toBe(65);
});
it('should handle negative targetStock values', () => {
fixture.componentRef.setInput('stock', 10);
fixture.componentRef.setInput('stockToRemit', 25);
fixture.detectChanges();
expect(component.targetStock()).toBe(-15);
});
it('should handle zero stock with positive stockToRemit', () => {
fixture.componentRef.setInput('stock', 0);
fixture.componentRef.setInput('stockToRemit', 10);
fixture.detectChanges();
expect(component.targetStock()).toBe(-10);
});
});
describe('Template Rendering', () => {
beforeEach(() => {
fixture.componentRef.setInput('stock', 100);
fixture.componentRef.setInput('stockToRemit', 25);
fixture.detectChanges();
});
it('should display current stock value', () => {
const currentStockElement = fixture.debugElement.query(
By.css('[data-which="current-stock"] [data-what="stock-value"]')
);
expect(currentStockElement).toBeTruthy();
expect(currentStockElement.nativeElement.textContent.trim()).toBe('100x');
});
it('should display current stock label', () => {
const currentStockLabel = fixture.debugElement.query(
By.css('[data-which="current-stock"] [data-what="stock-label"]')
);
expect(currentStockLabel).toBeTruthy();
expect(currentStockLabel.nativeElement.textContent.trim()).toBe('Aktueller Bestand');
});
it('should display remit amount value', () => {
const remitAmountElement = fixture.debugElement.query(
By.css('[data-which="remit-amount"] [data-what="stock-value"]')
);
expect(remitAmountElement).toBeTruthy();
expect(remitAmountElement.nativeElement.textContent.trim()).toBe('25x');
});
it('should display remit amount label', () => {
const remitAmountLabel = fixture.debugElement.query(
By.css('[data-which="remit-amount"] [data-what="stock-label"]')
);
expect(remitAmountLabel).toBeTruthy();
expect(remitAmountLabel.nativeElement.textContent.trim()).toBe('Remi Menge');
});
it('should display remaining stock value', () => {
const remainingStockElement = fixture.debugElement.query(
By.css('[data-which="remaining-stock"] [data-what="stock-value"]')
);
expect(remainingStockElement).toBeTruthy();
expect(remainingStockElement.nativeElement.textContent.trim()).toBe('75x');
});
it('should display remaining stock label', () => {
const remainingStockLabel = fixture.debugElement.query(
By.css('[data-which="remaining-stock"] [data-what="stock-label"]')
);
expect(remainingStockLabel).toBeTruthy();
expect(remainingStockLabel.nativeElement.textContent.trim()).toBe('Übriger Bestand');
});
it('should display ZOB value', () => {
const zobElement = fixture.debugElement.query(
By.css('[data-which="zob"] [data-what="stock-value"]')
);
expect(zobElement).toBeTruthy();
expect(zobElement.nativeElement.textContent.trim()).toBe('0x');
});
it('should display ZOB label', () => {
const zobLabel = fixture.debugElement.query(
By.css('[data-which="zob"] [data-what="stock-label"]')
);
expect(zobLabel).toBeTruthy();
expect(zobLabel.nativeElement.textContent.trim()).toBe('ZOB');
});
it('should have proper CSS classes on stock info rows', () => {
const stockInfoRows = fixture.debugElement.queryAll(
By.css('.product-stock-info-row')
);
expect(stockInfoRows.length).toBe(4);
stockInfoRows.forEach(row => {
expect(row.nativeElement.classList.contains('product-stock-info-row')).toBe(true);
expect(row.nativeElement.getAttribute('data-what')).toBe('stock-info-row');
});
});
it('should have correct data-which attributes', () => {
const dataWhichValues = ['current-stock', 'remit-amount', 'remaining-stock', 'zob'];
dataWhichValues.forEach(value => {
const element = fixture.debugElement.query(
By.css(`[data-which="${value}"]`)
);
expect(element).toBeTruthy();
});
});
});
describe('Signal Updates', () => {
it('should update zob signal value', () => {
expect(component.zob()).toBe(0);
component.zob.set(15);
fixture.detectChanges();
expect(component.zob()).toBe(15);
const zobElement = fixture.debugElement.query(
By.css('[data-which="zob"] [data-what="stock-value"]')
);
expect(zobElement.nativeElement.textContent.trim()).toBe('15x');
});
it('should handle multiple zob signal updates', () => {
component.zob.set(10);
fixture.detectChanges();
expect(component.zob()).toBe(10);
component.zob.set(25);
fixture.detectChanges();
expect(component.zob()).toBe(25);
component.zob.set(0);
fixture.detectChanges();
expect(component.zob()).toBe(0);
});
});
describe('Template Value Updates', () => {
it('should update template when stock input changes', () => {
fixture.componentRef.setInput('stock', 50);
fixture.detectChanges();
const currentStockElement = fixture.debugElement.query(
By.css('[data-which="current-stock"] [data-what="stock-value"]')
);
expect(currentStockElement.nativeElement.textContent.trim()).toBe('50x');
fixture.componentRef.setInput('stock', 75);
fixture.detectChanges();
expect(currentStockElement.nativeElement.textContent.trim()).toBe('75x');
});
it('should update template when stockToRemit input changes', () => {
fixture.componentRef.setInput('stockToRemit', 15);
fixture.detectChanges();
const remitAmountElement = fixture.debugElement.query(
By.css('[data-which="remit-amount"] [data-what="stock-value"]')
);
expect(remitAmountElement.nativeElement.textContent.trim()).toBe('15x');
fixture.componentRef.setInput('stockToRemit', 20);
fixture.detectChanges();
expect(remitAmountElement.nativeElement.textContent.trim()).toBe('20x');
});
it('should update remaining stock in template when computed value changes', () => {
fixture.componentRef.setInput('stock', 100);
fixture.componentRef.setInput('stockToRemit', 30);
fixture.detectChanges();
const remainingStockElement = fixture.debugElement.query(
By.css('[data-which="remaining-stock"] [data-what="stock-value"]')
);
expect(remainingStockElement.nativeElement.textContent.trim()).toBe('70x');
fixture.componentRef.setInput('stockToRemit', 45);
fixture.detectChanges();
expect(remainingStockElement.nativeElement.textContent.trim()).toBe('55x');
});
});
describe('Edge Cases', () => {
it('should handle large stock numbers', () => {
fixture.componentRef.setInput('stock', 999999);
fixture.componentRef.setInput('stockToRemit', 100000);
fixture.detectChanges();
expect(component.stock()).toBe(999999);
expect(component.stockToRemit()).toBe(100000);
expect(component.targetStock()).toBe(899999);
});
it('should handle negative stock values', () => {
fixture.componentRef.setInput('stock', -10);
fixture.componentRef.setInput('stockToRemit', 5);
fixture.detectChanges();
expect(component.stock()).toBe(-10);
expect(component.stockToRemit()).toBe(5);
expect(component.targetStock()).toBe(-15);
});
it('should handle decimal stock values', () => {
fixture.componentRef.setInput('stock', 10.5);
fixture.componentRef.setInput('stockToRemit', 2.3);
fixture.detectChanges();
expect(component.stock()).toBe(10.5);
expect(component.stockToRemit()).toBe(2.3);
expect(component.targetStock()).toBe(8.2);
});
it('should maintain change detection strategy', () => {
expect(component.constructor.name).toContain('ProductStockInfoComponent');
// Initial render
fixture.detectChanges();
// Verify OnPush change detection by checking that manual changes
// require detectChanges to be reflected in the template
const zobElement = fixture.debugElement.query(
By.css('[data-which="zob"] [data-what="stock-value"]')
);
expect(zobElement.nativeElement.textContent.trim()).toBe('0x');
// Change the signal value
component.zob.set(50);
// After detectChanges, template should update
fixture.detectChanges();
expect(zobElement.nativeElement.textContent.trim()).toBe('50x');
});
});
describe('Component Structure', () => {
beforeEach(() => {
fixture.componentRef.setInput('stock', 100);
fixture.componentRef.setInput('stockToRemit', 25);
fixture.detectChanges();
});
it('should render all required stock info rows', () => {
const stockInfoRows = fixture.debugElement.queryAll(
By.css('[data-what="stock-info-row"]')
);
expect(stockInfoRows.length).toBe(4);
});
it('should have proper styling classes', () => {
const boldElements = fixture.debugElement.queryAll(
By.css('.isa-text-body-2-bold')
);
expect(boldElements.length).toBe(4);
});
it('should maintain proper semantic structure', () => {
const stockLabels = fixture.debugElement.queryAll(
By.css('[data-what="stock-label"]')
);
const stockValues = fixture.debugElement.queryAll(
By.css('[data-what="stock-value"]')
);
expect(stockLabels.length).toBe(4);
expect(stockValues.length).toBe(4);
});
});
});

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -1,12 +1,21 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"target": "es2022",
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
@@ -17,12 +26,5 @@
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
]
}

View File

@@ -21,8 +21,7 @@
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/test-setup.ts"
"src/**/*.spec.jsx"
],
"include": ["src/**/*.ts"]
}

View File

@@ -4,10 +4,9 @@ import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig({
export default defineConfig(() => ({
root: __dirname,
cacheDir:
'../../../../node_modules/.vite/libs/remission/shared/select-remission-quantity-and-reason-dialog',
cacheDir: '../../../../node_modules/.vite/libs/remission/shared/product',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
@@ -17,13 +16,12 @@ export default defineConfig({
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: ['default'],
coverage: {
reportsDirectory:
'../../../../coverage/libs/remission/shared/select-remission-quantity-and-reason-dialog',
provider: 'v8',
reportsDirectory: '../../../../coverage/libs/remission/shared/product',
provider: 'v8' as const,
},
},
});
}));

View File

@@ -1,20 +0,0 @@
{
"name": "remission-shared",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/remission/shared/src",
"prefix": "remi",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/remission/shared/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -1,12 +1,12 @@
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);
import '@analogjs/vitest-angular/setup-zone';
import { getTestBed } from '@angular/core/testing';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -1,7 +0,0 @@
# remission-shared-select-remission-quantity-and-reason-dialog
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remission-shared-select-remission-quantity-and-reason-dialog` to execute the unit tests.

View File

@@ -1 +0,0 @@
export * from './lib/remission-shared-select-remission-quantity-and-reason-dialog/remission-shared-select-remission-quantity-and-reason-dialog.component';

View File

@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemissionSharedSelectRemissionQuantityAndReasonDialogComponent } from './remission-shared-select-remission-quantity-and-reason-dialog.component';
describe('RemissionSharedSelectRemissionQuantityAndReasonDialogComponent', () => {
let component: RemissionSharedSelectRemissionQuantityAndReasonDialogComponent;
let fixture: ComponentFixture<RemissionSharedSelectRemissionQuantityAndReasonDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RemissionSharedSelectRemissionQuantityAndReasonDialogComponent],
}).compileComponents();
fixture = TestBed.createComponent(
RemissionSharedSelectRemissionQuantityAndReasonDialogComponent,
);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -1,13 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'remi-remission-shared-select-remission-quantity-and-reason-dialog',
standalone: true,
imports: [CommonModule],
templateUrl:
'./remission-shared-select-remission-quantity-and-reason-dialog.component.html',
styleUrl:
'./remission-shared-select-remission-quantity-and-reason-dialog.component.css',
})
export class RemissionSharedSelectRemissionQuantityAndReasonDialogComponent {}

View File

@@ -1,12 +0,0 @@
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);

View File

View File

@@ -1,10 +0,0 @@
// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
globalThis.ngJest = {
testEnvironmentOptions: {
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
},
};
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv();

View File

@@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "es2022",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,18 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"moduleResolution": "bundler"
},
"exclude": [
"**/*.spec.ts",
"test-setup.ts",
"jest.config.ts",
"**/*.test.ts"
],
"include": ["**/*.ts"]
}

View File

@@ -1,16 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

4266
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -58,11 +58,12 @@
"zone.js": "~0.15.0"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "~1.10.0",
"@analogjs/vitest-angular": "~1.10.0",
"@analogjs/vite-plugin-angular": "~1.17.1",
"@analogjs/vitest-angular": "~1.17.1",
"@angular-devkit/build-angular": "20.0.3",
"@angular-devkit/core": "20.0.2",
"@angular-devkit/schematics": "20.0.2",
"@angular/build": "20.0.3",
"@angular/cli": "~20.0.0",
"@angular/compiler-cli": "20.0.3",
"@angular/language-service": "20.0.3",
@@ -89,8 +90,8 @@
"@types/node": "18.16.9",
"@types/uuid": "^10.0.0",
"@typescript-eslint/utils": "^8.33.1",
"@vitest/coverage-v8": "^1.0.4",
"@vitest/ui": "^1.3.1",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"angular-eslint": "20.1.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.28.0",
@@ -119,7 +120,7 @@
"typescript": "5.8.3",
"typescript-eslint": "^8.33.1",
"vite": "6.3.5",
"vitest": "^1.3.1"
"vitest": "^3.1.1"
},
"optionalDependencies": {
"@esbuild/linux-x64": "^0.25.5"

View File

@@ -74,19 +74,12 @@
"libs/remission/feature/remission-list/src/index.ts"
],
"@isa/remission/helpers": ["libs/remission/helpers/src/index.ts"],
"@isa/remission/shared": ["libs/remission/shared/src/index.ts"],
"@isa/remission/shared/product": [
"libs/remission/shared/product/src/index.ts"
],
"@isa/remission/shared/product-details": [
"libs/remission/shared/product-details/src/index.ts"
],
"@isa/remission/shared/search-item-to-remit-dialog": [
"libs/remission/shared/search-item-to-remit-dialog/src/index.ts"
],
"@isa/remission/shared/select-remission-quantity-and-reason-dialog": [
"libs/remission/shared/select-remission-quantity-and-reason-dialog/src/index.ts"
],
"@isa/shared/filter": ["libs/shared/filter/src/index.ts"],
"@isa/shared/product-foramt": ["libs/shared/product-format/src/index.ts"],
"@isa/shared/product-image": ["libs/shared/product-image/src/index.ts"],