Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/domain/cart/policy/__spec__/ShippingPolicy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@ import { describe, expect, it } from "vitest";
import { Money } from "@/domain/cart/value-objects/Money";
import { ShippingPolicy } from "../ShippingPolicy";

const threshold = ShippingPolicy.SHIPPING_THRESHOLD.toAmount();
const cost = ShippingPolicy.SHIPPING_COST.toAmount();

describe("ShippingPolicy", () => {
it("should return 0 shipping if subtotal is equal to or above the threshold", () => {
const subtotalAtThreshold = new Money(50);
const subtotalAboveThreshold = new Money(100);
const subtotalAtThreshold = new Money(threshold);
const subtotalAboveThreshold = new Money(threshold + 100);

expect(ShippingPolicy.calculate(subtotalAtThreshold).toAmount()).toBe(0);
expect(ShippingPolicy.calculate(subtotalAboveThreshold).toAmount()).toBe(0);
});

it("should return the shipping cost if subtotal is below the threshold", () => {
const subtotalBelowThreshold = new Money(25);
const subtotalBelowThreshold = new Money(threshold - 100);
const shippingCost = ShippingPolicy.calculate(subtotalBelowThreshold);

expect(shippingCost.toAmount()).toBe(5.99);
expect(shippingCost.toAmount()).toBe(cost);
});

it("should correctly handle edge case just below threshold", () => {
const subtotalJustBelow = new Money(49.99);
const subtotalJustBelow = new Money(threshold - 0.01);
const shippingCost = ShippingPolicy.calculate(subtotalJustBelow);
expect(shippingCost.toAmount()).toBe(5.99);
expect(shippingCost.toAmount()).toBe(cost);
});

it("should correctly handle edge case just above threshold", () => {
const subtotalJustAbove = new Money(50.01);
const subtotalJustAbove = new Money(threshold + 0.01);
const shippingCost = ShippingPolicy.calculate(subtotalJustAbove);
expect(shippingCost.toAmount()).toBe(0);
});
Expand Down
4 changes: 0 additions & 4 deletions src/domain/cart/value-objects/Product/Product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,10 @@ export class Product {
}

getLoreLink(level: KnowledgeProfile): string {
if (level === "beginner") return this._knowledge.getProfileLoreLink(level);
if (level === "adventurer") return this._knowledge.getProfileLoreLink(level);
return this._knowledge.getProfileLoreLink(level);
}

getVideoUrl(level: KnowledgeProfile): string {
if (level === "beginner") return this._knowledge.getProfileVideoUrl(level);
if (level === "adventurer") return this._knowledge.getProfileVideoUrl(level);
return this._knowledge.getProfileVideoUrl(level);
}
}
54 changes: 30 additions & 24 deletions src/domain/cart/value-objects/Product/__spec__/Product.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,27 @@ describe("Product Value Objects", () => {
});

describe("KnowledgeContent and Product", () => {
const stats = new Stats(10, 20, 5);
const effects = [new Effect("Fire damage")];
const extraResources = new ExtraResources("https://lore.link", "https://video.link");
const profile = new ProfileDetails("A legendary sword", stats, effects, extraResources);
const statsByLevel = {
beginner: { power: 1, durability: 2, manaBoost: 3 },
adventurer: { power: 4, durability: 5, manaBoost: 6 },
expert: { power: 7, durability: 8, manaBoost: 9 },
} as const;

const knowledge = new KnowledgeContent(profile, profile, profile);
const profileFor = (level: "beginner" | "adventurer" | "expert") => {
const { power, durability, manaBoost } = statsByLevel[level];
return new ProfileDetails(
`${level} tale`,
new Stats(power, durability, manaBoost),
[new Effect(`${level} effect`)],
new ExtraResources(`https://lore.link/${level}`, `https://video.link/${level}`),
);
};

const knowledge = new KnowledgeContent(
profileFor("beginner"),
profileFor("adventurer"),
profileFor("expert"),
);

const product = new Product(
new ProductId("sword01"),
Expand All @@ -46,23 +61,14 @@ describe("KnowledgeContent and Product", () => {
expect(product.displayImage()).toBe("http://image.link");
});

it("should return profile description, effects, stats, lore link, and video URL", () => {
const levels: ("beginner" | "adventurer" | "expert")[] = [
"beginner",
"adventurer",
"expert",
];

for (const level of levels) {
expect(product.describeProfile(level)).toBe("A legendary sword");
expect(product.listProfileEffects(level)).toEqual(["Fire damage"]);
expect(product.getProfileStats(level)).toEqual({
power: 10,
durability: 20,
manaBoost: 5,
});
expect(product.getLoreLink(level)).toBe("https://lore.link");
expect(product.getVideoUrl(level)).toBe("https://video.link");
}
});
it.each(["beginner", "adventurer", "expert"] as const)(
"should route %s to its own profile's description, effects, stats, lore link, and video URL",
(level) => {
expect(product.describeProfile(level)).toBe(`${level} tale`);
expect(product.listProfileEffects(level)).toEqual([`${level} effect`]);
expect(product.getProfileStats(level)).toEqual(statsByLevel[level]);
expect(product.getLoreLink(level)).toBe(`https://lore.link/${level}`);
expect(product.getVideoUrl(level)).toBe(`https://video.link/${level}`);
},
);
});
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {

export class LocalStorageWelcomeSurveyRepository implements WelcomeStorageRepository {
private listeners = new Set<Listener>();
private cache: WelcomeSurveyStoreState = { survey: null };
private cache: WelcomeSurveyStoreState | undefined;

getSurvey = (): WelcomeSurveyStoreState["survey"] => {
if (this.cache !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import {
type WelcomeSurveyStoreState,
} from "@/domain/welcomeSurvey/WelcomeSurveyStore.config";

// This module runs at import time both in the browser and, once the app is prerendered
// (see the SEO PR later in the stack), under Node during a server-side render. Node has
// no localStorage, so every access is guarded rather than assumed.
const isBrowser = typeof localStorage !== "undefined";

const loadInitialState = (): WelcomeSurveyStoreState => {
if (!isBrowser) return { survey: null };

try {
const stored = localStorage.getItem(WELCOME_SURVEY_KEY);

Expand All @@ -22,6 +29,8 @@ const loadInitialState = (): WelcomeSurveyStoreState => {
const welcomeStore = new Store<WelcomeSurveyStoreState>(loadInitialState());

welcomeStore.subscribe(() => {
if (!isBrowser) return;

try {
localStorage.setItem(WELCOME_SURVEY_KEY, JSON.stringify(welcomeStore.state));
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/** @vitest-environment jsdom */
import { beforeEach, describe, expect, it, vi } from "vitest";
import { WELCOME_SURVEY_KEY } from "@/domain/welcomeSurvey/WelcomeSurveyStore.config";
import { LocalStorageWelcomeSurveyRepository } from "../LocalStorageWelcomeSurvey.repository";

describe("LocalStorageWelcomeSurveyRepository", () => {
beforeEach(() => {
localStorage.clear();
});

it("should return null when nothing has been saved yet", () => {
const repository = new LocalStorageWelcomeSurveyRepository();

expect(repository.getSurvey()).toBeNull();
});

it("should persist and read back a saved survey", () => {
const repository = new LocalStorageWelcomeSurveyRepository();

repository.saveSurvey({ skill: "expert" });

expect(repository.getSurvey()).toEqual({ skill: "expert" });
});

it("should read a survey that was written to storage by another instance", () => {
const writer = new LocalStorageWelcomeSurveyRepository();
writer.saveSurvey({ skill: "intermediate" });

const reader = new LocalStorageWelcomeSurveyRepository();

expect(reader.getSurvey()).toEqual({ skill: "intermediate" });
});

it("should fall back to null for malformed stored data", () => {
localStorage.setItem(WELCOME_SURVEY_KEY, "not json");
const repository = new LocalStorageWelcomeSurveyRepository();

expect(repository.getSurvey()).toBeNull();
});

it("should notify subscribers when a survey is saved", () => {
const repository = new LocalStorageWelcomeSurveyRepository();
const listener = vi.fn();
repository.subscribe(listener);

repository.saveSurvey({ skill: "beginner" });

expect(listener).toHaveBeenCalledTimes(1);
});

it("should stop notifying once unsubscribed", () => {
const repository = new LocalStorageWelcomeSurveyRepository();
const listener = vi.fn();
const unsubscribe = repository.subscribe(listener);
unsubscribe();

repository.saveSurvey({ skill: "beginner" });

expect(listener).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// This file has no jsdom environment override, so it runs in vitest's default node
// environment, which has no localStorage. That intentionally proves the module is safe
// to import and use during a server-side render (see the SEO PR later in the stack).
import { describe, expect, it, vi } from "vitest";
import { TanStackStoreWelcomeSurveyRepository } from "../TanstackStoreWelcomeSurvey.repository";

// TanStackStoreWelcomeSurveyRepository wraps one module-level store shared by every
// instance, so these tests read back whatever the previous one just wrote rather than
// assuming a clean starting state.
describe("TanStackStoreWelcomeSurveyRepository (no localStorage)", () => {
it("should be importable and constructible without a DOM", () => {
expect(() => new TanStackStoreWelcomeSurveyRepository()).not.toThrow();
});

it("should save and read back a survey in memory", () => {
const repository = new TanStackStoreWelcomeSurveyRepository();

repository.saveSurvey({ skill: "expert" });

expect(repository.getSurvey()).toEqual({ skill: "expert" });
});

it("should notify and allow unsubscribing", () => {
const repository = new TanStackStoreWelcomeSurveyRepository();
const listener = vi.fn();
const unsubscribe = repository.subscribe(listener);

repository.saveSurvey({ skill: "beginner" });
expect(listener).toHaveBeenCalledTimes(1);

unsubscribe();
repository.saveSurvey({ skill: "intermediate" });
expect(listener).toHaveBeenCalledTimes(1);
});
});
Loading