Merged PR 1931: fix(remission-quantity-and-reason-item)

fix(remission-quantity-and-reason-item)
Ref: #5292
This commit is contained in:
Nino Righi
2025-09-02 15:20:44 +00:00
committed by Andreas Schickinger
parent 332699ca74
commit 708ec01704
9 changed files with 39 additions and 389 deletions

View File

@@ -25,11 +25,11 @@
</div>
<div
class="grid"
[class.grid-cols-[minmax(20rem,1fr),auto]]="!horizontal"
[ngClass]="!horizontal ? innerGridClass() : ''"
[class.gap-6]="!horizontal"
[class.grid-flow-row]="horizontal"
[class.gap-2]="horizontal"
class="grid"
>
<div class="grid grid-flow-row gap-2">
<div class="isa-text-body-2-bold" data-what="product-contributors">

View File

@@ -1,6 +1,6 @@
import { CurrencyPipe } from '@angular/common';
import { CurrencyPipe, NgClass } from '@angular/common';
import { Component, input } from '@angular/core';
import { RemissionItem, ReturnItem } from '@isa/remission/data-access';
import { RemissionItem } from '@isa/remission/data-access';
import { ProductImageDirective } from '@isa/shared/product-image';
import { ProductRouterLinkDirective } from '@isa/shared/product-router-link';
import { ProductFormatComponent } from '@isa/shared/product-foramt';
@@ -23,6 +23,7 @@ export const RemissionItemTags = {
selector: 'remi-product-info',
templateUrl: 'product-info.component.html',
imports: [
NgClass,
ProductImageDirective,
ProductRouterLinkDirective,
CurrencyPipe,
@@ -50,4 +51,6 @@ export class ProductInfoComponent {
item = input.required<ProductInfoItem>();
orientation = input<ProductInfoOrientation>('horizontal');
innerGridClass = input<string>('grid-cols-[minmax(20rem,1fr),auto]');
}

View File

@@ -2,7 +2,7 @@
<remi-select-remi-quantity-and-reason></remi-select-remi-quantity-and-reason>
} @else {
<button
class="absolute top-1 right-[1.33rem]"
class="absolute top-4 right-[1.33rem]"
type="button"
uiTextButton
size="small"

View File

@@ -1,3 +1,3 @@
:host {
@apply block h-full;
@apply block h-full mt-6;
}

View File

@@ -1,377 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
import { DialogComponent } from '@isa/ui/dialog';
import { MockComponents } from 'ng-mocks';
import { TextButtonComponent } from '@isa/ui/buttons';
import { SearchItemToRemitListComponent } from './search-item-to-remit-list.component';
import { SelectRemiQuantityAndReasonComponent } from './select-remi-quantity-and-reason.component';
import { signal } from '@angular/core';
import { Item } from '@isa/catalogue/data-access';
import { By } from '@angular/platform-browser';
describe('SearchItemToRemitDialogComponent', () => {
let component: SearchItemToRemitDialogComponent;
let fixture: ComponentFixture<SearchItemToRemitDialogComponent>;
let mockDialogRef: {
updateSize: ReturnType<typeof vi.fn>;
close: ReturnType<typeof vi.fn>;
};
let mockDialogComponent: {
title: ReturnType<typeof signal>;
};
const mockItem = {
id: 1,
product: {
id: 1,
name: 'Test Product',
},
catalogAvailability: {},
} as unknown as Item;
beforeEach(async () => {
mockDialogRef = {
updateSize: vi.fn(),
close: vi.fn(),
};
mockDialogComponent = {
title: signal(''),
};
const mockData = { searchTerm: 'test' };
await TestBed.configureTestingModule({
imports: [SearchItemToRemitDialogComponent],
providers: [
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DIALOG_DATA, useValue: mockData },
{ provide: DialogComponent, useValue: mockDialogComponent },
],
})
.overrideComponent(SearchItemToRemitDialogComponent, {
remove: {
imports: [
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
],
},
add: {
imports: MockComponents(
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
),
},
})
.compileComponents();
fixture = TestBed.createComponent(SearchItemToRemitDialogComponent);
component = fixture.componentInstance;
});
describe('Component Setup and Initialization', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should initialize searchTerm from string data', () => {
fixture.detectChanges();
expect(component.searchTerm()).toBe('test');
});
it('should initialize searchTerm from Signal data', async () => {
const searchTermSignal = signal('signal test');
const mockSignalData = { searchTerm: searchTermSignal };
TestBed.resetTestingModule();
await TestBed.configureTestingModule({
imports: [SearchItemToRemitDialogComponent],
providers: [
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DIALOG_DATA, useValue: mockSignalData },
{ provide: DialogComponent, useValue: mockDialogComponent },
],
})
.overrideComponent(SearchItemToRemitDialogComponent, {
remove: {
imports: [
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
],
},
add: {
imports: MockComponents(
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
),
},
})
.compileComponents();
fixture = TestBed.createComponent(SearchItemToRemitDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.searchTerm()).toBe('signal test');
// Test that it reacts to signal changes
searchTermSignal.set('updated signal test');
fixture.detectChanges();
expect(component.searchTerm()).toBe('updated signal test');
});
it('should initialize item signal as undefined', () => {
fixture.detectChanges();
expect(component.item()).toBeUndefined();
});
it('should extend DialogContentDirective', () => {
expect(component.dialogRef).toBeDefined();
expect(component.data).toBeDefined();
expect(component.close).toBeDefined();
});
});
describe('Signal and Effect Behavior', () => {
it('should update dialog size to auto when item is undefined', () => {
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('auto');
});
it('should update dialog size to 36rem when item is set', () => {
fixture.detectChanges();
mockDialogRef.updateSize.mockClear();
component.item.set(mockItem);
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('36rem');
});
it('should update searchTerm when linkedSignal source changes', () => {
const searchTermSignal = signal('initial');
const mockSignalData = { searchTerm: searchTermSignal };
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [SearchItemToRemitDialogComponent],
providers: [
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DIALOG_DATA, useValue: mockSignalData },
{ provide: DialogComponent, useValue: mockDialogComponent },
],
})
.overrideComponent(SearchItemToRemitDialogComponent, {
remove: {
imports: [
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
],
},
add: {
imports: MockComponents(
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
),
},
})
.compileComponents();
fixture = TestBed.createComponent(SearchItemToRemitDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.searchTerm()).toBe('initial');
searchTermSignal.set('updated');
fixture.detectChanges();
expect(component.searchTerm()).toBe('updated');
});
});
describe('Template Behavior', () => {
it('should show search list component when item is undefined', () => {
fixture.detectChanges();
const searchList = fixture.debugElement.query(
By.css('remi-search-item-to-remit-list'),
);
const selectQuantity = fixture.debugElement.query(
By.css('remi-select-remi-quantity-and-reason'),
);
expect(searchList).toBeTruthy();
expect(selectQuantity).toBeFalsy();
});
it('should show select quantity component when item is set', () => {
component.item.set(mockItem);
fixture.detectChanges();
const searchList = fixture.debugElement.query(
By.css('remi-search-item-to-remit-list'),
);
const selectQuantity = fixture.debugElement.query(
By.css('remi-select-remi-quantity-and-reason'),
);
expect(searchList).toBeFalsy();
expect(selectQuantity).toBeTruthy();
});
it('should show close button only when item is undefined', () => {
fixture.detectChanges();
let closeButton = fixture.debugElement.query(
By.css('button[uiTextButton]'),
);
expect(closeButton).toBeTruthy();
expect(closeButton.nativeElement.textContent.trim()).toBe('Schließen');
component.item.set(mockItem);
fixture.detectChanges();
closeButton = fixture.debugElement.query(By.css('button[uiTextButton]'));
expect(closeButton).toBeFalsy();
});
it('should call close with undefined when close button is clicked', () => {
fixture.detectChanges();
const closeButton = fixture.debugElement.query(
By.css('button[uiTextButton]'),
);
closeButton.nativeElement.click();
expect(mockDialogRef.close).toHaveBeenCalledWith(undefined);
});
it('should have correct button attributes', () => {
fixture.detectChanges();
const closeButton = fixture.debugElement.query(By.css('button'));
const buttonEl = closeButton.nativeElement;
expect(buttonEl.type).toBe('button');
expect(buttonEl.classList.contains('absolute')).toBe(true);
expect(buttonEl.classList.contains('top-1')).toBe(true);
expect(buttonEl.classList.contains('right-[1.33rem]')).toBe(true);
});
});
describe('DialogRef Integration', () => {
it('should call dialogRef.updateSize on initialization', () => {
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('auto');
});
it('should call dialogRef.updateSize when item changes', () => {
fixture.detectChanges();
mockDialogRef.updateSize.mockClear();
component.item.set(mockItem);
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('36rem');
component.item.set(undefined);
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('auto');
});
it('should inherit close method from DialogContentDirective', () => {
const closeSpy = vi.spyOn(component, 'close');
component.close(mockItem);
expect(closeSpy).toHaveBeenCalledWith(mockItem);
expect(mockDialogRef.close).toHaveBeenCalledWith(mockItem);
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle empty searchTerm', async () => {
const emptyData = { searchTerm: '' };
TestBed.resetTestingModule();
await TestBed.configureTestingModule({
imports: [SearchItemToRemitDialogComponent],
providers: [
{ provide: DialogRef, useValue: mockDialogRef },
{ provide: DIALOG_DATA, useValue: emptyData },
{ provide: DialogComponent, useValue: mockDialogComponent },
],
})
.overrideComponent(SearchItemToRemitDialogComponent, {
remove: {
imports: [
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
],
},
add: {
imports: MockComponents(
TextButtonComponent,
SearchItemToRemitListComponent,
SelectRemiQuantityAndReasonComponent,
),
},
})
.compileComponents();
fixture = TestBed.createComponent(SearchItemToRemitDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
expect(component.searchTerm()).toBe('');
});
it('should handle multiple rapid item changes', () => {
fixture.detectChanges();
mockDialogRef.updateSize.mockClear();
// First change
component.item.set(mockItem);
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('36rem');
// Second change
component.item.set(undefined);
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('auto');
// Third change
component.item.set(mockItem);
fixture.detectChanges();
expect(mockDialogRef.updateSize).toHaveBeenCalledWith('36rem');
// Total calls
expect(mockDialogRef.updateSize).toHaveBeenCalledTimes(3);
});
it('should handle component destruction gracefully', () => {
fixture.detectChanges();
// Component destruction should not throw errors
expect(() => {
fixture.destroy();
}).not.toThrow();
});
it('should maintain data integrity', () => {
const originalData = { searchTerm: 'test' };
fixture.detectChanges();
// Data should remain unchanged
expect(component.data).toEqual(originalData);
expect(component.data.searchTerm).toBe('test');
});
});
});

View File

@@ -20,16 +20,25 @@
></ui-icon-button>
</ui-search-bar>
<p
class="text-isa-neutral-600 isa-text-body-1-regular pb-4 border-b border-b-isa-neutral-300"
class="relative text-isa-neutral-600 isa-text-body-1-regular pb-4 border-b border-b-isa-neutral-300"
>
Sie können Artikel die nicht auf der Remi Liste stehen direkt zum
Warenbegleitschein hinzufügen.
<button
class="absolute ml-1 w-6 h-6 inline-flex items-center justify-center text-isa-accent-blue"
uiTooltip
[content]="'Es werden nur Artikel mit Bestand angezeigt'"
[triggerOn]="['click', 'hover']"
>
<ng-icon size="1.5rem" name="isaOtherInfo"></ng-icon>
</button>
</p>
<div class="overflow-y-auto">
@if (searchResource.value()?.result; as items) {
@for (item of items; track item.id) {
@defer {
<remi-search-item-to-remit
<remi-search-item-to-remit
[item]="item"
data-what="list-item"
data-which="search-result"

View File

@@ -6,13 +6,13 @@ import {
resource,
signal,
} from '@angular/core';
import { isaActionSearch } from '@isa/icons';
import { isaActionSearch, isaOtherInfo } from '@isa/icons';
import { IconButtonComponent } from '@isa/ui/buttons';
import {
UiSearchBarClearComponent,
UiSearchBarComponent,
} from '@isa/ui/search-bar';
import { provideIcons } from '@ng-icons/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { SearchItemToRemitComponent } from './search-item-to-remit.component';
import { FormsModule } from '@angular/forms';
import { DEFAULT_LIST_RESPONSE_ARGS_OF_ITEM } from './constants';
@@ -27,7 +27,7 @@ import {
} from '@isa/common/data-access';
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { TooltipDirective } from '@isa/ui/tooltip';
@Component({
selector: 'remi-search-item-to-remit-list',
templateUrl: './search-item-to-remit-list.component.html',
@@ -41,8 +41,10 @@ import { CdkTrapFocus } from '@angular/cdk/a11y';
FormsModule,
SearchItemToRemitComponent,
CdkTrapFocus,
TooltipDirective,
NgIcon,
],
providers: [provideIcons({ isaActionSearch })],
providers: [provideIcons({ isaActionSearch, isaOtherInfo })],
})
export class SearchItemToRemitListComponent implements OnInit {
host = inject(SearchItemToRemitDialogComponent);

View File

@@ -4,6 +4,7 @@
retailPrice: item().catalogAvailability.price,
}"
[orientation]="productInfoOrientation()"
[innerGridClass]="'grid-cols-[minmax(20rem,1fr),minmax(18rem,auto)]'"
></remi-product-info>
<div class="text-right">
<button