Merged PR 1903: fix(remission-list, product-info, search-item-to-remit): improve responsive l...

fix(remission-list, product-info, search-item-to-remit): improve responsive layout and fix orientation logic

- Fix grid layout responsiveness in remission-list-item component by updating breakpoint conditions from mobileBreakpoint to desktopBreakpoint
- Correct product-info orientation logic to properly apply horizontal/vertical layouts based on breakpoint state
- Add consistent orientation handling to search-item-to-remit component with proper breakpoint detection
- Update CSS classes to use desktop-large breakpoint for better grid column management
- Add bottom margin to remission list container to prevent overlap with fixed action button
- Enhance test coverage for new computed properties and breakpoint-dependent behavior

Ref: #5239
This commit is contained in:
Nino Righi
2025-07-31 16:44:06 +00:00
committed by Andreas Schickinger
parent ad00899b6e
commit d7d535c10d
9 changed files with 278 additions and 260 deletions

View File

@@ -5,7 +5,7 @@
[item]="i"
[orientation]="remiProductInfoOrientation()"
></remi-product-info>
@if (displayActions() && mobileBreakpoint()) {
@if (displayActions() && !desktopBreakpoint()) {
<remi-feature-remission-list-item-select
class="self-start mt-4"
[item]="i"
@@ -32,10 +32,8 @@
</ui-item-row-data>
@if (displayActions()) {
<ui-item-row-data
class="justify-end desktop-small:justify-between col-end-last"
>
@if (!mobileBreakpoint()) {
<ui-item-row-data class="justify-end desktop:justify-between col-end-last">
@if (desktopBreakpoint()) {
<remi-feature-remission-list-item-select
class="self-end mt-4"
[item]="i"

View File

@@ -3,7 +3,11 @@
}
.ui-client-row {
@apply isa-desktop-l:grid-cols-4;
@apply isa-desktop:grid-cols-2 desktop-large:grid-cols-4;
}
.ui-client-row-content {
@apply isa-desktop:col-span-2 desktop-large:col-span-1;
}
.col-end-last {

View File

@@ -16,6 +16,7 @@ import {
import { MockComponent } from 'ng-mocks';
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
import { signal } from '@angular/core';
import { of } from 'rxjs';
// --- Setup dynamic mocking for injectRemissionListType ---
let remissionListTypeValue: RemissionListType = RemissionListType.Pflicht;
@@ -26,7 +27,9 @@ jest.mock('../injects/inject-remission-list-type', () => ({
// Mock the calculation functions to have predictable behavior
jest.mock('@isa/remission/data-access', () => ({
...jest.requireActual('@isa/remission/data-access'),
calculateStockToRemit: jest.fn(),
getStockToRemit: jest.fn(),
calculateAvailableStock: jest.fn(),
calculateTargetStock: jest.fn(),
}));
// Mock the RemissionStore
@@ -57,7 +60,6 @@ describe('RemissionListItemComponent', () => {
): ReturnItem =>
({
id: 1,
predefinedReturnQuantity: 5,
remainingQuantityInStock: 10,
...overrides,
}) as ReturnItem;
@@ -71,7 +73,6 @@ describe('RemissionListItemComponent', () => {
returnItem: {
data: {
id: 1,
predefinedReturnQuantity: 10,
},
},
...overrides,
@@ -110,9 +111,15 @@ describe('RemissionListItemComponent', () => {
mockRemissionStore.selectedQuantity.set({});
mockRemissionStore.remissionStarted.set(true);
// Reset the mocked function to return 0 by default
const { calculateStockToRemit } = require('@isa/remission/data-access');
calculateStockToRemit.mockReturnValue(0);
// Reset the mocked functions to return default values
const {
getStockToRemit,
calculateAvailableStock,
calculateTargetStock,
} = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(0);
calculateAvailableStock.mockReturnValue(100);
calculateTargetStock.mockReturnValue(100);
});
describe('Component Setup', () => {
@@ -137,263 +144,245 @@ describe('RemissionListItemComponent', () => {
});
});
describe('predefinedReturnQuantity computed signal', () => {
describe('with ReturnItem', () => {
beforeEach(() => setRemissionListType(RemissionListType.Pflicht));
describe('computed properties', () => {
describe('availableStock', () => {
it('should calculate available stock correctly', () => {
const {
calculateAvailableStock,
} = require('@isa/remission/data-access');
calculateAvailableStock.mockReturnValue(90);
it('should return predefinedReturnQuantity when available', () => {
const mockItem = createMockReturnItem({ predefinedReturnQuantity: 15 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(15);
});
it('should return 0 when predefinedReturnQuantity is null', () => {
const mockItem = createMockReturnItem({
predefinedReturnQuantity: null as any,
const mockStock = createMockStockInfo({
inStock: 100,
removedFromStock: 10,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.componentRef.setInput('item', createMockReturnItem());
fixture.componentRef.setInput('stock', mockStock);
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
it('should return 0 when predefinedReturnQuantity is undefined', () => {
const mockItem = createMockReturnItem({
predefinedReturnQuantity: undefined,
expect(component.availableStock()).toBe(90);
expect(calculateAvailableStock).toHaveBeenCalledWith({
stock: 100,
removedFromStock: 10,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
it('should return 0 when predefinedReturnQuantity is 0', () => {
const mockItem = createMockReturnItem({ predefinedReturnQuantity: 0 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
});
describe('with ReturnSuggestion', () => {
beforeEach(() => setRemissionListType(RemissionListType.Abteilung));
describe('stockToRemit', () => {
it('should calculate stock to remit correctly', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(25);
it('should return predefinedReturnQuantity from returnItem.data when available', () => {
const mockSuggestion = createMockReturnSuggestion({
returnItem: {
data: {
id: 1,
predefinedReturnQuantity: 25,
},
},
});
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(25);
});
it('should return 0 when returnItem.data.predefinedReturnQuantity is null', () => {
const mockSuggestion = createMockReturnSuggestion({
returnItem: {
data: {
id: 1,
predefinedReturnQuantity: null as any,
},
},
});
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
it('should return 0 when returnItem.data.predefinedReturnQuantity is undefined', () => {
const mockSuggestion = createMockReturnSuggestion({
returnItem: {
data: {
id: 1,
predefinedReturnQuantity: undefined,
},
},
});
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
it('should return 0 when returnItem is null', () => {
const mockSuggestion = createMockReturnSuggestion({
returnItem: null as any,
});
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
it('should return 0 when returnItem.data is null', () => {
const mockSuggestion = createMockReturnSuggestion({
returnItem: {
data: null as any,
},
});
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
});
});
describe('Type detection', () => {
it('should correctly identify ReturnSuggestion type', () => {
setRemissionListType(RemissionListType.Abteilung);
const mockSuggestion = createMockReturnSuggestion();
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
const item = component.item();
expect('returnItem' in item).toBe(true);
expect('predefinedReturnQuantity' in item).toBe(false);
});
it('should correctly identify ReturnItem type', () => {
setRemissionListType(RemissionListType.Pflicht);
const mockItem = createMockReturnItem();
const mockStock = createMockStockInfo();
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', mockStock);
fixture.detectChanges();
expect(component.stockToRemit()).toBe(25);
expect(getStockToRemit).toHaveBeenCalledWith({
remissionItem: mockItem,
remissionListType: remissionListTypeValue,
availableStock: 100, // default mock value
});
});
});
describe('targetStock', () => {
it('should calculate target stock correctly', () => {
const { calculateTargetStock } = require('@isa/remission/data-access');
calculateTargetStock.mockReturnValue(75);
const mockItem = createMockReturnItem({ remainingQuantityInStock: 15 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
const item = component.item();
expect('returnItem' in item).toBe(false);
expect('predefinedReturnQuantity' in item).toBe(true);
expect(component.targetStock()).toBe(75);
expect(calculateTargetStock).toHaveBeenCalledWith({
availableStock: 100, // default mock value
stockToRemit: 0, // default mock value
remainingQuantityInStock: 15,
});
});
});
describe('remainingQuantityInStock', () => {
it('should return remainingQuantityInStock from item', () => {
const mockItem = createMockReturnItem({ remainingQuantityInStock: 42 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.remainingQuantityInStock()).toBe(42);
});
it('should handle undefined remainingQuantityInStock', () => {
const mockItem = createMockReturnItem({
remainingQuantityInStock: undefined,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.remainingQuantityInStock()).toBeUndefined();
});
});
describe('selectedStockToRemit', () => {
it('should return selected quantity from store', () => {
mockRemissionStore.selectedQuantity.set({ 1: 15 });
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.selectedStockToRemit()).toBe(15);
});
it('should return undefined when no selected quantity exists', () => {
mockRemissionStore.selectedQuantity.set({});
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.selectedStockToRemit()).toBeUndefined();
});
});
describe('displayActions', () => {
it('should return true when stockToRemit > 0 and remission started', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(5);
mockRemissionStore.remissionStarted.set(true);
fixture.componentRef.setInput('item', createMockReturnItem());
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.displayActions()).toBe(true);
});
it('should return false when stockToRemit is 0', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(0);
mockRemissionStore.remissionStarted.set(true);
fixture.componentRef.setInput('item', createMockReturnItem());
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.displayActions()).toBe(false);
});
it('should return false when remission has not started', () => {
const { getStockToRemit } = require('@isa/remission/data-access');
getStockToRemit.mockReturnValue(5);
mockRemissionStore.remissionStarted.set(false);
fixture.componentRef.setInput('item', createMockReturnItem());
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.displayActions()).toBe(false);
});
});
});
describe('Component reactivity', () => {
it('should update predefinedReturnQuantity when input changes from ReturnItem to ReturnSuggestion', () => {
describe('Component behavior with different remission list types', () => {
it('should call getStockToRemit with correct parameters for Pflicht type', () => {
setRemissionListType(RemissionListType.Pflicht);
const mockItem = createMockReturnItem({ predefinedReturnQuantity: 5 });
const { getStockToRemit } = require('@isa/remission/data-access');
const mockItem = createMockReturnItem();
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(5);
// Access the computed property to trigger the calculation
component.stockToRemit();
setRemissionListType(RemissionListType.Abteilung);
const mockSuggestion = createMockReturnSuggestion({
returnItem: {
data: {
id: 1,
predefinedReturnQuantity: 20,
},
},
expect(getStockToRemit).toHaveBeenCalledWith({
remissionItem: mockItem,
remissionListType: RemissionListType.Pflicht,
availableStock: 100,
});
fixture.componentRef.setInput('item', mockSuggestion);
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(20);
});
it('should update predefinedReturnQuantity when input changes from ReturnSuggestion to ReturnItem', () => {
it('should call getStockToRemit with correct parameters for Abteilung type', () => {
setRemissionListType(RemissionListType.Abteilung);
const mockSuggestion = createMockReturnSuggestion({
returnItem: {
data: {
id: 1,
predefinedReturnQuantity: 30,
},
},
});
const { getStockToRemit } = require('@isa/remission/data-access');
const mockSuggestion = createMockReturnSuggestion();
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(30);
// Access the computed property to trigger the calculation
component.stockToRemit();
setRemissionListType(RemissionListType.Pflicht);
const mockItem = createMockReturnItem({ predefinedReturnQuantity: 8 });
fixture.componentRef.setInput('item', mockItem);
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(8);
expect(getStockToRemit).toHaveBeenCalledWith({
remissionItem: mockSuggestion,
remissionListType: RemissionListType.Abteilung,
availableStock: 100,
});
});
});
describe('Edge cases', () => {
beforeEach(() => setRemissionListType(RemissionListType.Pflicht));
describe('Dialog interactions', () => {
it('should open remission quantity dialog and update store on valid input', async () => {
const mockDialogRef = {
closed: of({ inputValue: 10 }), // Return Observable instead of object with toPromise
};
mockNumberInputDialog.mockReturnValue(mockDialogRef);
it('should handle negative predefinedReturnQuantity values', () => {
const mockItem = createMockReturnItem({ predefinedReturnQuantity: -5 });
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(-5);
});
await component.openRemissionQuantityDialog();
it('should handle very large predefinedReturnQuantity values', () => {
const mockItem = createMockReturnItem({
predefinedReturnQuantity: 999999,
});
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(999999);
});
it('should handle decimal predefinedReturnQuantity values', () => {
const mockItem = createMockReturnItem({ predefinedReturnQuantity: 3.5 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(3.5);
});
it('should handle deeply nested null values in ReturnSuggestion', () => {
setRemissionListType(RemissionListType.Abteilung);
const mockSuggestion = {
id: 1,
remainingQuantityInStock: 10,
returnItem: {
data: null as any,
expect(mockNumberInputDialog).toHaveBeenCalledWith({
title: 'Remi-Menge ändern',
data: {
message: 'Wie viele Exemplare können remittiert werden?',
inputLabel: 'Remi-Menge',
inputValidation: expect.arrayContaining([
expect.objectContaining({ errorKey: 'required' }),
expect.objectContaining({ errorKey: 'pattern' }),
]),
},
} as ReturnSuggestion;
fixture.componentRef.setInput('item', mockSuggestion);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
});
expect(component.predefinedReturnQuantity()).toBe(0);
expect(mockRemissionStore.updateRemissionQuantity).toHaveBeenCalledWith(
1,
mockItem,
10,
);
expect(mockFeedbackDialog).toHaveBeenCalledWith({
data: { message: 'Remi-Menge wurde geändert' },
});
});
it('should handle item with unexpected structure', () => {
const unexpectedItem = {
id: 1,
remainingQuantityInStock: 10,
// Missing both returnItem and predefinedReturnQuantity
} as any;
fixture.componentRef.setInput('item', unexpectedItem);
it('should not update store when dialog is cancelled', async () => {
const mockDialogRef = {
closed: of(null), // Return Observable with null result
};
mockNumberInputDialog.mockReturnValue(mockDialogRef);
const mockItem = createMockReturnItem({ id: 1 });
fixture.componentRef.setInput('item', mockItem);
fixture.componentRef.setInput('stock', createMockStockInfo());
fixture.detectChanges();
expect(component.predefinedReturnQuantity()).toBe(0);
await component.openRemissionQuantityDialog();
expect(mockRemissionStore.updateRemissionQuantity).not.toHaveBeenCalled();
expect(mockFeedbackDialog).not.toHaveBeenCalled();
});
});
});

View File

@@ -26,7 +26,6 @@ import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
import { firstValueFrom } from 'rxjs';
import { Breakpoint, breakpoint } from '@isa/ui/layout';
import { injectRemissionListType } from '../injects/inject-remission-list-type';
import { CheckboxComponent } from '@isa/ui/input-controls';
import { RemissionListItemSelectComponent } from './remission-list-item-select.component';
/**
@@ -56,7 +55,6 @@ import { RemissionListItemSelectComponent } from './remission-list-item-select.c
TextButtonComponent,
ClientRowImports,
ItemRowDataImports,
CheckboxComponent,
RemissionListItemSelectComponent,
],
})
@@ -80,9 +78,10 @@ export class RemissionListItemComponent {
#store = inject(RemissionStore);
/**
* Signal indicating if the current layout is mobile (tablet breakpoint or below).
* Signal providing the current breakpoint state.
* Used to determine layout orientation and visibility of action buttons.
*/
mobileBreakpoint = breakpoint([Breakpoint.Tablet]);
desktopBreakpoint = breakpoint([Breakpoint.DekstopL, Breakpoint.DekstopXL]);
/**
* Signal providing the current remission list type (Abteilung or Pflicht).
@@ -106,11 +105,11 @@ export class RemissionListItemComponent {
productGroupValue = input<string>('');
/**
* Computes the orientation for the product info section based on breakpoint.
* @returns 'horizontal' if mobile, otherwise 'vertical'
* Computes the orientation of the product info based on the mobile breakpoint.
* If on mobile, uses vertical layout; otherwise, horizontal.
*/
remiProductInfoOrientation = computed(() => {
return this.mobileBreakpoint() ? 'horizontal' : 'vertical';
return this.desktopBreakpoint() ? 'horizontal' : 'vertical';
});
/**

View File

@@ -22,7 +22,7 @@
{{ hits() }} Einträge
</span>
<div class="flex flex-col gap-4 w-full items-center justify-center">
<div class="flex flex-col gap-4 w-full items-center justify-center mb-24">
@for (item of items(); track item.id) {
@defer (on viewport) {
<remi-feature-remission-list-item

View File

@@ -14,10 +14,10 @@
<div
class="grid"
[class.grid-cols-[minmax(20rem,1fr),auto]]="horizontal"
[class.gap-6]="horizontal"
[class.grid-flow-row]="!horizontal"
[class.gap-2]="!horizontal"
[class.grid-cols-[minmax(20rem,1fr),auto]]="!horizontal"
[class.gap-6]="!horizontal"
[class.grid-flow-row]="horizontal"
[class.gap-2]="horizontal"
>
<div class="grid grid-flow-row gap-2">
<div class="isa-text-body-2-bold" data-what="product-contributors">

View File

@@ -1,5 +1,8 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductInfoComponent, ProductInfoItem } from './product-info.component';
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';
@@ -33,7 +36,11 @@ describe('ProductInfoComponent', () => {
})
.overrideComponent(ProductInfoComponent, {
remove: {
imports: [ProductFormatComponent, ProductImageDirective, ProductRouterLinkDirective],
imports: [
ProductFormatComponent,
ProductImageDirective,
ProductRouterLinkDirective,
],
},
add: {
imports: [
@@ -81,7 +88,7 @@ describe('ProductInfoComponent', () => {
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');
@@ -98,21 +105,25 @@ describe('ProductInfoComponent', () => {
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.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"]')
By.css('[data-what="product-contributors"]'),
);
expect(contributorsElement).toBeTruthy();
expect(contributorsElement.nativeElement.textContent.trim()).toBe('Test Contributors');
expect(contributorsElement.nativeElement.textContent.trim()).toBe(
'Test Contributors',
);
});
it('should display product name', () => {
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
By.css('[data-what="product-name"]'),
);
expect(nameElement).toBeTruthy();
expect(nameElement.nativeElement.textContent.trim()).toBe('Test Product');
@@ -120,7 +131,7 @@ describe('ProductInfoComponent', () => {
it('should display formatted price', () => {
const priceElement = fixture.debugElement.query(
By.css('[data-what="product-price"]')
By.css('[data-what="product-price"]'),
);
expect(priceElement).toBeTruthy();
expect(priceElement.nativeElement.textContent.trim()).toBe('€19.99');
@@ -128,7 +139,7 @@ describe('ProductInfoComponent', () => {
it('should display product EAN', () => {
const eanElement = fixture.debugElement.query(
By.css('[data-what="product-ean"]')
By.css('[data-what="product-ean"]'),
);
expect(eanElement).toBeTruthy();
expect(eanElement.nativeElement.textContent.trim()).toBe('1234567890123');
@@ -136,10 +147,12 @@ describe('ProductInfoComponent', () => {
it('should render product format component with correct inputs', () => {
const formatComponent = fixture.debugElement.query(
By.css('shared-product-format')
By.css('shared-product-format'),
);
expect(formatComponent).toBeTruthy();
expect(formatComponent.nativeElement.getAttribute('data-what')).toBe('product-format');
expect(formatComponent.nativeElement.getAttribute('data-what')).toBe(
'product-format',
);
});
});
@@ -150,10 +163,10 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const layoutDiv = fixture.debugElement.query(
By.css('.grid.grid-cols-\\[minmax\\(20rem\\,1fr\\)\\,auto\\]')
By.css('.grid.grid-flow-row.gap-2'),
);
expect(layoutDiv).toBeTruthy();
expect(layoutDiv.nativeElement.classList.contains('gap-6')).toBe(true);
expect(layoutDiv.nativeElement.classList.contains('gap-2')).toBe(true);
});
it('should apply vertical layout classes when orientation is vertical', () => {
@@ -185,9 +198,11 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
By.css('[data-what="product-name"]'),
);
expect(nameElement.nativeElement.textContent.trim()).toBe(
'Updated Product Name',
);
expect(nameElement.nativeElement.textContent.trim()).toBe('Updated Product Name');
const hostElement = fixture.debugElement.nativeElement;
expect(hostElement.getAttribute('data-ean')).toBe('9876543210987');
@@ -198,7 +213,7 @@ describe('ProductInfoComponent', () => {
...mockProductItem,
retailPrice: {
value: {
value: 25.50,
value: 25.5,
currencySymbol: '$',
},
},
@@ -208,7 +223,7 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const priceElement = fixture.debugElement.query(
By.css('[data-what="product-price"]')
By.css('[data-what="product-price"]'),
);
expect(priceElement.nativeElement.textContent.trim()).toBe('$25.50');
});
@@ -228,7 +243,7 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const priceElement = fixture.debugElement.query(
By.css('[data-what="product-price"]')
By.css('[data-what="product-price"]'),
);
expect(priceElement.nativeElement.textContent.trim()).toBe('€0.00');
});
@@ -243,28 +258,30 @@ describe('ProductInfoComponent', () => {
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('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]')
By.css('[data-what]'),
);
expect(dataWhatElements.length).toBeGreaterThan(0);
const expectedDataWhatValues = [
'product-image',
'product-contributors',
'product-name',
'product-name',
'product-price',
'product-format',
'product-ean',
];
expectedDataWhatValues.forEach(value => {
expectedDataWhatValues.forEach((value) => {
const element = fixture.debugElement.query(
By.css(`[data-what="${value}"]`)
By.css(`[data-what="${value}"]`),
);
expect(element).toBeTruthy();
});
@@ -285,7 +302,7 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
By.css('[data-what="product-name"]'),
);
expect(nameElement.nativeElement.textContent.trim()).toBe('');
});
@@ -303,7 +320,7 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const contributorsElement = fixture.debugElement.query(
By.css('[data-what="product-contributors"]')
By.css('[data-what="product-contributors"]'),
);
expect(contributorsElement.nativeElement.textContent.trim()).toBe('');
});
@@ -321,9 +338,11 @@ describe('ProductInfoComponent', () => {
fixture.detectChanges();
const nameElement = fixture.debugElement.query(
By.css('[data-what="product-name"]')
By.css('[data-what="product-name"]'),
);
expect(nameElement.nativeElement.textContent.trim()).toContain(
'This is a very long product name',
);
expect(nameElement.nativeElement.textContent.trim()).toContain('This is a very long product name');
});
});
});
});

View File

@@ -3,6 +3,7 @@
product: item().product,
retailPrice: item().catalogAvailability.price,
}"
[orientation]="productInfoOrientation()"
></remi-product-info>
<div class="text-right">
<button

View File

@@ -3,10 +3,12 @@ import {
Component,
inject,
input,
computed,
} from '@angular/core';
import { Item } from '@isa/catalogue/data-access';
import { ProductInfoComponent } from '@isa/remission/shared/product';
import { TextButtonComponent } from '@isa/ui/buttons';
import { Breakpoint, breakpoint } from '@isa/ui/layout';
import { SearchItemToRemitDialogComponent } from './search-item-to-remit-dialog.component';
@Component({
@@ -20,4 +22,10 @@ export class SearchItemToRemitComponent {
host = inject(SearchItemToRemitDialogComponent);
item = input.required<Item>();
desktopBreakpoint = breakpoint([Breakpoint.DekstopL, Breakpoint.DekstopXL]);
productInfoOrientation = computed(() => {
return this.desktopBreakpoint() ? 'vertical' : 'horizontal';
});
}