/// /** * Test Pattern: Shopping List * * Tests the core shopping list functionality: * - Initial state (empty list) * - Adding items * - Marking items as done * - Removing items * - Statistics (totalCount, doneCount, remainingCount) * * Run: deno task ct test packages/patterns/shopping-list.test.tsx --verbose */ import { computed, handler, pattern, Writable } from "commontools"; import ShoppingList from "./shopping-list.tsx"; interface ShoppingItem { title: string; done: boolean; aisleSeed: number; aisleOverride: string; } // Handler to set items const setItems = handler< void, { items: Writable; data: ShoppingItem[] } >( (_event, { items, data }) => { // Copy to make mutable items.set([...data]); }, ); // Handler to set store layout const setLayout = handler< void, { storeLayout: Writable; layout: string } >( (_event, { storeLayout, layout }) => { storeLayout.set(layout); }, ); export default pattern(() => { // Create writable cells that we control const itemsCell = Writable.of([]); const layoutCell = Writable.of(""); // Instantiate the shopping list pattern const list = ShoppingList({ items: itemsCell, storeLayout: layoutCell, }); // ========================================================================== // Actions - bind handlers with hardcoded data // ========================================================================== const action_add_milk = setItems({ items: itemsCell, data: [{ title: "Milk", done: false, aisleSeed: 0, aisleOverride: "" }], }); const action_add_bread_eggs = setItems({ items: itemsCell, data: [ { title: "Milk", done: false, aisleSeed: 0, aisleOverride: "" }, { title: "Bread", done: false, aisleSeed: 0, aisleOverride: "" }, { title: "Eggs", done: false, aisleSeed: 0, aisleOverride: "" }, ], }); const action_mark_first_done = setItems({ items: itemsCell, data: [ { title: "Milk", done: true, aisleSeed: 0, aisleOverride: "" }, { title: "Bread", done: false, aisleSeed: 0, aisleOverride: "" }, { title: "Eggs", done: false, aisleSeed: 0, aisleOverride: "" }, ], }); const action_remove_first = setItems({ items: itemsCell, data: [ { title: "Bread", done: false, aisleSeed: 0, aisleOverride: "" }, { title: "Eggs", done: false, aisleSeed: 0, aisleOverride: "" }, ], }); const action_set_store_layout = setLayout({ storeLayout: layoutCell, layout: `# Aisle 1 Dairy, Milk, Eggs # Aisle 2 Bread, Bakery # Produce Fresh vegetables and fruits `, }); const action_clear_store_layout = setLayout({ storeLayout: layoutCell, layout: "", }); // ========================================================================== // Assertions // ========================================================================== // Initial state - use totalCount which is computed from items.get().length const assert_initial_empty = computed(() => list.totalCount === 0); const assert_initial_total_zero = computed(() => list.totalCount === 0); const assert_initial_done_zero = computed(() => list.doneCount === 0); const assert_initial_remaining_zero = computed(() => list.remainingCount === 0 ); // After adding one item const assert_one_item = computed(() => list.totalCount === 1); const assert_total_one = computed(() => list.totalCount === 1); const assert_remaining_one = computed(() => list.remainingCount === 1); const assert_first_item_milk = computed(() => list.items[0]?.title === "Milk" ); // After adding three items const assert_three_items = computed(() => list.totalCount === 3); const assert_total_three = computed(() => list.totalCount === 3); const assert_remaining_three = computed(() => list.remainingCount === 3); const assert_done_still_zero = computed(() => list.doneCount === 0); // After marking first done const assert_done_one = computed(() => list.doneCount === 1); const assert_remaining_two = computed(() => list.remainingCount === 2); const assert_first_is_done = computed(() => list.items[0]?.done === true); // After removing first item const assert_two_items = computed(() => list.totalCount === 2); const assert_total_two = computed(() => list.totalCount === 2); const assert_done_zero_after_remove = computed(() => list.doneCount === 0); const assert_first_now_bread = computed(() => list.items[0]?.title === "Bread" ); // Store layout const assert_no_layout_initially = computed(() => String(list.storeLayout).trim().length === 0 ); const assert_has_layout = computed(() => String(list.storeLayout).trim().length > 0 ); const assert_layout_cleared = computed(() => String(list.storeLayout).trim().length === 0 ); // ========================================================================== // Test Sequence // ========================================================================== return { tests: [ // === Test 1: Initial empty state === { assertion: assert_initial_empty }, { assertion: assert_initial_total_zero }, { assertion: assert_initial_done_zero }, { assertion: assert_initial_remaining_zero }, { assertion: assert_no_layout_initially }, // === Test 2: Add first item === { action: action_add_milk }, { assertion: assert_one_item }, { assertion: assert_total_one }, { assertion: assert_remaining_one }, { assertion: assert_first_item_milk }, // === Test 3: Add more items === { action: action_add_bread_eggs }, { assertion: assert_three_items }, { assertion: assert_total_three }, { assertion: assert_remaining_three }, { assertion: assert_done_still_zero }, // === Test 4: Mark item as done === { action: action_mark_first_done }, { assertion: assert_done_one }, { assertion: assert_remaining_two }, { assertion: assert_first_is_done }, // === Test 5: Remove item === { action: action_remove_first }, { assertion: assert_two_items }, { assertion: assert_total_two }, { assertion: assert_done_zero_after_remove }, { assertion: assert_first_now_bread }, // === Test 6: Store layout === { action: action_set_store_layout }, { assertion: assert_has_layout }, { action: action_clear_store_layout }, { assertion: assert_layout_cleared }, ], list, }; });