Compare commits
3 Commits
0621a0be16
...
ac4416f7ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac4416f7ab | ||
|
|
992e726406 | ||
|
|
988625ba42 |
@@ -11,3 +11,5 @@ Dieser Skill enthält die vollständige Dokumentation und Spezifikation des CASP
|
|||||||
- [Datenmodell & Schema](file:///c:/source/webshop/.agents/skills/grand-functions/references/schema.md)
|
- [Datenmodell & Schema](file:///c:/source/webshop/.agents/skills/grand-functions/references/schema.md)
|
||||||
- [Kernprozesse](file:///c:/source/webshop/.agents/skills/grand-functions/references/processes.md)
|
- [Kernprozesse](file:///c:/source/webshop/.agents/skills/grand-functions/references/processes.md)
|
||||||
- [Sicherheitskonzept (RLS)](file:///c:/source/webshop/.agents/skills/grand-functions/references/security.md)
|
- [Sicherheitskonzept (RLS)](file:///c:/source/webshop/.agents/skills/grand-functions/references/security.md)
|
||||||
|
- [Testumgebung & -abdeckung](file:///c:/source/webshop/.agents/skills/grand-functions/references/testing.md)
|
||||||
|
|
||||||
|
|||||||
21
.agents/skills/grand-functions/references/testing.md
Normal file
21
.agents/skills/grand-functions/references/testing.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Testumgebung & Testabdeckung
|
||||||
|
|
||||||
|
## 1. Test-Setup
|
||||||
|
- **Framework**: Vitest (Version ^1.6.0)
|
||||||
|
- **Konfiguration**: [vitest.config.ts](file:///c:/source/webshop/shop/vitest.config.ts)
|
||||||
|
- **Befehl**: `npm run test` (führt `vitest run` aus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Test-Szenarien
|
||||||
|
|
||||||
|
### A. Frontend Wizard State-Management
|
||||||
|
Getestet in [wizard-state.test.ts](file:///c:/source/webshop/shop/lib/wizard-state.test.ts):
|
||||||
|
- **Auto-Reset bei Produktwechsel**: Prüft, ob nachgelagerte Module, die durch `global_exclusions` für das neue Produkt gesperrt sind, automatisch aus der Selektion fliegen.
|
||||||
|
- **Radio-Button-Verhalten (allow_multiselect = false)**: Stellt sicher, dass in Single-Select-Kategorien nur maximal ein Modul aktiv ist und bei Auswahl eines anderen Moduls das vorherige automatisch abgewählt wird.
|
||||||
|
|
||||||
|
### B. Backend Checkout-Split
|
||||||
|
Getestet in [checkout-split.test.ts](file:///c:/source/webshop/shop/lib/checkout-split.test.ts):
|
||||||
|
- **Kauf-Warenkorb**: Verifiziert, dass ein reiner Kauf-Warenkorb (`one_time`/`purchase`) exakt eine Order erzeugt.
|
||||||
|
- **Abo-Warenkorb**: Verifiziert, dass ein reiner Abo-Warenkorb (`monthly`/`subscription`) exakt eine Order erzeugt.
|
||||||
|
- **Gemischter Warenkorb**: Stellt sicher, dass ein gemischter Warenkorb korrekt in zwei separate Bestell-Gruppen aufgeteilt wird (Kauf-Items & Abo-Items separat).
|
||||||
42
shop/lib/checkout-split.test.ts
Normal file
42
shop/lib/checkout-split.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { splitCart, CartItem } from './checkout-split';
|
||||||
|
|
||||||
|
describe('Checkout Split Logic', () => {
|
||||||
|
it('should place purchase items only into purchase group', () => {
|
||||||
|
const cart: CartItem[] = [
|
||||||
|
{ billingInterval: 'one_time', selections: {} },
|
||||||
|
{ billingType: 'purchase', selections: {} }
|
||||||
|
];
|
||||||
|
|
||||||
|
const { purchaseItems, subscriptionItems } = splitCart(cart);
|
||||||
|
|
||||||
|
expect(purchaseItems.length).toBe(2);
|
||||||
|
expect(subscriptionItems.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should place subscription items only into subscription group', () => {
|
||||||
|
const cart: CartItem[] = [
|
||||||
|
{ billingInterval: 'monthly', selections: {} },
|
||||||
|
{ billingType: 'subscription', selections: {} }
|
||||||
|
];
|
||||||
|
|
||||||
|
const { purchaseItems, subscriptionItems } = splitCart(cart);
|
||||||
|
|
||||||
|
expect(purchaseItems.length).toBe(0);
|
||||||
|
expect(subscriptionItems.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should split mixed cart into separate purchase and subscription groups', () => {
|
||||||
|
const cart: CartItem[] = [
|
||||||
|
{ billingInterval: 'one_time', selections: {} },
|
||||||
|
{ billingInterval: 'monthly', selections: {} },
|
||||||
|
{ billingType: 'purchase', selections: {} },
|
||||||
|
{ billingType: 'subscription', selections: {} }
|
||||||
|
];
|
||||||
|
|
||||||
|
const { purchaseItems, subscriptionItems } = splitCart(cart);
|
||||||
|
|
||||||
|
expect(purchaseItems.length).toBe(2);
|
||||||
|
expect(subscriptionItems.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
17
shop/lib/checkout-split.ts
Normal file
17
shop/lib/checkout-split.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export interface CartItem {
|
||||||
|
id?: string;
|
||||||
|
billingInterval?: 'one_time' | 'monthly';
|
||||||
|
billingType?: 'purchase' | 'subscription';
|
||||||
|
selections: any;
|
||||||
|
moduleQuantities?: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitCart(items: CartItem[]) {
|
||||||
|
const purchaseItems = items.filter(
|
||||||
|
item => item.billingInterval === 'one_time' || item.billingType === 'purchase'
|
||||||
|
);
|
||||||
|
const subscriptionItems = items.filter(
|
||||||
|
item => item.billingInterval === 'monthly' || item.billingType === 'subscription'
|
||||||
|
);
|
||||||
|
return { purchaseItems, subscriptionItems };
|
||||||
|
}
|
||||||
110
shop/lib/wizard-state.test.ts
Normal file
110
shop/lib/wizard-state.test.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { handleBaseProductChange, handleModuleToggle } from './wizard-state';
|
||||||
|
import { Product, ProductModule, Category, WizardSelections } from './types';
|
||||||
|
|
||||||
|
describe('Wizard State Management', () => {
|
||||||
|
const mockCategories: Category[] = [
|
||||||
|
{
|
||||||
|
id: 'cat-1',
|
||||||
|
name: 'Kasse',
|
||||||
|
sort_order: 1,
|
||||||
|
is_required: true,
|
||||||
|
allow_multiselect: false,
|
||||||
|
free_items_limit: 0,
|
||||||
|
preselect: false,
|
||||||
|
resets_others: false,
|
||||||
|
created_at: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat-2',
|
||||||
|
name: 'Backoffice',
|
||||||
|
sort_order: 2,
|
||||||
|
is_required: false,
|
||||||
|
allow_multiselect: false, // Radio behavior
|
||||||
|
free_items_limit: 0,
|
||||||
|
preselect: false,
|
||||||
|
resets_others: false,
|
||||||
|
created_at: '',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockModules: ProductModule[] = [
|
||||||
|
{
|
||||||
|
id: 'mod-cloud',
|
||||||
|
product_id: 'prod-gastro',
|
||||||
|
name: 'POSCloud',
|
||||||
|
price: 10,
|
||||||
|
requirements: [],
|
||||||
|
exclusions: [],
|
||||||
|
created_at: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mod-office-1',
|
||||||
|
product_id: 'prod-gastro',
|
||||||
|
name: 'Office Pro',
|
||||||
|
price: 20,
|
||||||
|
requirements: [],
|
||||||
|
exclusions: [],
|
||||||
|
created_at: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mod-office-2',
|
||||||
|
product_id: 'prod-gastro',
|
||||||
|
name: 'Office Light',
|
||||||
|
price: 5,
|
||||||
|
requirements: [],
|
||||||
|
exclusions: [],
|
||||||
|
created_at: '',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should automatically remove excluded modules when switching base product', () => {
|
||||||
|
const currentSelections: WizardSelections = {
|
||||||
|
'cat-2': {
|
||||||
|
productId: null,
|
||||||
|
moduleIds: ['mod-cloud'] // User has POSCloud selected
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Exclusion rule: 'prod-small' excludes 'mod-cloud'
|
||||||
|
const mockExclusions = [
|
||||||
|
{
|
||||||
|
product_id: 'prod-small',
|
||||||
|
excluded_module_id: 'mod-cloud'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = handleBaseProductChange(
|
||||||
|
'prod-small',
|
||||||
|
currentSelections,
|
||||||
|
mockModules,
|
||||||
|
[],
|
||||||
|
mockExclusions
|
||||||
|
);
|
||||||
|
|
||||||
|
// POSCloud should be removed
|
||||||
|
expect(result['cat-2'].moduleIds).not.toContain('mod-cloud');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respect allow_multiselect = false and only allow one selection in that category', () => {
|
||||||
|
let selections: WizardSelections = {
|
||||||
|
'cat-2': {
|
||||||
|
productId: null,
|
||||||
|
moduleIds: ['mod-office-1']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle on 'mod-office-2' in a single-select category
|
||||||
|
selections = handleModuleToggle(
|
||||||
|
'mod-office-2',
|
||||||
|
'cat-2',
|
||||||
|
selections,
|
||||||
|
mockCategories,
|
||||||
|
mockModules
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should only contain 'mod-office-2'
|
||||||
|
expect(selections['cat-2'].moduleIds).toContain('mod-office-2');
|
||||||
|
expect(selections['cat-2'].moduleIds).not.toContain('mod-office-1');
|
||||||
|
});
|
||||||
|
});
|
||||||
1618
shop/package-lock.json
generated
1618
shop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,8 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
@@ -51,6 +52,7 @@
|
|||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
shop/vitest.config.ts
Normal file
14
shop/vitest.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'node',
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user