diff --git a/api/routes/_.context.ts b/api/routes/_.context.ts index 2bbd799..7713817 100644 --- a/api/routes/_.context.ts +++ b/api/routes/_.context.ts @@ -13,87 +13,364 @@ import type { Pet } from "../types/components/schemas/Pet.js"; import type { User } from "../types/components/schemas/User.js"; import type { Order } from "../types/components/schemas/Order.js"; +interface Scenario { + pets: Pet[]; + users: User[]; + orders: Order[]; +} + +/** + * Pre-defined scenarios that can be loaded into the context at any time. + * + * Load a scenario from the REPL with: + * context.loadScenario("empty") + * + * Available scenarios: startup, empty, sold-out, busy + */ +const SCENARIOS: Record = { + /** + * Default startup state: a small mix of pets, a couple of users, and a + * couple of orders — enough for basic end-to-end exploration. + */ + startup: { + pets: [ + { + id: 1, + name: "Buddy", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/buddy.jpg"], + tags: [{ id: 1, name: "friendly" }], + status: "available", + }, + { + id: 2, + name: "Whiskers", + category: { id: 2, name: "Cats" }, + photoUrls: ["https://example.com/whiskers.jpg"], + tags: [{ id: 2, name: "indoor" }], + status: "available", + }, + { + id: 3, + name: "Goldie", + category: { id: 3, name: "Fish" }, + photoUrls: ["https://example.com/goldie.jpg"], + tags: [], + status: "pending", + }, + { + id: 4, + name: "Max", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/max.jpg"], + tags: [{ id: 3, name: "trained" }], + status: "sold", + }, + ], + users: [ + { + id: 1, + username: "user1", + firstName: "John", + lastName: "Doe", + email: "john@example.com", + password: "password123", + phone: "555-1234", + userStatus: 1, + }, + { + id: 2, + username: "jane_smith", + firstName: "Jane", + lastName: "Smith", + email: "jane@example.com", + password: "secret456", + phone: "555-5678", + userStatus: 1, + }, + ], + orders: [ + { + id: 1, + petId: 1, + quantity: 1, + shipDate: "2024-01-15T10:00:00Z", + status: "placed", + complete: false, + }, + { + id: 2, + petId: 4, + quantity: 1, + shipDate: "2024-01-10T08:00:00Z", + status: "delivered", + complete: true, + }, + ], + }, + + /** + * Empty store: no pets, users, or orders. + * Useful for testing the "no data" state or building up state from scratch. + */ + empty: { + pets: [], + users: [], + orders: [], + }, + + /** + * Sold-out store: all pets have been sold and all orders are complete. + * Useful for testing behaviour when no pets are available. + */ + "sold-out": { + pets: [ + { + id: 1, + name: "Buddy", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/buddy.jpg"], + tags: [{ id: 1, name: "friendly" }], + status: "sold", + }, + { + id: 2, + name: "Whiskers", + category: { id: 2, name: "Cats" }, + photoUrls: ["https://example.com/whiskers.jpg"], + tags: [{ id: 2, name: "indoor" }], + status: "sold", + }, + { + id: 3, + name: "Goldie", + category: { id: 3, name: "Fish" }, + photoUrls: ["https://example.com/goldie.jpg"], + tags: [], + status: "sold", + }, + ], + users: [ + { + id: 1, + username: "user1", + firstName: "John", + lastName: "Doe", + email: "john@example.com", + password: "password123", + phone: "555-1234", + userStatus: 1, + }, + ], + orders: [ + { + id: 1, + petId: 1, + quantity: 1, + shipDate: "2024-01-10T08:00:00Z", + status: "delivered", + complete: true, + }, + { + id: 2, + petId: 2, + quantity: 1, + shipDate: "2024-01-11T09:00:00Z", + status: "delivered", + complete: true, + }, + { + id: 3, + petId: 3, + quantity: 1, + shipDate: "2024-01-12T10:00:00Z", + status: "delivered", + complete: true, + }, + ], + }, + + /** + * Busy store: a large and varied inventory with many pending orders. + * Useful for load-testing UI components or pagination. + */ + busy: { + pets: [ + { + id: 1, + name: "Buddy", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/buddy.jpg"], + tags: [{ id: 1, name: "friendly" }], + status: "available", + }, + { + id: 2, + name: "Whiskers", + category: { id: 2, name: "Cats" }, + photoUrls: ["https://example.com/whiskers.jpg"], + tags: [{ id: 2, name: "indoor" }], + status: "available", + }, + { + id: 3, + name: "Goldie", + category: { id: 3, name: "Fish" }, + photoUrls: ["https://example.com/goldie.jpg"], + tags: [], + status: "available", + }, + { + id: 4, + name: "Max", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/max.jpg"], + tags: [{ id: 3, name: "trained" }], + status: "available", + }, + { + id: 5, + name: "Tweety", + category: { id: 4, name: "Birds" }, + photoUrls: ["https://example.com/tweety.jpg"], + tags: [{ id: 4, name: "singing" }], + status: "available", + }, + { + id: 6, + name: "Bella", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/bella.jpg"], + tags: [{ id: 1, name: "friendly" }], + status: "pending", + }, + { + id: 7, + name: "Luna", + category: { id: 2, name: "Cats" }, + photoUrls: ["https://example.com/luna.jpg"], + tags: [{ id: 2, name: "indoor" }], + status: "pending", + }, + { + id: 8, + name: "Charlie", + category: { id: 1, name: "Dogs" }, + photoUrls: ["https://example.com/charlie.jpg"], + tags: [{ id: 3, name: "trained" }], + status: "sold", + }, + ], + users: [ + { + id: 1, + username: "user1", + firstName: "John", + lastName: "Doe", + email: "john@example.com", + password: "password123", + phone: "555-1234", + userStatus: 1, + }, + { + id: 2, + username: "jane_smith", + firstName: "Jane", + lastName: "Smith", + email: "jane@example.com", + password: "secret456", + phone: "555-5678", + userStatus: 1, + }, + { + id: 3, + username: "bob_jones", + firstName: "Bob", + lastName: "Jones", + email: "bob@example.com", + password: "pass789", + phone: "555-9012", + userStatus: 1, + }, + ], + orders: [ + { + id: 1, + petId: 6, + quantity: 1, + shipDate: "2024-01-15T10:00:00Z", + status: "placed", + complete: false, + }, + { + id: 2, + petId: 7, + quantity: 1, + shipDate: "2024-01-16T11:00:00Z", + status: "approved", + complete: false, + }, + { + id: 3, + petId: 8, + quantity: 1, + shipDate: "2024-01-10T08:00:00Z", + status: "delivered", + complete: true, + }, + { + id: 4, + petId: 1, + quantity: 2, + shipDate: "2024-01-17T09:00:00Z", + status: "placed", + complete: false, + }, + ], + }, +}; + export class Context { - pets: Pet[] = [ - { - id: 1, - name: "Buddy", - category: { id: 1, name: "Dogs" }, - photoUrls: ["https://example.com/buddy.jpg"], - tags: [{ id: 1, name: "friendly" }], - status: "available", - }, - { - id: 2, - name: "Whiskers", - category: { id: 2, name: "Cats" }, - photoUrls: ["https://example.com/whiskers.jpg"], - tags: [{ id: 2, name: "indoor" }], - status: "available", - }, - { - id: 3, - name: "Goldie", - category: { id: 3, name: "Fish" }, - photoUrls: ["https://example.com/goldie.jpg"], - tags: [], - status: "pending", - }, - { - id: 4, - name: "Max", - category: { id: 1, name: "Dogs" }, - photoUrls: ["https://example.com/max.jpg"], - tags: [{ id: 3, name: "trained" }], - status: "sold", - }, - ]; - - users: User[] = [ - { - id: 1, - username: "user1", - firstName: "John", - lastName: "Doe", - email: "john@example.com", - password: "password123", - phone: "555-1234", - userStatus: 1, - }, - { - id: 2, - username: "jane_smith", - firstName: "Jane", - lastName: "Smith", - email: "jane@example.com", - password: "secret456", - phone: "555-5678", - userStatus: 1, - }, - ]; - - orders: Order[] = [ - { - id: 1, - petId: 1, - quantity: 1, - shipDate: "2024-01-15T10:00:00Z", - status: "placed", - complete: false, - }, - { - id: 2, - petId: 4, - quantity: 1, - shipDate: "2024-01-10T08:00:00Z", - status: "delivered", - complete: true, - }, - ]; - - private nextPetId = 5; - private nextUserId = 3; - private nextOrderId = 3; + pets: Pet[] = []; + users: User[] = []; + orders: Order[] = []; + + private nextPetId = 1; + private nextUserId = 1; + private nextOrderId = 1; + + constructor() { + this.loadScenario("startup"); + } + + /** + * Load a named scenario, replacing all current pets, users, and orders. + * + * Available scenarios: startup, empty, sold-out, busy + * + * Example (from the REPL): + * context.loadScenario("empty") + */ + loadScenario(name: string): void { + const scenario = SCENARIOS[name]; + if (!scenario) { + throw new Error( + `Scenario "${name}" not found. Available: ${Object.keys(SCENARIOS).join(", ")}`, + ); + } + this.pets = scenario.pets.map((p) => ({ ...p })); + this.users = scenario.users.map((u) => ({ ...u })); + this.orders = scenario.orders.map((o) => ({ ...o })); + this.nextPetId = Math.max(0, ...this.pets.map((p) => p.id ?? 0)) + 1; + this.nextUserId = Math.max(0, ...this.users.map((u) => u.id ?? 0)) + 1; + this.nextOrderId = + Math.max(0, ...this.orders.map((o) => o.id ?? 0)) + 1; + } + + /** Returns the names of all available scenarios. */ + get scenarios(): string[] { + return Object.keys(SCENARIOS); + } addPet(pet: Pet): Pet { const newPet = { ...pet, id: this.nextPetId++ }; diff --git a/test/context.test.ts b/test/context.test.ts index 4fd0177..eabd582 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -252,4 +252,107 @@ describe("Context", () => { assert.equal(inventory.available, 1); }); }); + + describe("scenarios", () => { + it("lists available scenario names", () => { + assert.ok(context.scenarios.includes("startup")); + assert.ok(context.scenarios.includes("empty")); + assert.ok(context.scenarios.includes("sold-out")); + assert.ok(context.scenarios.includes("busy")); + }); + }); + + describe("loadScenario", () => { + it("loads the startup scenario with four pets", () => { + context.loadScenario("startup"); + assert.equal(context.pets.length, 4); + }); + + it("loads the startup scenario with two users", () => { + context.loadScenario("startup"); + assert.equal(context.users.length, 2); + }); + + it("loads the startup scenario with two orders", () => { + context.loadScenario("startup"); + assert.equal(context.orders.length, 2); + }); + + it("loads the empty scenario with no pets", () => { + context.loadScenario("empty"); + assert.equal(context.pets.length, 0); + }); + + it("loads the empty scenario with no users", () => { + context.loadScenario("empty"); + assert.equal(context.users.length, 0); + }); + + it("loads the empty scenario with no orders", () => { + context.loadScenario("empty"); + assert.equal(context.orders.length, 0); + }); + + it("assigns a new id after loading the empty scenario", () => { + context.loadScenario("empty"); + const pet = context.addPet({ name: "Rex", photoUrls: [] }); + assert.ok(typeof pet.id === "number"); + }); + + it("loads the sold-out scenario where all pets are sold", () => { + context.loadScenario("sold-out"); + assert.ok(context.pets.every((p) => p.status === "sold")); + }); + + it("loads the sold-out scenario where all orders are complete", () => { + context.loadScenario("sold-out"); + assert.ok(context.orders.every((o) => o.complete === true)); + }); + + it("loads the busy scenario with more than four pets", () => { + context.loadScenario("busy"); + assert.ok(context.pets.length > 4); + }); + + it("loads the busy scenario with more than two users", () => { + context.loadScenario("busy"); + assert.ok(context.users.length > 2); + }); + + it("loads the busy scenario with more than two orders", () => { + context.loadScenario("busy"); + assert.ok(context.orders.length > 2); + }); + + it("replaces existing data when switching scenarios", () => { + context.addPet({ name: "Extra", photoUrls: [] }); + context.loadScenario("empty"); + assert.equal(context.pets.length, 0); + }); + + it("does not share pet array references between loads", () => { + context.loadScenario("startup"); + context.pets.push({ name: "Extra", photoUrls: [] }); + context.loadScenario("startup"); + assert.equal(context.pets.length, 4); + }); + + it("throws an error for an unknown scenario name", () => { + assert.throws(() => context.loadScenario("nonexistent"), /nonexistent/); + }); + + it("error message lists available scenarios", () => { + assert.throws( + () => context.loadScenario("nonexistent"), + /startup.*empty|empty.*startup/, + ); + }); + + it("assigns sequential ids after loading a non-empty scenario", () => { + context.loadScenario("startup"); + const first = context.addPet({ name: "A", photoUrls: [] }); + const second = context.addPet({ name: "B", photoUrls: [] }); + assert.equal((second.id as number) - (first.id as number), 1); + }); + }); });