|
// EZShop™ Books — Test Suite |
|
// Loaded after store.js; run via tests.html in the browser. |
|
|
|
let _passed = 0; |
|
let _failed = 0; |
|
const _results: string[] = []; |
|
|
|
function assert(condition: boolean, name: string) { |
|
if (condition) { |
|
_passed++; |
|
_results.push(`✓ ${name}`); |
|
} else { |
|
_failed++; |
|
_results.push(`✗ ${name}`); |
|
} |
|
} |
|
|
|
function assertClose(actual: number, expected: number, name: string) { |
|
assert(Math.abs(actual - expected) < 0.01, `${name} (got ${actual}, expected ${expected})`); |
|
} |
|
|
|
// ---- Test data ---- |
|
|
|
const TEST_PRODUCTS: Product[] = [ |
|
{ id: 1, name: "Book A", author: "Alice Smith", genres: ["programming", "software-engineering"], price: 40.00 }, |
|
{ id: 2, name: "Book B", author: "Bob Jones", genres: ["programming"], price: 20.00 }, |
|
{ id: 3, name: "Code C", author: "Alice Jones", genres: ["programming", "computer-science"], price: 50.00 }, |
|
{ id: 4, name: "Algorithms", author: "Carol White", genres: ["computer-science"], price: 60.00 }, |
|
{ id: 5, name: "Managing Teams", author: "Dan Green", genres: ["management"], price: 25.00 }, |
|
]; |
|
|
|
// ---- parseProductsCsv ---- |
|
|
|
function testParseCsv() { |
|
const csv = "id,name,author,genres,price\n1,Widget,Jane Doe,programming;cs,19.99\n2,Novel,John Smith,management,12.50\n"; |
|
const products = parseProductsCsv(csv); |
|
|
|
assert(products.length === 2, "parseProductsCsv: correct row count"); |
|
assert(products[0].name === "Widget", "parseProductsCsv: name parsed"); |
|
assert(products[0].author === "Jane Doe", "parseProductsCsv: author parsed"); |
|
assert(products[0].genres.length === 2, "parseProductsCsv: genres split"); |
|
assert(products[0].genres[0] === "programming", "parseProductsCsv: first genre"); |
|
assert(products[0].genres[1] === "cs", "parseProductsCsv: second genre"); |
|
assert(products[1].genres.length === 1, "parseProductsCsv: single genre"); |
|
} |
|
|
|
// ---- filterByTitle ---- |
|
|
|
function testFilterByTitleMatch() { |
|
const result = filterByTitle(TEST_PRODUCTS, "book"); |
|
assert(result.length === 2, "filterByTitle: finds 2 books"); |
|
} |
|
|
|
function testFilterByTitleEmpty() { |
|
const result = filterByTitle(TEST_PRODUCTS, ""); |
|
assert(result.length === TEST_PRODUCTS.length, "filterByTitle empty: returns all"); |
|
} |
|
|
|
function testFilterByTitleNoMatch() { |
|
const result = filterByTitle(TEST_PRODUCTS, "xyz"); |
|
assert(result.length === 0, "filterByTitle no match: returns empty"); |
|
} |
|
|
|
// ---- filterByAuthor ---- |
|
|
|
function testFilterByAuthorMatch() { |
|
const result = filterByAuthor(TEST_PRODUCTS, "alice"); |
|
assert(result.length === 2, "filterByAuthor: finds 2 by Alice"); |
|
} |
|
|
|
function testFilterByAuthorEmpty() { |
|
const result = filterByAuthor(TEST_PRODUCTS, ""); |
|
assert(result.length === TEST_PRODUCTS.length, "filterByAuthor empty: returns all"); |
|
} |
|
|
|
function testFilterByAuthorNoMatch() { |
|
const result = filterByAuthor(TEST_PRODUCTS, "xyz"); |
|
assert(result.length === 0, "filterByAuthor no match: returns empty"); |
|
} |
|
|
|
// ---- sortByTitle ---- |
|
|
|
function testSortByTitle() { |
|
const result = sortByTitle(TEST_PRODUCTS); |
|
assert(result[0].name === "Algorithms", "sortByTitle: Algorithms first"); |
|
assert(result[1].name === "Book A", "sortByTitle: Book A second"); |
|
} |
|
|
|
function testSortByTitleImmutable() { |
|
const original = [TEST_PRODUCTS[2], TEST_PRODUCTS[0]]; |
|
const sorted = sortByTitle(original); |
|
assert(original[0].id === 3, "sortByTitle: original unchanged"); |
|
assert(sorted[0].id === 1, "sortByTitle: sorted copy reordered"); |
|
} |
|
|
|
// ---- sortByPrice ---- |
|
|
|
function testSortByPrice() { |
|
const result = sortByPrice(TEST_PRODUCTS); |
|
assert(result[0].price === 20.00, "sortByPrice: cheapest first"); |
|
assert(result[result.length - 1].price === 60.00, "sortByPrice: most expensive last"); |
|
} |
|
|
|
function testSortByPriceImmutable() { |
|
const original = [TEST_PRODUCTS[0], TEST_PRODUCTS[1]]; |
|
const sorted = sortByPrice(original); |
|
assert(original[0].id === 1, "sortByPrice: original unchanged"); |
|
assert(sorted[0].id === 2, "sortByPrice: sorted copy reordered"); |
|
} |
|
|
|
// ---- addToCart ---- |
|
|
|
function testAddToCartNew() { |
|
const cart = addToCart([], 1); |
|
assert(cart.length === 1, "addToCart new: one entry"); |
|
assert(cart[0].productId === 1, "addToCart new: correct product"); |
|
assert(cart[0].quantity === 1, "addToCart new: quantity 1"); |
|
} |
|
|
|
function testAddToCartExisting() { |
|
const cart = addToCart([{ productId: 1, quantity: 1 }], 1); |
|
assert(cart.length === 1, "addToCart existing: still one entry"); |
|
assert(cart[0].quantity === 2, "addToCart existing: quantity incremented"); |
|
} |
|
|
|
function testAddToCartImmutable() { |
|
const original: CartEntry[] = [{ productId: 1, quantity: 1 }]; |
|
const updated = addToCart(original, 1); |
|
assert(original[0].quantity === 1, "addToCart: original unchanged"); |
|
assert(updated[0].quantity === 2, "addToCart: new cart updated"); |
|
} |
|
|
|
// ---- changeQuantity ---- |
|
|
|
function testChangeQuantityUp() { |
|
const cart = changeQuantity([{ productId: 1, quantity: 1 }], 1, 1); |
|
assert(cart[0].quantity === 2, "changeQuantity +1: quantity increased"); |
|
} |
|
|
|
function testChangeQuantityDown() { |
|
const cart = changeQuantity([{ productId: 1, quantity: 3 }], 1, -1); |
|
assert(cart[0].quantity === 2, "changeQuantity -1: quantity decreased"); |
|
} |
|
|
|
function testChangeQuantityRemoves() { |
|
const cart = changeQuantity([{ productId: 1, quantity: 1 }], 1, -1); |
|
assert(cart.length === 0, "changeQuantity to 0: entry removed"); |
|
} |
|
|
|
function testChangeQuantityImmutable() { |
|
const original: CartEntry[] = [{ productId: 1, quantity: 2 }]; |
|
const updated = changeQuantity(original, 1, -1); |
|
assert(original[0].quantity === 2, "changeQuantity: original unchanged"); |
|
assert(updated[0].quantity === 1, "changeQuantity: new cart updated"); |
|
} |
|
|
|
// ---- getLineItemPricing: programming books ---- |
|
|
|
function testPricingProgrammingOneItem() { |
|
const cart: CartEntry[] = [{ productId: 1, quantity: 1 }]; |
|
const result = getLineItemPricing(TEST_PRODUCTS[0], cart); |
|
assertClose(result.salePrice, 32.00, "programming 1 item: 20% off"); |
|
assert(result.promotion !== null, "programming 1 item: has promotion"); |
|
} |
|
|
|
function testPricingProgrammingThreeItems() { |
|
const cart: CartEntry[] = [{ productId: 1, quantity: 3 }]; |
|
const result = getLineItemPricing(TEST_PRODUCTS[0], cart); |
|
assertClose(result.salePrice, 28.00, "programming 3 items: 30% off"); |
|
assert(result.promotion!.includes("30%"), "programming 3 items: promotion says 30%"); |
|
} |
|
|
|
// ---- getLineItemPricing: computer-science books (no special discount) ---- |
|
|
|
function testPricingCsNoDiscount() { |
|
const cart: CartEntry[] = [{ productId: 4, quantity: 1 }]; |
|
const result = getLineItemPricing(TEST_PRODUCTS[3], cart); |
|
assertClose(result.salePrice, 60.00, "cs-only: full price"); |
|
assert(result.promotion === null, "cs-only: no promotion"); |
|
} |
|
|
|
// ---- getLineItemPricing: no discount genres ---- |
|
|
|
function testPricingManagementNoDiscount() { |
|
const cart: CartEntry[] = [{ productId: 5, quantity: 1 }]; |
|
const result = getLineItemPricing(TEST_PRODUCTS[4], cart); |
|
assertClose(result.salePrice, 25.00, "management: full price"); |
|
assert(result.promotion === null, "management: no promotion"); |
|
} |
|
|
|
// ---- calculateCart ---- |
|
|
|
function testEmptyCart() { |
|
const result = calculateCart([], TEST_PRODUCTS); |
|
assert(result.lineItems.length === 0, "empty cart: no line items"); |
|
assertClose(result.subtotal, 0, "empty cart: subtotal 0"); |
|
assertClose(result.shipping, 5.99, "empty cart: shipping charged"); |
|
assertClose(result.total, 5.99, "empty cart: total = shipping only"); |
|
} |
|
|
|
function testCartMixedDiscounts() { |
|
const cart: CartEntry[] = [ |
|
{ productId: 1, quantity: 1 }, // programming: $40 -> $28 (30% off, 3+ items) |
|
{ productId: 4, quantity: 1 }, // cs-only: $60 (full price) |
|
{ productId: 5, quantity: 1 }, // management: $25 (full price) |
|
]; |
|
const result = calculateCart(cart, TEST_PRODUCTS); |
|
assert(result.lineItems.length === 3, "mixed cart: 3 line items"); |
|
assertClose(result.lineItems[0].unitPrice, 28.00, "mixed cart: programming at 30% (3+ items)"); |
|
assertClose(result.lineItems[1].unitPrice, 60.00, "mixed cart: cs full price"); |
|
assertClose(result.lineItems[2].unitPrice, 25.00, "mixed cart: management full price"); |
|
} |
|
|
|
function testShippingUnder50() { |
|
const cart: CartEntry[] = [{ productId: 5, quantity: 1 }]; // $25, no discount |
|
const result = calculateCart(cart, TEST_PRODUCTS); |
|
assertClose(result.shipping, 5.99, "under $50: shipping charged"); |
|
assert(result.shippingPromo === null, "under $50: no shipping promo"); |
|
} |
|
|
|
function testShippingOver50() { |
|
const cart: CartEntry[] = [{ productId: 4, quantity: 1 }]; // $60 -> $51 |
|
const result = calculateCart(cart, TEST_PRODUCTS); |
|
assertClose(result.shipping, 0, "over $50: free shipping"); |
|
assert(result.shippingPromo !== null, "over $50: shipping promo present"); |
|
} |
|
|
|
function testTotalEqualsSubtotalPlusShipping() { |
|
const cart: CartEntry[] = [{ productId: 5, quantity: 1 }]; |
|
const result = calculateCart(cart, TEST_PRODUCTS); |
|
assertClose(result.total, result.subtotal + result.shipping, "total = subtotal + shipping"); |
|
} |
|
|
|
// ---- Run all tests ---- |
|
|
|
function runAllTests() { |
|
testParseCsv(); |
|
testFilterByTitleMatch(); |
|
testFilterByTitleEmpty(); |
|
testFilterByTitleNoMatch(); |
|
testFilterByAuthorMatch(); |
|
testFilterByAuthorEmpty(); |
|
testFilterByAuthorNoMatch(); |
|
testSortByTitle(); |
|
testSortByTitleImmutable(); |
|
testSortByPrice(); |
|
testSortByPriceImmutable(); |
|
testAddToCartNew(); |
|
testAddToCartExisting(); |
|
testAddToCartImmutable(); |
|
testChangeQuantityUp(); |
|
testChangeQuantityDown(); |
|
testChangeQuantityRemoves(); |
|
testChangeQuantityImmutable(); |
|
testPricingProgrammingOneItem(); |
|
testPricingProgrammingThreeItems(); |
|
testPricingCsNoDiscount(); |
|
testPricingManagementNoDiscount(); |
|
testEmptyCart(); |
|
testCartMixedDiscounts(); |
|
testShippingUnder50(); |
|
testShippingOver50(); |
|
testTotalEqualsSubtotalPlusShipping(); |
|
} |
Do you know if this was Opus or Sonnet?