Add OMS WMS ERP platform
Validators (Pushes and Pull Requests) / UI & Storage Validation (pull_request) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (pull_request) Successful in 13s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (pull_request) Failing after 28s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (pull_request) Failing after 10s
Validators (Pushes and Pull Requests) / Security & Secrets (pull_request) Successful in 12s
Validators (Pushes and Pull Requests) / Notify PR Results (pull_request) Successful in 2s
Frontend CI Pipeline / ci-frontend-8-steps (pull_request) Failing after 2m50s
Validators (Pushes and Pull Requests) / UI & Storage Validation (pull_request) Failing after 12s
Validators (Pushes and Pull Requests) / Database & Schema Validation (pull_request) Successful in 13s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (pull_request) Failing after 28s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (pull_request) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (pull_request) Failing after 10s
Validators (Pushes and Pull Requests) / Security & Secrets (pull_request) Successful in 12s
Validators (Pushes and Pull Requests) / Notify PR Results (pull_request) Successful in 2s
Frontend CI Pipeline / ci-frontend-8-steps (pull_request) Failing after 2m50s
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test.describe("Authentication System", () => {
|
||||
test("Redirect to login when not authenticated", async ({ page }) => {
|
||||
// Try to access admin page without login
|
||||
await page.goto("http://localhost:5173/admin/dashboard")
|
||||
|
||||
// Should redirect to login
|
||||
await expect(page).toHaveURL(/login/)
|
||||
console.log("✅ Not authenticated users redirected to login")
|
||||
})
|
||||
|
||||
test("Login with valid credentials", async ({ page }) => {
|
||||
// Go to login page
|
||||
await page.goto("http://localhost:5173/login")
|
||||
|
||||
// Fill login form
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
|
||||
// Submit
|
||||
await page.click("button[type='submit']")
|
||||
|
||||
// Should redirect to dashboard
|
||||
await page.waitForURL(/admin\/dashboard/, { timeout: 5000 })
|
||||
console.log("✅ Login successful")
|
||||
|
||||
// Check if dashboard loads
|
||||
const dashboard = await page.locator("h1, .dashboard-title").first().isVisible().catch(() => false)
|
||||
console.log("✅ Dashboard visible after login")
|
||||
})
|
||||
|
||||
test("Access admin pages after login", async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Check sidebar is visible
|
||||
const sidebar = await page.locator(".sidebar").isVisible()
|
||||
expect(sidebar).toBeTruthy()
|
||||
console.log("✅ Sidebar visible")
|
||||
|
||||
// Check user info in sidebar
|
||||
const userInfo = await page.locator(".user-info").isVisible()
|
||||
expect(userInfo).toBeTruthy()
|
||||
console.log("✅ User info displayed in sidebar")
|
||||
|
||||
// Check logout button
|
||||
const logoutBtn = await page.locator(".logout-btn").isVisible()
|
||||
expect(logoutBtn).toBeTruthy()
|
||||
console.log("✅ Logout button visible")
|
||||
|
||||
// Navigate to Orders page
|
||||
await page.click("text=Orders")
|
||||
await page.waitForURL(/admin\/orders/)
|
||||
console.log("✅ Can navigate to Orders page")
|
||||
|
||||
// Navigate to Products page
|
||||
await page.click("text=Products")
|
||||
await page.waitForURL(/admin\/products/)
|
||||
console.log("✅ Can navigate to Products page")
|
||||
})
|
||||
|
||||
test("Logout functionality", async ({ page }) => {
|
||||
// Login first
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Register dialog handler BEFORE clicking logout (timing-critical)
|
||||
page.once("dialog", async (dialog) => {
|
||||
console.log(`Dialog: ${dialog.message()}`)
|
||||
await dialog.accept()
|
||||
})
|
||||
|
||||
// Click logout
|
||||
await page.click(".logout-btn")
|
||||
|
||||
// Should redirect to login
|
||||
await page.waitForURL(/login/, { timeout: 5000 })
|
||||
console.log("✅ Logout successful")
|
||||
})
|
||||
|
||||
test("Session persistence", async ({ page, context }) => {
|
||||
// Login
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Check localStorage
|
||||
const token = await page.evaluate(() => localStorage.getItem("auth-token"))
|
||||
expect(token).toBeTruthy()
|
||||
console.log("✅ Auth token stored in localStorage")
|
||||
|
||||
// Open new page in same context (shares storage)
|
||||
const page2 = await context.newPage()
|
||||
await page2.goto("http://localhost:5173/admin/dashboard")
|
||||
|
||||
// Should be authenticated
|
||||
await expect(page2).toHaveURL(/admin\/dashboard/)
|
||||
console.log("✅ Session persists across pages")
|
||||
|
||||
await page2.close()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* E2E Tests: Complete Order-to-Inventory Flow
|
||||
* Tests the full business flow from order creation to inventory update
|
||||
*
|
||||
* Run: npm run test:e2e
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('OMS·WMS·ERP Complete Flow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to application
|
||||
await page.goto('http://localhost:5173')
|
||||
|
||||
// Wait for app to load
|
||||
await page.waitForLoadState('networkidle')
|
||||
})
|
||||
|
||||
test('orders page loads with navigation', async ({ page }) => {
|
||||
// Find navigation to orders
|
||||
const ordersNav = page.getByRole('link', { name: /orders/i })
|
||||
await expect(ordersNav).toBeVisible()
|
||||
|
||||
// Click and verify page loads
|
||||
await ordersNav.click()
|
||||
await page.waitForURL('**/orders**')
|
||||
await expect(page).toHaveTitle(/.*orders.*/i)
|
||||
})
|
||||
|
||||
test('inventory page loads with navigation', async ({ page }) => {
|
||||
const inventoryNav = page.getByRole('link', { name: /inventory/i })
|
||||
await expect(inventoryNav).toBeVisible()
|
||||
|
||||
await inventoryNav.click()
|
||||
await page.waitForURL('**/inventory**')
|
||||
await expect(page).toHaveTitle(/.*inventory.*/i)
|
||||
})
|
||||
|
||||
test('products page loads with navigation', async ({ page }) => {
|
||||
const productsNav = page.getByRole('link', { name: /products/i })
|
||||
await expect(productsNav).toBeVisible()
|
||||
|
||||
await productsNav.click()
|
||||
await page.waitForURL('**/products**')
|
||||
await expect(page).toHaveTitle(/.*products.*/i)
|
||||
})
|
||||
|
||||
test('responsive navigation on mobile', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 })
|
||||
|
||||
// Look for mobile menu toggle
|
||||
const menuToggle = page.getByRole('button', { name: /menu|toggle/i })
|
||||
if (await menuToggle.isVisible()) {
|
||||
await menuToggle.click()
|
||||
await expect(page.getByRole('navigation')).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('orders page displays data', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Wait for table to load
|
||||
const table = page.locator('table')
|
||||
if (await table.isVisible({ timeout: 5000 }).catch(() => false)) {
|
||||
const rows = await page.locator('tbody tr').count()
|
||||
// Should have at least mock data
|
||||
expect(rows).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
test('inventory page displays data', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/inventory')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const content = page.locator('main, [role="main"]')
|
||||
await expect(content).toBeVisible()
|
||||
})
|
||||
|
||||
test('products page displays data', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/products')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const content = page.locator('main, [role="main"]')
|
||||
await expect(content).toBeVisible()
|
||||
})
|
||||
|
||||
test('buttons are accessible and interactive', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Look for action buttons
|
||||
const buttons = page.getByRole('button')
|
||||
const count = await buttons.count()
|
||||
|
||||
if (count > 0) {
|
||||
const firstButton = buttons.first()
|
||||
await expect(firstButton).toBeEnabled()
|
||||
}
|
||||
})
|
||||
|
||||
test('forms have proper validation', async ({ page }) => {
|
||||
// Navigate to create order (if form exists)
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
|
||||
// Look for create button or form
|
||||
const createButton = page.getByRole('button', { name: /create|new|add/i })
|
||||
if (await createButton.isVisible({ timeout: 3000 }).catch(() => false)) {
|
||||
await createButton.click()
|
||||
|
||||
// Look for form inputs
|
||||
const inputs = page.locator('input')
|
||||
const inputCount = await inputs.count()
|
||||
|
||||
if (inputCount > 0) {
|
||||
// Verify inputs are interactive
|
||||
const firstInput = inputs.first()
|
||||
await expect(firstInput).toBeFocusable()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('error handling shows user-friendly messages', async ({ page }) => {
|
||||
// This test would require a specific error scenario
|
||||
// For now, verify that error states don't break the UI
|
||||
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Verify page content is visible even if API might fail
|
||||
const mainContent = page.locator('main, [role="main"]')
|
||||
await expect(mainContent).toBeVisible()
|
||||
})
|
||||
|
||||
test('page navigation history works', async ({ page }) => {
|
||||
// Start at home
|
||||
await page.goto('http://localhost:5173')
|
||||
|
||||
// Navigate to orders
|
||||
await page.getByRole('link', { name: /orders/i }).click()
|
||||
await page.waitForURL('**/orders**')
|
||||
|
||||
// Navigate to products
|
||||
await page.getByRole('link', { name: /products/i }).click()
|
||||
await page.waitForURL('**/products**')
|
||||
|
||||
// Go back
|
||||
await page.goBack()
|
||||
await page.waitForURL('**/orders**')
|
||||
|
||||
// Go forward
|
||||
await page.goForward()
|
||||
await page.waitForURL('**/products**')
|
||||
})
|
||||
|
||||
test('accessibility: keyboard navigation', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173')
|
||||
|
||||
// Tab through navigation
|
||||
await page.keyboard.press('Tab')
|
||||
const focusedElement = await page.evaluate(() => {
|
||||
return document.activeElement?.tagName
|
||||
})
|
||||
|
||||
// Should focus on an interactive element
|
||||
expect(['A', 'BUTTON', 'INPUT', 'SELECT']).toContain(focusedElement)
|
||||
})
|
||||
|
||||
test('accessibility: color contrast', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Run axe accessibility audit
|
||||
const { injectAxe, checkA11y } = require('axe-playwright')
|
||||
|
||||
await injectAxe(page)
|
||||
await checkA11y(page, null, {
|
||||
detailedReport: true,
|
||||
detailedReportOptions: {
|
||||
html: true
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Order Management Flow', () => {
|
||||
test('create order workflow', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Look for create order button
|
||||
const createBtn = page.getByRole('button', { name: /create|new|add/i })
|
||||
|
||||
if (await createBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await createBtn.click()
|
||||
|
||||
// Verify modal or form appears
|
||||
const form = page.locator('form')
|
||||
if (await form.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
// Fill in sample data
|
||||
const inputs = form.locator('input, select')
|
||||
const inputCount = await inputs.count()
|
||||
|
||||
expect(inputCount).toBeGreaterThan(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('view order details', async ({ page }) => {
|
||||
await page.goto('http://localhost:5173/orders')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Find first order row
|
||||
const firstRow = page.locator('tbody tr').first()
|
||||
if (await firstRow.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await firstRow.click()
|
||||
|
||||
// Verify details view loads
|
||||
await page.waitForLoadState('networkidle')
|
||||
const details = page.locator('[data-test="order-details"], .order-details')
|
||||
if (await details.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
expect(details).toBeVisible()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Performance Checks', () => {
|
||||
test('page loads in acceptable time', async ({ page }) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
await page.goto('http://localhost:5173/orders', {
|
||||
waitUntil: 'networkidle'
|
||||
})
|
||||
|
||||
const loadTime = Date.now() - startTime
|
||||
|
||||
// Should load within 3 seconds (including network delays)
|
||||
expect(loadTime).toBeLessThan(3000)
|
||||
})
|
||||
|
||||
test('no console errors on navigation', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text())
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto('http://localhost:5173')
|
||||
await page.getByRole('link', { name: /orders/i }).click()
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
expect(errors).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test.describe("Form Submission & CRUD Operations", () => {
|
||||
test("Create customer form submission", async ({ page }) => {
|
||||
// Login
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Navigate to customers
|
||||
await page.click("text=Customers")
|
||||
await page.waitForURL(/admin\/customers/)
|
||||
|
||||
// Click create customer
|
||||
await page.click("text=Create Customer")
|
||||
await page.waitForURL(/admin\/customers\/new/)
|
||||
|
||||
// Fill form
|
||||
await page.fill("input#name", "Test Company ABC")
|
||||
await page.fill("input#email", "test@company.com")
|
||||
await page.fill("input#phone", "010-1234-5678")
|
||||
await page.fill("input#address", "Seoul, Korea")
|
||||
await page.fill("input#creditLimit", "5000000")
|
||||
|
||||
// Submit
|
||||
await page.click("button[type='submit']:has-text('Create')")
|
||||
|
||||
// Should redirect to customers list
|
||||
await page.waitForURL(/admin\/customers/)
|
||||
console.log("✅ Customer created successfully")
|
||||
|
||||
// Verify in list (should show newly created customer)
|
||||
const customerNameVisible = await page.locator("text=Test Company ABC").isVisible()
|
||||
expect(customerNameVisible).toBeTruthy()
|
||||
console.log("✅ Customer appears in list")
|
||||
})
|
||||
|
||||
test("View customer list with data", async ({ page }) => {
|
||||
// Login
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Navigate to customers
|
||||
await page.click("text=Customers")
|
||||
await page.waitForURL(/admin\/customers/)
|
||||
|
||||
// Check if table has data rows
|
||||
const tableRows = await page.locator("table tbody tr").count()
|
||||
console.log(`✅ Found ${tableRows} customer rows`)
|
||||
|
||||
// Verify table structure (headers)
|
||||
const headers = await page.locator("table thead th").allTextContents()
|
||||
expect(headers).toContain("Customer Name")
|
||||
expect(headers).toContain("Email")
|
||||
expect(headers).toContain("Status")
|
||||
console.log("✅ Customer table has correct structure")
|
||||
})
|
||||
|
||||
test("Edit customer (if data available)", async ({ page }) => {
|
||||
// Login
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Navigate to customers
|
||||
await page.click("text=Customers")
|
||||
await page.waitForURL(/admin\/customers/)
|
||||
|
||||
// Check if edit button exists
|
||||
const editButton = page.locator("a:text('Edit')").first()
|
||||
const isVisible = await editButton.isVisible().catch(() => false)
|
||||
|
||||
if (isVisible) {
|
||||
await editButton.click()
|
||||
await page.waitForURL(/admin\/customers\/.*\/edit/)
|
||||
|
||||
// Verify form loaded
|
||||
const form = await page.locator("form").first()
|
||||
expect(form).toBeTruthy()
|
||||
console.log("✅ Edit form loaded successfully")
|
||||
|
||||
// Make a change
|
||||
const nameInput = page.locator("input#name")
|
||||
const currentValue = await nameInput.inputValue()
|
||||
await nameInput.fill(currentValue + " (Updated)")
|
||||
|
||||
// Submit
|
||||
await page.click("button[type='submit']:has-text('Update')")
|
||||
await page.waitForURL(/admin\/customers/)
|
||||
console.log("✅ Customer updated successfully")
|
||||
} else {
|
||||
console.log("⚠️ No customers in list to edit (this is OK for fresh database)")
|
||||
}
|
||||
})
|
||||
|
||||
test("Form validation on customer creation", async ({ page }) => {
|
||||
// Login
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/)
|
||||
|
||||
// Navigate to create customer
|
||||
await page.click("text=Customers")
|
||||
await page.waitForURL(/admin\/customers/)
|
||||
await page.click("text=Create Customer")
|
||||
await page.waitForURL(/admin\/customers\/new/)
|
||||
|
||||
// Try to submit empty form
|
||||
await page.click("button[type='submit']:has-text('Create')")
|
||||
|
||||
// Should show validation error (page should not navigate)
|
||||
await page.waitForTimeout(500)
|
||||
const currentUrl = page.url()
|
||||
expect(currentUrl).toContain("/customers/new")
|
||||
console.log("✅ Form validation prevents empty submission")
|
||||
|
||||
// Fill only name and try again
|
||||
await page.fill("input#name", "Incomplete Company")
|
||||
await page.fill("input#email", "invalid-email")
|
||||
await page.click("button[type='submit']:has-text('Create')")
|
||||
|
||||
// Should still be on form (email validation failed)
|
||||
await page.waitForTimeout(500)
|
||||
const stillOnForm = page.url().includes("/customers/new")
|
||||
console.log(`✅ Email validation ${stillOnForm ? "working" : "bypassed (may be OK)"}`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { test, expect } from "@playwright/test"
|
||||
|
||||
test("Complete admin navigation", async ({ page }) => {
|
||||
// Login
|
||||
await page.goto("http://localhost:5173/login")
|
||||
await page.fill("input[type='text']", "admin")
|
||||
await page.fill("input[type='password']", "password")
|
||||
await page.click("button[type='submit']")
|
||||
await page.waitForURL(/admin\/dashboard/, { timeout: 5000 })
|
||||
console.log("✅ Logged in")
|
||||
|
||||
// Test all sidebar links
|
||||
const links = [
|
||||
{ text: "Dashboard", path: /admin\/dashboard/ },
|
||||
{ text: "Orders", path: /admin\/orders/ },
|
||||
{ text: "Products", path: /admin\/products/ },
|
||||
{ text: "Customers", path: /admin\/customers/ },
|
||||
{ text: "Inventory", path: /admin\/inventory/ },
|
||||
{ text: "Warehouses", path: /admin\/warehouses/ }
|
||||
]
|
||||
|
||||
for (const link of links) {
|
||||
await page.click(`text=${link.text}`)
|
||||
await page.waitForURL(link.path, { timeout: 5000 })
|
||||
const heading = await page.locator("h2.page-title, h1").first()
|
||||
expect(heading).toBeTruthy()
|
||||
console.log(`✅ ${link.text} page loaded`)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* E2E Tests Index
|
||||
* Full workflow tests using Playwright
|
||||
*
|
||||
* Test Scenarios:
|
||||
* - Order Flow (create, filter, edit, delete)
|
||||
* - Inventory Flow (transfer, low stock alerts)
|
||||
* - Dashboard flows (KPI cards, navigation)
|
||||
*/
|
||||
|
||||
export * from './order-flow.spec'
|
||||
export * from './inventory-flow.spec'
|
||||
@@ -0,0 +1,49 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Inventory Management E2E Tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://localhost:5173')
|
||||
})
|
||||
|
||||
test('should transfer inventory between warehouses', async ({ page }) => {
|
||||
await page.click('a[href="/inventory"]')
|
||||
await page.click('button:has-text("Transfer")')
|
||||
|
||||
// Select warehouses
|
||||
await page.selectOption('select:has-text("From Warehouse")', 'WH-001')
|
||||
await page.selectOption('select:has-text("To Warehouse")', 'WH-002')
|
||||
|
||||
// Enter product and quantity
|
||||
await page.fill('input[name="productSku"]', 'SKU-12345')
|
||||
await page.fill('input[name="quantity"]', '100')
|
||||
|
||||
// Submit transfer
|
||||
await page.click('button:has-text("Transfer")')
|
||||
|
||||
// Verify success
|
||||
await expect(page.locator('text=Transfer completed')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should view inventory levels', async ({ page }) => {
|
||||
await page.click('a[href="/inventory"]')
|
||||
|
||||
// Verify table displays
|
||||
const table = await page.locator('table').isVisible()
|
||||
expect(table).toBeTruthy()
|
||||
|
||||
// Verify columns
|
||||
await expect(page.locator('th:has-text("Product")')).toBeVisible()
|
||||
await expect(page.locator('th:has-text("Quantity")')).toBeVisible()
|
||||
await expect(page.locator('th:has-text("Available")')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should alert on low stock items', async ({ page }) => {
|
||||
await page.click('a[href="/inventory"]')
|
||||
await page.click('button:has-text("Low Stock")')
|
||||
|
||||
// Verify low stock items displayed
|
||||
const lowStockBadges = await page.locator('.badge-warning')
|
||||
const count = await lowStockBadges.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Order Management E2E Tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to app
|
||||
await page.goto('http://localhost:5173')
|
||||
})
|
||||
|
||||
test('should complete full order workflow', async ({ page }) => {
|
||||
// Navigate to orders
|
||||
await page.click('a[href="/orders"]')
|
||||
await expect(page).toHaveURL(/orders/)
|
||||
|
||||
// Create new order
|
||||
await page.click('button:has-text("New Order")')
|
||||
await expect(page).toHaveTitle(/Create Order/)
|
||||
|
||||
// Select customer
|
||||
await page.selectOption('select[name="customerId"]', 'cust-001')
|
||||
|
||||
// Set dates
|
||||
await page.fill('input[name="orderDate"]', '2026-08-01')
|
||||
await page.fill('input[name="dueDate"]', '2026-08-15')
|
||||
|
||||
// Add items
|
||||
await page.click('button:has-text("Add Item")')
|
||||
await page.fill('input[name="productSku"]', 'SKU-12345')
|
||||
await page.fill('input[name="quantity"]', '100')
|
||||
await page.fill('input[name="unitPrice"]', '50')
|
||||
|
||||
// Verify totals
|
||||
const subtotal = await page.locator('text=Subtotal: $5000').isVisible()
|
||||
expect(subtotal).toBeTruthy()
|
||||
|
||||
// Save order
|
||||
await page.click('button:has-text("Create Order")')
|
||||
|
||||
// Verify order created
|
||||
await expect(page).toHaveURL(/orders\//)
|
||||
const orderNumber = await page.locator('h1').textContent()
|
||||
expect(orderNumber).toMatch(/ORD-/)
|
||||
})
|
||||
|
||||
test('should filter orders by status', async ({ page }) => {
|
||||
await page.click('a[href="/orders"]')
|
||||
|
||||
// Open filter
|
||||
await page.click('button:has-text("Filter")')
|
||||
|
||||
// Select status
|
||||
await page.selectOption('select[name="status"]', 'CONFIRMED')
|
||||
|
||||
// Apply
|
||||
await page.click('button:has-text("Apply")')
|
||||
|
||||
// Verify filtered results
|
||||
const rows = await page.locator('table tbody tr')
|
||||
const count = await rows.count()
|
||||
expect(count).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('should edit order', async ({ page }) => {
|
||||
await page.click('a[href="/orders"]')
|
||||
|
||||
// Find and click first order
|
||||
await page.click('table tbody tr:first-child')
|
||||
await page.click('button:has-text("Edit")')
|
||||
|
||||
// Update status
|
||||
await page.selectOption('select[name="status"]', 'SHIPPED')
|
||||
|
||||
// Save
|
||||
await page.click('button:has-text("Save")')
|
||||
|
||||
// Verify update
|
||||
await expect(page.locator('text=Order updated successfully')).toBeVisible()
|
||||
})
|
||||
|
||||
test('should delete order with confirmation', async ({ page }) => {
|
||||
await page.click('a[href="/orders"]')
|
||||
|
||||
const initialCount = await page.locator('table tbody tr').count()
|
||||
|
||||
// Find and click first order
|
||||
await page.click('table tbody tr:first-child')
|
||||
await page.click('button:has-text("Delete")')
|
||||
|
||||
// Confirm deletion
|
||||
await page.click('button:has-text("Confirm")')
|
||||
|
||||
// Verify deletion
|
||||
await expect(page.locator('text=Order deleted successfully')).toBeVisible()
|
||||
const finalCount = await page.locator('table tbody tr').count()
|
||||
expect(finalCount).toBe(initialCount - 1)
|
||||
})
|
||||
|
||||
test('should calculate order totals correctly', async ({ page }) => {
|
||||
await page.click('a[href="/orders"]')
|
||||
await page.click('button:has-text("New Order")')
|
||||
|
||||
await page.selectOption('select[name="customerId"]', 'cust-001')
|
||||
await page.fill('input[name="orderDate"]', '2026-08-01')
|
||||
await page.fill('input[name="dueDate"]', '2026-08-15')
|
||||
|
||||
// Add multiple items
|
||||
await page.click('button:has-text("Add Item")')
|
||||
await page.fill('input[name="productSku"]', 'SKU-001')
|
||||
await page.fill('input[name="quantity"]', '100')
|
||||
await page.fill('input[name="unitPrice"]', '50') // 5000
|
||||
|
||||
await page.click('button:has-text("Add Item")')
|
||||
const skuInputs = await page.locator('input[name="productSku"]')
|
||||
await skuInputs.nth(1).fill('SKU-002')
|
||||
await page.locator('input[name="quantity"]').nth(1).fill('50')
|
||||
await page.locator('input[name="unitPrice"]').nth(1).fill('30') // 1500
|
||||
|
||||
// Verify totals: 6500 subtotal, 650 tax, 7150 total
|
||||
await expect(page.locator('text=Subtotal: $6500')).toBeVisible()
|
||||
await expect(page.locator('text=Tax: $650')).toBeVisible()
|
||||
await expect(page.locator('text=Total: $7150')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useCustomerStore } from '@/stores'
|
||||
|
||||
describe('Customer Workflow Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('Customer Management', () => {
|
||||
it('should create and manage customers', async () => {
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
const customer = await customerStore.createCustomer({
|
||||
code: 'CUST-001',
|
||||
name: 'Test Customer',
|
||||
email: 'test@example.com',
|
||||
phone: '123-456-7890',
|
||||
address: '123 Main St',
|
||||
city: 'Springfield',
|
||||
country: 'USA',
|
||||
creditLimit: 10000
|
||||
})
|
||||
|
||||
expect(customer.code).toBe('CUST-001')
|
||||
expect(customer.status).toBe('ACTIVE')
|
||||
expect(customer.creditUsed).toBe(0)
|
||||
})
|
||||
|
||||
it('should track credit usage', async () => {
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
const customer = await customerStore.createCustomer({
|
||||
code: 'CUST-002',
|
||||
name: 'Credit Test',
|
||||
email: 'credit@test.com',
|
||||
phone: '999-999-9999',
|
||||
address: 'Test',
|
||||
city: 'Test',
|
||||
country: 'USA',
|
||||
creditLimit: 5000
|
||||
})
|
||||
|
||||
await customerStore.updateCreditUsage(customer.id, 2000)
|
||||
const updated = customerStore.customers.find((c) => c.id === customer.id)
|
||||
expect(updated?.creditUsed).toBe(2000)
|
||||
})
|
||||
|
||||
it('should identify at-risk customers', async () => {
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
const customer = await customerStore.createCustomer({
|
||||
code: 'CUST-RISK',
|
||||
name: 'Risk Customer',
|
||||
email: 'risk@test.com',
|
||||
phone: '888-888-8888',
|
||||
address: 'Risk Ave',
|
||||
city: 'Risk City',
|
||||
country: 'USA',
|
||||
creditLimit: 1000
|
||||
})
|
||||
|
||||
// Use 85% of credit
|
||||
await customerStore.updateCreditUsage(customer.id, 850)
|
||||
|
||||
const atRisk = customerStore.atRiskCustomers
|
||||
expect(atRisk.some((c) => c.id === customer.id)).toBe(true)
|
||||
})
|
||||
|
||||
it('should update customer status', async () => {
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
const customer = await customerStore.createCustomer({
|
||||
code: 'CUST-003',
|
||||
name: 'Status Test',
|
||||
email: 'status@test.com',
|
||||
phone: '777-777-7777',
|
||||
address: 'Status St',
|
||||
city: 'Status City',
|
||||
country: 'USA'
|
||||
})
|
||||
|
||||
await customerStore.updateCustomer(customer.id, {
|
||||
status: 'SUSPENDED'
|
||||
})
|
||||
|
||||
const updated = customerStore.customers.find((c) => c.id === customer.id)
|
||||
expect(updated?.status).toBe('SUSPENDED')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Customer Validation', () => {
|
||||
it('should maintain active customer count', async () => {
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
const initial = customerStore.activeCustomers.length
|
||||
|
||||
await customerStore.createCustomer({
|
||||
code: 'CUST-NEW',
|
||||
name: 'New Customer',
|
||||
email: 'new@test.com',
|
||||
phone: '666-666-6666',
|
||||
address: 'New St',
|
||||
city: 'New City',
|
||||
country: 'USA'
|
||||
})
|
||||
|
||||
expect(customerStore.activeCustomers.length).toBe(initial + 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Integration Tests Index
|
||||
* Tests for store + component interactions
|
||||
*
|
||||
* Test Coverage:
|
||||
* - Order Workflow (create → update → delete, filtering, totals)
|
||||
* - Inventory Workflow (reserve, release, low stock alerts)
|
||||
* - Customer Workflow (CRUD, credit management, at-risk detection)
|
||||
* - Multi-store coordination tests
|
||||
*/
|
||||
|
||||
export * from './order-workflow.spec'
|
||||
export * from './inventory-workflow.spec'
|
||||
export * from './customer-workflow.spec'
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useInventoryStore, useWarehouseStore } from '@/stores'
|
||||
|
||||
describe('Inventory Workflow Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('Stock Management', () => {
|
||||
it('should update quantity correctly', async () => {
|
||||
const inventoryStore = useInventoryStore()
|
||||
|
||||
// Initial state
|
||||
expect(inventoryStore.totalQuantity).toBe(0)
|
||||
|
||||
// Update quantity
|
||||
await inventoryStore.updateQuantity('item-001', 100)
|
||||
expect(inventoryStore.totalQuantity).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should reserve and release quantity', async () => {
|
||||
const inventoryStore = useInventoryStore()
|
||||
|
||||
// Setup
|
||||
await inventoryStore.updateQuantity('item-001', 100)
|
||||
const initial = inventoryStore.availableQuantity
|
||||
|
||||
// Reserve
|
||||
await inventoryStore.reserveQuantity('item-001', 30)
|
||||
expect(inventoryStore.availableQuantity).toBeLessThan(initial)
|
||||
|
||||
// Release
|
||||
await inventoryStore.releaseQuantity('item-001', 30)
|
||||
expect(inventoryStore.availableQuantity).toBe(initial)
|
||||
})
|
||||
|
||||
it('should prevent overselling', async () => {
|
||||
const inventoryStore = useInventoryStore()
|
||||
|
||||
await inventoryStore.updateQuantity('item-001', 50)
|
||||
|
||||
try {
|
||||
await inventoryStore.reserveQuantity('item-001', 100)
|
||||
expect.fail('Should throw error')
|
||||
} catch (err: any) {
|
||||
expect(err.message).toContain('Insufficient')
|
||||
}
|
||||
})
|
||||
|
||||
it('should track low stock items', async () => {
|
||||
const inventoryStore = useInventoryStore()
|
||||
|
||||
// Setup low stock
|
||||
await inventoryStore.updateQuantity('item-001', 5) // Below reorder level
|
||||
|
||||
const lowStock = inventoryStore.lowStockItems
|
||||
expect(lowStock.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Warehouse Integration', () => {
|
||||
it('should coordinate inventory with warehouses', async () => {
|
||||
const inventoryStore = useInventoryStore()
|
||||
const warehouseStore = useWarehouseStore()
|
||||
|
||||
await warehouseStore.fetchWarehouses()
|
||||
expect(warehouseStore.warehouseCount).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Inventory in warehouses
|
||||
await inventoryStore.fetchInventory()
|
||||
expect(inventoryStore.items).toBeDefined()
|
||||
})
|
||||
|
||||
it('should update warehouse load based on inventory', async () => {
|
||||
const warehouseStore = useWarehouseStore()
|
||||
|
||||
await warehouseStore.createWarehouse({
|
||||
code: 'WH-TEST',
|
||||
name: 'Test Warehouse',
|
||||
location: 'Test Location',
|
||||
capacity: 1000
|
||||
})
|
||||
|
||||
const warehouses = warehouseStore.warehouses
|
||||
expect(warehouses.length).toBeGreaterThan(0)
|
||||
|
||||
// Update load
|
||||
const warehouse = warehouses[0]
|
||||
await warehouseStore.updateLoad(warehouse.id, 500)
|
||||
expect(warehouse.currentLoad).toBe(500)
|
||||
})
|
||||
|
||||
it('should alert on overloaded warehouses', async () => {
|
||||
const warehouseStore = useWarehouseStore()
|
||||
|
||||
await warehouseStore.createWarehouse({
|
||||
code: 'WH-FULL',
|
||||
name: 'Full Warehouse',
|
||||
location: 'Location',
|
||||
capacity: 100
|
||||
})
|
||||
|
||||
const warehouse = warehouseStore.warehouses[0]
|
||||
await warehouseStore.updateLoad(warehouse.id, 95) // 95% capacity
|
||||
|
||||
const overloaded = warehouseStore.overloadedWarehouses
|
||||
expect(overloaded.some((w) => w.id === warehouse.id)).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Integration Tests: Inventory (WMS) Store
|
||||
* Tests warehouse inventory management with mocked API
|
||||
*
|
||||
* Run: npm run test:integration
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { server } from '@/tests/mocks/server'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
/**
|
||||
* Mock Inventory Store (placeholder until actual store is created)
|
||||
*/
|
||||
class MockInventoryStore {
|
||||
inventory: any[] = []
|
||||
loading = false
|
||||
error: string | null = null
|
||||
|
||||
async fetchInventory() {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch(`${process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/inventory`)
|
||||
const data = await response.json()
|
||||
this.inventory = data.data || []
|
||||
} catch (err) {
|
||||
this.error = (err as Error).message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async updateInventory(inventoryId: string, payload: any) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/inventory/${inventoryId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
)
|
||||
if (!response.ok) throw new Error('Update failed')
|
||||
const data = await response.json()
|
||||
const index = this.inventory.findIndex((i) => i.inventoryId === inventoryId)
|
||||
if (index !== -1) this.inventory[index] = data
|
||||
return data
|
||||
} catch (err) {
|
||||
this.error = (err as Error).message
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Inventory (WMS) Store Integration', () => {
|
||||
let store: MockInventoryStore
|
||||
|
||||
beforeEach(() => {
|
||||
store = new MockInventoryStore()
|
||||
})
|
||||
|
||||
describe('fetchInventory', () => {
|
||||
it('loads inventory from WMS API', async () => {
|
||||
await store.fetchInventory()
|
||||
|
||||
expect(store.inventory).toHaveLength(1)
|
||||
expect(store.inventory[0].inventoryId).toBe('INV-001')
|
||||
expect(store.inventory[0].qtyOnHand).toBe(1000)
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBeNull()
|
||||
})
|
||||
|
||||
it('calculates available quantity correctly', async () => {
|
||||
await store.fetchInventory()
|
||||
|
||||
const inv = store.inventory[0]
|
||||
const available = inv.qtyOnHand - inv.qtyReserved
|
||||
expect(available).toBe(900)
|
||||
expect(inv.qtyAvailable).toBe(available)
|
||||
})
|
||||
|
||||
it('handles API errors gracefully', async () => {
|
||||
server.use(
|
||||
http.get('*/api/inventory', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Database connection failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await store.fetchInventory()
|
||||
|
||||
expect(store.error).not.toBeNull()
|
||||
expect(store.inventory).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateInventory', () => {
|
||||
beforeEach(async () => {
|
||||
await store.fetchInventory()
|
||||
})
|
||||
|
||||
it('updates inventory quantities', async () => {
|
||||
const updates = { qtyOnHand: 1200, qtyReserved: 150 }
|
||||
const updated = await store.updateInventory('INV-001', updates)
|
||||
|
||||
expect(updated.qtyOnHand).toBe(1200)
|
||||
expect(updated.qtyReserved).toBe(150)
|
||||
expect(updated.qtyAvailable).toBe(1050)
|
||||
})
|
||||
|
||||
it('reflects updates in store', async () => {
|
||||
const updates = { qtyOnHand: 800, qtyReserved: 100 }
|
||||
await store.updateInventory('INV-001', updates)
|
||||
|
||||
const inv = store.inventory.find((i) => i.inventoryId === 'INV-001')
|
||||
expect(inv?.qtyOnHand).toBe(800)
|
||||
})
|
||||
|
||||
it('handles update validation errors', async () => {
|
||||
server.use(
|
||||
http.patch('*/api/inventory/:inventoryId', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Quantity cannot be negative' },
|
||||
{ status: 400 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.updateInventory('INV-001', { qtyOnHand: -100 })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('stock availability checks', () => {
|
||||
beforeEach(async () => {
|
||||
await store.fetchInventory()
|
||||
})
|
||||
|
||||
it('reserves stock correctly', async () => {
|
||||
const initial = store.inventory[0]
|
||||
const reservation = 100
|
||||
|
||||
const updated = await store.updateInventory('INV-001', {
|
||||
qtyOnHand: initial.qtyOnHand,
|
||||
qtyReserved: initial.qtyReserved + reservation
|
||||
})
|
||||
|
||||
expect(updated.qtyAvailable).toBe(initial.qtyAvailable - reservation)
|
||||
})
|
||||
|
||||
it('prevents over-reservation', async () => {
|
||||
const current = store.inventory[0]
|
||||
// Try to reserve more than available
|
||||
server.use(
|
||||
http.patch('*/api/inventory/:inventoryId', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Cannot reserve more than available quantity' },
|
||||
{ status: 422 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.updateInventory('INV-001', {
|
||||
qtyOnHand: current.qtyOnHand,
|
||||
qtyReserved: 10000 // Exceed available
|
||||
})
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('multi-warehouse scenarios', () => {
|
||||
it('handles inventory across multiple warehouses', async () => {
|
||||
server.use(
|
||||
http.get('*/api/inventory', () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{ inventoryId: 'INV-001', warehouseId: 'WH-SEOUL', qtyOnHand: 1000 },
|
||||
{ inventoryId: 'INV-002', warehouseId: 'WH-BUSAN', qtyOnHand: 500 },
|
||||
{ inventoryId: 'INV-003', warehouseId: 'WH-DAEGU', qtyOnHand: 300 }
|
||||
]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const tempStore = new MockInventoryStore()
|
||||
await tempStore.fetchInventory()
|
||||
|
||||
expect(tempStore.inventory).toHaveLength(3)
|
||||
const totalQty = tempStore.inventory.reduce((sum, inv) => sum + inv.qtyOnHand, 0)
|
||||
expect(totalQty).toBe(1800)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,221 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useOrderStore, useCustomerStore, useInventoryStore } from '@/stores'
|
||||
import type { Order } from '@/stores'
|
||||
|
||||
describe('Order Workflow Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('Complete Order Lifecycle', () => {
|
||||
it('should create order → update → delete workflow', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
const customerStore = useCustomerStore()
|
||||
|
||||
// Step 1: Fetch customers
|
||||
await customerStore.fetchCustomers()
|
||||
expect(customerStore.activeCustomers.length).toBeGreaterThanOrEqual(0)
|
||||
|
||||
// Step 2: Create order
|
||||
const newOrder = await orderStore.createOrder({
|
||||
customerId: 'cust-001',
|
||||
customerName: 'Test Customer',
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [
|
||||
{
|
||||
id: 'item-001',
|
||||
lineNumber: 1,
|
||||
productId: 'prod-001',
|
||||
productSku: 'SKU-12345',
|
||||
quantity: 100,
|
||||
unitPrice: 50,
|
||||
lineTotal: 5000
|
||||
}
|
||||
],
|
||||
subtotal: 5000,
|
||||
tax: 500,
|
||||
total: 5500
|
||||
})
|
||||
|
||||
expect(newOrder).toBeDefined()
|
||||
expect(newOrder.status).toBe('DRAFT')
|
||||
expect(orderStore.orderCount).toBeGreaterThan(0)
|
||||
|
||||
// Step 3: Update order
|
||||
const updated = await orderStore.updateOrder(newOrder.id, {
|
||||
status: 'CONFIRMED',
|
||||
notes: 'Updated via workflow test'
|
||||
})
|
||||
|
||||
expect(updated.status).toBe('CONFIRMED')
|
||||
expect(updated.notes).toBe('Updated via workflow test')
|
||||
|
||||
// Step 4: Delete order
|
||||
await orderStore.deleteOrder(newOrder.id)
|
||||
expect(orderStore.orders.find((o) => o.id === newOrder.id)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should filter orders by status', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
await orderStore.fetchOrders()
|
||||
|
||||
// Set filter
|
||||
orderStore.setFilter({ status: 'CONFIRMED' })
|
||||
expect(orderStore.filter.status).toBe('CONFIRMED')
|
||||
|
||||
// Filter computed
|
||||
const filtered = orderStore.filteredOrders
|
||||
expect(filtered.every((o) => o.status === 'CONFIRMED')).toBe(true)
|
||||
|
||||
// Clear filter
|
||||
orderStore.clearFilter()
|
||||
expect(orderStore.filter.status).toBe('ALL')
|
||||
})
|
||||
|
||||
it('should calculate order totals correctly', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const order = await orderStore.createOrder({
|
||||
customerId: 'cust-001',
|
||||
customerName: 'Test',
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
lineNumber: 1,
|
||||
productId: 'p1',
|
||||
productSku: 'SKU-001',
|
||||
quantity: 100,
|
||||
unitPrice: 50,
|
||||
lineTotal: 5000
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
lineNumber: 2,
|
||||
productId: 'p2',
|
||||
productSku: 'SKU-002',
|
||||
quantity: 50,
|
||||
unitPrice: 30,
|
||||
lineTotal: 1500
|
||||
}
|
||||
],
|
||||
subtotal: 6500,
|
||||
tax: 650,
|
||||
total: 7150
|
||||
})
|
||||
|
||||
expect(order.subtotal).toBe(6500)
|
||||
expect(order.tax).toBe(650)
|
||||
expect(order.total).toBe(7150)
|
||||
})
|
||||
|
||||
it('should track recent orders', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
// Create multiple orders
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await orderStore.createOrder({
|
||||
customerId: `cust-${i}`,
|
||||
customerName: `Customer ${i}`,
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [],
|
||||
subtotal: 1000,
|
||||
tax: 100,
|
||||
total: 1100
|
||||
})
|
||||
}
|
||||
|
||||
const recent = orderStore.recentOrders
|
||||
expect(recent.length).toBeLessThanOrEqual(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Order Status Transitions', () => {
|
||||
it('should transition through valid statuses', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const order = await orderStore.createOrder({
|
||||
customerId: 'cust-001',
|
||||
customerName: 'Test',
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [],
|
||||
subtotal: 0,
|
||||
tax: 0,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const statuses = ['DRAFT', 'PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED']
|
||||
let current = order
|
||||
|
||||
for (const status of statuses) {
|
||||
current = await orderStore.updateOrder(current.id, { status: status as any })
|
||||
expect(current.status).toBe(status)
|
||||
}
|
||||
})
|
||||
|
||||
it('should count orders by status', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await orderStore.createOrder({
|
||||
customerId: 'cust-001',
|
||||
customerName: 'Test',
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [],
|
||||
subtotal: 0,
|
||||
tax: 0,
|
||||
total: 0,
|
||||
status: i === 0 ? 'CONFIRMED' : 'PENDING'
|
||||
} as any)
|
||||
}
|
||||
|
||||
const byStatus = orderStore.ordersByStatus
|
||||
expect(byStatus['CONFIRMED']).toBeGreaterThan(0)
|
||||
expect(byStatus['PENDING']).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Order Validation', () => {
|
||||
it('should require customer for order', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const order = await orderStore.createOrder({
|
||||
customerId: '',
|
||||
customerName: '',
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [],
|
||||
subtotal: 0,
|
||||
tax: 0,
|
||||
total: 0
|
||||
})
|
||||
|
||||
expect(order.customerId).toBe('')
|
||||
// Validation would be enforced at form level
|
||||
})
|
||||
|
||||
it('should handle empty order items', async () => {
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const order = await orderStore.createOrder({
|
||||
customerId: 'cust-001',
|
||||
customerName: 'Test',
|
||||
orderDate: '2026-08-01',
|
||||
dueDate: '2026-08-15',
|
||||
items: [],
|
||||
subtotal: 0,
|
||||
tax: 0,
|
||||
total: 0
|
||||
})
|
||||
|
||||
expect(order.items.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Integration Tests: Orders Store with API
|
||||
* Tests Pinia store integration with mocked API endpoints
|
||||
*
|
||||
* Run: npm run test:integration
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { useOrderStore } from '@/stores/modules/orders'
|
||||
import { ordersApi } from '@/services/api/client'
|
||||
import { server } from '@/tests/mocks/server'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
/**
|
||||
* Test Suite: Orders Store
|
||||
*/
|
||||
describe('Orders Store with API Integration', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('fetchOrders', () => {
|
||||
it('loads orders from API', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
await store.fetchOrders(100, 0)
|
||||
|
||||
expect(store.orders).toHaveLength(2)
|
||||
expect(store.orders[0].orderNo).toBe('ORD-001')
|
||||
expect(store.loading).toBe(false)
|
||||
expect(store.error).toBeNull()
|
||||
})
|
||||
|
||||
it('sets loading state during fetch', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
// Delay the API response
|
||||
server.use(
|
||||
http.get('*/api/orders', async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
return HttpResponse.json({ data: [] })
|
||||
})
|
||||
)
|
||||
|
||||
const fetchPromise = store.fetchOrders()
|
||||
expect(store.loading).toBe(true)
|
||||
|
||||
await fetchPromise
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('handles API errors gracefully', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
server.use(
|
||||
http.get('*/api/orders', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await store.fetchOrders()
|
||||
|
||||
expect(store.error).not.toBeNull()
|
||||
expect(store.orders).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchOrderById', () => {
|
||||
it('loads single order by ID', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
await store.fetchOrderById('ORD-001')
|
||||
|
||||
expect(store.selectedOrder).not.toBeNull()
|
||||
expect(store.selectedOrder?.orderId).toBe('ORD-001')
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('handles 404 not found', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
server.use(
|
||||
http.get('*/api/orders/:orderId', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Not Found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await store.fetchOrderById('ORD-NONEXISTENT')
|
||||
|
||||
expect(store.error).not.toBeNull()
|
||||
expect(store.selectedOrder).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createOrder', () => {
|
||||
it('creates new order and updates store', async () => {
|
||||
const store = useOrderStore()
|
||||
const payload = {
|
||||
customerId: 'CUST-001',
|
||||
totalAmount: 100000
|
||||
}
|
||||
|
||||
const createdOrder = await store.createOrder(payload)
|
||||
|
||||
expect(createdOrder).toBeDefined()
|
||||
expect(createdOrder.orderId).toBe('ORD-NEW')
|
||||
expect(store.selectedOrder?.orderId).toBe('ORD-NEW')
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('adds created order to store', async () => {
|
||||
const store = useOrderStore()
|
||||
const initialCount = store.orders.length
|
||||
|
||||
await store.createOrder({ customerId: 'CUST-002', totalAmount: 50000 })
|
||||
|
||||
expect(store.orders.length).toBeGreaterThan(initialCount)
|
||||
})
|
||||
|
||||
it('throws error on creation failure', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
server.use(
|
||||
http.post('*/api/orders', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Validation failed' },
|
||||
{ status: 400 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.createOrder({ customerId: 'CUST-001' })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateOrder', () => {
|
||||
it('updates order and reflects changes', async () => {
|
||||
const store = useOrderStore()
|
||||
const orderId = 'ORD-001'
|
||||
const updates = { status: 'CONFIRMED', totalAmount: 150000 }
|
||||
|
||||
const updatedOrder = await store.updateOrder(orderId, updates)
|
||||
|
||||
expect(updatedOrder?.status).toBe('CONFIRMED')
|
||||
expect(updatedOrder?.totalAmount).toBe(150000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteOrder', () => {
|
||||
it('removes order from store', async () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
// Mock: add an order first
|
||||
store.orders.push({
|
||||
orderId: 'ORD-001',
|
||||
orderNo: 'ORD-001',
|
||||
status: 'DRAFT',
|
||||
totalAmount: 100000
|
||||
} as any)
|
||||
|
||||
const initialCount = store.orders.length
|
||||
|
||||
await store.deleteOrder('ORD-001')
|
||||
|
||||
expect(store.orders.length).toBeLessThan(initialCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filtering', () => {
|
||||
it('applies filters to orders', () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
store.setFilter('status', 'DRAFT')
|
||||
store.setFilter('customerId', 'CUST-001')
|
||||
|
||||
expect(store.filters.status).toBe('DRAFT')
|
||||
expect(store.filters.customerId).toBe('CUST-001')
|
||||
})
|
||||
|
||||
it('clears all filters', () => {
|
||||
const store = useOrderStore()
|
||||
|
||||
store.setFilter('status', 'DRAFT')
|
||||
store.setFilter('customerId', 'CUST-001')
|
||||
store.clearFilters()
|
||||
|
||||
expect(Object.keys(store.filters)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Test Suite: Orders API Client
|
||||
*/
|
||||
describe('Orders API Client', () => {
|
||||
it('lists orders via API', async () => {
|
||||
const orders = await ordersApi.listOrders({ limit: 100 })
|
||||
expect(orders).toBeDefined()
|
||||
expect(Array.isArray(orders.data || orders)).toBe(true)
|
||||
})
|
||||
|
||||
it('gets single order via API', async () => {
|
||||
const order = await ordersApi.getOrder('ORD-001')
|
||||
expect(order.orderId).toBe('ORD-001')
|
||||
})
|
||||
|
||||
it('creates order via API', async () => {
|
||||
const payload = { customerId: 'CUST-001', totalAmount: 100000 }
|
||||
const created = await ordersApi.createOrder(payload)
|
||||
expect(created.orderId).toBeDefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* Integration Tests: Products (ERP) Store
|
||||
* Tests product catalog management with mocked API
|
||||
*
|
||||
* Run: npm run test:integration
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { server } from '@/tests/mocks/server'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
/**
|
||||
* Mock Products Store (placeholder until actual store is created)
|
||||
*/
|
||||
class MockProductsStore {
|
||||
products: any[] = []
|
||||
loading = false
|
||||
error: string | null = null
|
||||
filters: Record<string, any> = {}
|
||||
|
||||
async fetchProducts() {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/products`
|
||||
)
|
||||
const data = await response.json()
|
||||
this.products = data.data || []
|
||||
} catch (err) {
|
||||
this.error = (err as Error).message
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async createProduct(payload: any) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/products`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
)
|
||||
if (!response.ok) throw new Error('Creation failed')
|
||||
const data = await response.json()
|
||||
this.products.push(data)
|
||||
return data
|
||||
} catch (err) {
|
||||
this.error = (err as Error).message
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async getProduct(productId: string) {
|
||||
this.loading = true
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/products/${productId}`
|
||||
)
|
||||
if (!response.ok) throw new Error('Not found')
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (err) {
|
||||
this.error = (err as Error).message
|
||||
throw err
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(key: string, value: any) {
|
||||
this.filters[key] = value
|
||||
}
|
||||
|
||||
clearFilters() {
|
||||
this.filters = {}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Products (ERP) Store Integration', () => {
|
||||
let store: MockProductsStore
|
||||
|
||||
beforeEach(() => {
|
||||
store = new MockProductsStore()
|
||||
})
|
||||
|
||||
describe('fetchProducts', () => {
|
||||
it('loads products from ERP API', async () => {
|
||||
await store.fetchProducts()
|
||||
|
||||
expect(store.products).toHaveLength(1)
|
||||
expect(store.products[0].productId).toBe('PROD-001')
|
||||
expect(store.products[0].productName).toBe('Product A')
|
||||
expect(store.loading).toBe(false)
|
||||
})
|
||||
|
||||
it('includes product metadata', async () => {
|
||||
await store.fetchProducts()
|
||||
|
||||
const product = store.products[0]
|
||||
expect(product).toHaveProperty('sku')
|
||||
expect(product).toHaveProperty('categoryId')
|
||||
expect(product).toHaveProperty('status')
|
||||
expect(product).toHaveProperty('createdAt')
|
||||
})
|
||||
|
||||
it('handles empty product list', async () => {
|
||||
server.use(
|
||||
http.get('*/api/products', () => {
|
||||
return HttpResponse.json({ data: [] })
|
||||
})
|
||||
)
|
||||
|
||||
await store.fetchProducts()
|
||||
|
||||
expect(store.products).toHaveLength(0)
|
||||
expect(store.error).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createProduct', () => {
|
||||
it('creates new product with required fields', async () => {
|
||||
const payload = {
|
||||
sku: 'SKU-NEW',
|
||||
productName: 'New Product',
|
||||
categoryId: 'CAT-002'
|
||||
}
|
||||
|
||||
const created = await store.createProduct(payload)
|
||||
|
||||
expect(created.productId).toBe('PROD-NEW')
|
||||
expect(created.sku).toBe('SKU-NEW')
|
||||
expect(created.productName).toBe('New Product')
|
||||
expect(created.status).toBe('ACTIVE')
|
||||
})
|
||||
|
||||
it('adds created product to store', async () => {
|
||||
const initialCount = store.products.length
|
||||
|
||||
await store.createProduct({
|
||||
sku: 'SKU-NEW-2',
|
||||
productName: 'Another Product',
|
||||
categoryId: 'CAT-003'
|
||||
})
|
||||
|
||||
expect(store.products.length).toBeGreaterThan(initialCount)
|
||||
})
|
||||
|
||||
it('validates required fields', async () => {
|
||||
server.use(
|
||||
http.post('*/api/products', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Product name is required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.createProduct({ sku: 'SKU-INVALID' })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('ensures unique SKU', async () => {
|
||||
server.use(
|
||||
http.post('*/api/products', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'SKU already exists' },
|
||||
{ status: 409 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
store.createProduct({ sku: 'SKU-001', productName: 'Duplicate' })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProduct', () => {
|
||||
it('fetches single product by ID', async () => {
|
||||
const product = await store.getProduct('PROD-001')
|
||||
|
||||
expect(product.productId).toBe('PROD-001')
|
||||
expect(product.productName).toBe('Product Sample')
|
||||
})
|
||||
|
||||
it('handles product not found', async () => {
|
||||
server.use(
|
||||
http.get('*/api/products/:productId', () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Product not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await expect(store.getProduct('PROD-INVALID')).rejects.toThrow()
|
||||
expect(store.error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('product filtering', () => {
|
||||
beforeEach(async () => {
|
||||
server.use(
|
||||
http.get('*/api/products', () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{ productId: 'PROD-001', categoryId: 'CAT-001', status: 'ACTIVE' },
|
||||
{ productId: 'PROD-002', categoryId: 'CAT-001', status: 'ACTIVE' },
|
||||
{ productId: 'PROD-003', categoryId: 'CAT-002', status: 'INACTIVE' },
|
||||
{ productId: 'PROD-004', categoryId: 'CAT-002', status: 'ACTIVE' }
|
||||
]
|
||||
})
|
||||
})
|
||||
)
|
||||
await store.fetchProducts()
|
||||
})
|
||||
|
||||
it('filters by category', () => {
|
||||
store.setFilter('categoryId', 'CAT-001')
|
||||
|
||||
const filtered = store.products.filter((p) => p.categoryId === store.filters.categoryId)
|
||||
expect(filtered).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('filters by status', () => {
|
||||
store.setFilter('status', 'ACTIVE')
|
||||
|
||||
const filtered = store.products.filter((p) => p.status === store.filters.status)
|
||||
expect(filtered.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('combines multiple filters', () => {
|
||||
store.setFilter('categoryId', 'CAT-001')
|
||||
store.setFilter('status', 'ACTIVE')
|
||||
|
||||
const filtered = store.products.filter(
|
||||
(p) =>
|
||||
p.categoryId === store.filters.categoryId &&
|
||||
p.status === store.filters.status
|
||||
)
|
||||
expect(filtered).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('clears all filters', () => {
|
||||
store.setFilter('categoryId', 'CAT-001')
|
||||
store.setFilter('status', 'ACTIVE')
|
||||
store.clearFilters()
|
||||
|
||||
expect(Object.keys(store.filters)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('bulk operations', () => {
|
||||
it('handles bulk product creation', async () => {
|
||||
server.use(
|
||||
http.post('*/api/products/bulk', async ({ request }) => {
|
||||
const body = await request.json() as any
|
||||
return HttpResponse.json(
|
||||
{
|
||||
created: body.items.length,
|
||||
items: body.items.map((item: any, idx: number) => ({
|
||||
productId: `PROD-BULK-${idx + 1}`,
|
||||
...item
|
||||
}))
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const items = [
|
||||
{ sku: 'SKU-BULK-1', productName: 'Bulk Product 1' },
|
||||
{ sku: 'SKU-BULK-2', productName: 'Bulk Product 2' }
|
||||
]
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'}/products/bulk`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items })
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.created).toBe(2)
|
||||
expect(data.items).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* MSW (Mock Service Worker) Handlers
|
||||
* Provides mock API responses for testing
|
||||
*
|
||||
* Usage in tests:
|
||||
* import { server } from '@/tests/mocks/server'
|
||||
*
|
||||
* beforeEach(() => server.listen())
|
||||
* afterEach(() => server.close())
|
||||
*
|
||||
* it('fetches orders', async () => {
|
||||
* const response = await fetch('/api/orders')
|
||||
* expect(response.status).toBe(200)
|
||||
* })
|
||||
*/
|
||||
|
||||
import { http, HttpResponse } from 'msw'
|
||||
|
||||
const API_BASE = process.env.VITE_API_BASE_URL || 'http://localhost:3000/api'
|
||||
|
||||
/**
|
||||
* Mock Handlers
|
||||
* Intercept HTTP requests and return predefined responses
|
||||
*/
|
||||
export const handlers = [
|
||||
// ===== ORDERS (OMS) =====
|
||||
|
||||
http.get(`${API_BASE}/orders`, () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{
|
||||
orderId: 'ORD-001',
|
||||
orderNo: 'ORD-001',
|
||||
customerId: 'CUST-001',
|
||||
orderDate: '2026-08-12',
|
||||
totalAmount: 100000,
|
||||
status: 'DRAFT',
|
||||
createdBy: 'user@example.com',
|
||||
createdAt: '2026-08-12T10:00:00Z'
|
||||
},
|
||||
{
|
||||
orderId: 'ORD-002',
|
||||
orderNo: 'ORD-002',
|
||||
customerId: 'CUST-002',
|
||||
orderDate: '2026-08-11',
|
||||
totalAmount: 250000,
|
||||
status: 'CONFIRMED',
|
||||
createdBy: 'user@example.com',
|
||||
createdAt: '2026-08-11T09:00:00Z'
|
||||
}
|
||||
],
|
||||
totalCount: 2
|
||||
})
|
||||
}),
|
||||
|
||||
http.get(`${API_BASE}/orders/:orderId`, ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
orderId: params.orderId,
|
||||
orderNo: `ORD-${String(params.orderId).padStart(3, '0')}`,
|
||||
customerId: 'CUST-001',
|
||||
orderDate: '2026-08-12',
|
||||
totalAmount: 100000,
|
||||
status: 'DRAFT',
|
||||
createdBy: 'user@example.com',
|
||||
createdAt: '2026-08-12T10:00:00Z'
|
||||
})
|
||||
}),
|
||||
|
||||
http.post(`${API_BASE}/orders`, async ({ request }) => {
|
||||
const body = await request.json() as any
|
||||
return HttpResponse.json(
|
||||
{
|
||||
orderId: 'ORD-NEW',
|
||||
orderNo: 'ORD-NEW',
|
||||
customerId: body.customerId,
|
||||
orderDate: new Date().toISOString().split('T')[0],
|
||||
totalAmount: body.totalAmount || 0,
|
||||
status: 'DRAFT',
|
||||
createdBy: 'user@example.com',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
}),
|
||||
|
||||
http.put(`${API_BASE}/orders/:orderId`, async ({ params, request }) => {
|
||||
const body = await request.json() as any
|
||||
return HttpResponse.json({
|
||||
orderId: params.orderId,
|
||||
orderNo: `ORD-${String(params.orderId).padStart(3, '0')}`,
|
||||
customerId: body.customerId,
|
||||
orderDate: body.orderDate,
|
||||
totalAmount: body.totalAmount,
|
||||
status: body.status,
|
||||
createdBy: 'user@example.com',
|
||||
createdAt: '2026-08-12T10:00:00Z',
|
||||
modifiedBy: 'user@example.com',
|
||||
modifiedAt: new Date().toISOString()
|
||||
})
|
||||
}),
|
||||
|
||||
http.delete(`${API_BASE}/orders/:orderId`, () => {
|
||||
return HttpResponse.json(
|
||||
{ message: 'Order deleted successfully' },
|
||||
{ status: 204 }
|
||||
)
|
||||
}),
|
||||
|
||||
// ===== INVENTORY (WMS) =====
|
||||
|
||||
http.get(`${API_BASE}/inventory`, () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{
|
||||
inventoryId: 'INV-001',
|
||||
warehouseId: 'WH-SEOUL',
|
||||
productId: 'PROD-001',
|
||||
qtyOnHand: 1000,
|
||||
qtyReserved: 100,
|
||||
qtyAvailable: 900,
|
||||
status: 'ACTIVE'
|
||||
}
|
||||
]
|
||||
})
|
||||
}),
|
||||
|
||||
http.patch(`${API_BASE}/inventory/:inventoryId`, async ({ params, request }) => {
|
||||
const body = await request.json() as any
|
||||
return HttpResponse.json({
|
||||
inventoryId: params.inventoryId,
|
||||
warehouseId: 'WH-SEOUL',
|
||||
productId: 'PROD-001',
|
||||
qtyOnHand: body.qtyOnHand,
|
||||
qtyReserved: body.qtyReserved,
|
||||
qtyAvailable: (body.qtyOnHand - body.qtyReserved),
|
||||
status: 'ACTIVE',
|
||||
modifiedAt: new Date().toISOString()
|
||||
})
|
||||
}),
|
||||
|
||||
// ===== PRODUCTS (ERP) =====
|
||||
|
||||
http.get(`${API_BASE}/products`, () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{
|
||||
productId: 'PROD-001',
|
||||
sku: 'SKU-001',
|
||||
productName: 'Product A',
|
||||
categoryId: 'CAT-001',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-08-01T00:00:00Z'
|
||||
}
|
||||
]
|
||||
})
|
||||
}),
|
||||
|
||||
http.get(`${API_BASE}/products/:productId`, ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
productId: params.productId,
|
||||
sku: `SKU-${params.productId}`,
|
||||
productName: 'Product Sample',
|
||||
categoryId: 'CAT-001',
|
||||
status: 'ACTIVE',
|
||||
createdAt: '2026-08-01T00:00:00Z'
|
||||
})
|
||||
}),
|
||||
|
||||
http.post(`${API_BASE}/products`, async ({ request }) => {
|
||||
const body = await request.json() as any
|
||||
return HttpResponse.json(
|
||||
{
|
||||
productId: 'PROD-NEW',
|
||||
sku: body.sku,
|
||||
productName: body.productName,
|
||||
categoryId: body.categoryId,
|
||||
status: 'ACTIVE',
|
||||
createdAt: new Date().toISOString()
|
||||
},
|
||||
{ status: 201 }
|
||||
)
|
||||
}),
|
||||
|
||||
// ===== ERROR HANDLER =====
|
||||
|
||||
http.all('*', ({ request }) => {
|
||||
console.warn(`Unhandled request: ${request.method} ${request.url}`)
|
||||
return HttpResponse.json(
|
||||
{ message: 'Not Found', path: request.url },
|
||||
{ status: 404 }
|
||||
)
|
||||
})
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* MSW Server Setup
|
||||
* Initialize mock server for testing
|
||||
*/
|
||||
|
||||
import { setupServer } from 'msw/node'
|
||||
import { handlers } from './handlers'
|
||||
|
||||
/**
|
||||
* Mock server instance
|
||||
* Automatically intercepts all HTTP requests during tests
|
||||
*/
|
||||
export const server = setupServer(...handlers)
|
||||
|
||||
// Enable request interception
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
|
||||
|
||||
// Reset handlers after each test
|
||||
afterEach(() => server.resetHandlers())
|
||||
|
||||
// Disable request interception after tests
|
||||
afterAll(() => server.close())
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Vitest Global Setup
|
||||
* Initializes MSW server for all test suites
|
||||
*/
|
||||
|
||||
import { beforeAll, afterEach, afterAll, vi } from 'vitest'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { handlers } from './mocks/handlers'
|
||||
|
||||
/**
|
||||
* MSW Server Instance
|
||||
* Automatically intercepts HTTP requests during tests
|
||||
*/
|
||||
export const server = setupServer(...handlers)
|
||||
|
||||
/**
|
||||
* Global Test Lifecycle Hooks
|
||||
*/
|
||||
|
||||
// Enable request interception before all tests
|
||||
beforeAll(() => {
|
||||
server.listen({ onUnhandledRequest: 'warn' })
|
||||
})
|
||||
|
||||
// Reset handlers after each test to ensure test isolation
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
// Disable request interception after all tests
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
/**
|
||||
* Mock console methods to reduce test output noise
|
||||
* Remove these if debugging is needed
|
||||
*/
|
||||
beforeAll(() => {
|
||||
// Optional: suppress console.warn for unhandled requests
|
||||
// vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
Reference in New Issue
Block a user