feat(codes): redeemed and expired codes move below, and new codes are marked
A game's Codes tab lists the codes still to use first; a code redeemed on every server played, or past its stated expiry, is greyed in a "Redeemed or expired" section below. A date-only expiry passes once that date has ended everywhere. Settings → Your games marks a followed game with its count of unseen, still usable codes until its Codes tab is opened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3c81b9b0f1
commit
3b29010e7c
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { CodesFeed, RedeemCode } from "../src/shared/codes.ts";
|
||||
import {
|
||||
codeExpired,
|
||||
codeUsed,
|
||||
unseenUsableCodes,
|
||||
} from "../src/client/state/reader/codes.ts";
|
||||
|
||||
const code = (id: string, over: Partial<RedeemCode> = {}): RedeemCode => ({
|
||||
id,
|
||||
game: "genshin",
|
||||
code: id.toUpperCase(),
|
||||
reward: null,
|
||||
expiresAt: null,
|
||||
expiresOn: null,
|
||||
firstObservedAt: "2026-10-01T00:00:00.000Z",
|
||||
sourceUrl: "https://example.com/codes",
|
||||
...over,
|
||||
});
|
||||
const now = Date.parse("2026-10-18T12:00:00.000Z");
|
||||
|
||||
test("a code expires at its stated instant", () => {
|
||||
expect(
|
||||
codeExpired(code("a", { expiresAt: "2026-10-18T11:59:59.000Z" }), now),
|
||||
).toBe(true);
|
||||
expect(
|
||||
codeExpired(code("a", { expiresAt: "2026-10-18T12:00:01.000Z" }), now),
|
||||
).toBe(false);
|
||||
expect(codeExpired(code("a"), now)).toBe(false);
|
||||
});
|
||||
|
||||
test("a date-only expiry passes only once the date has ended everywhere", () => {
|
||||
// 17 Oct has ended everywhere by 18 Oct 12:00 UTC; 18 Oct has not.
|
||||
expect(codeExpired(code("a", { expiresOn: "2026-10-17" }), now)).toBe(true);
|
||||
expect(codeExpired(code("a", { expiresOn: "2026-10-18" }), now)).toBe(false);
|
||||
expect(codeExpired(code("a", { expiresOn: "2026-10-17" }), now - 1)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("a code is used once it is redeemed on every server played", () => {
|
||||
const redeemed = { "account:eu": { a: "2026-10-18T00:00:00.000Z" } };
|
||||
expect(codeUsed(code("a"), ["account:eu"], redeemed)).toBe(true);
|
||||
expect(codeUsed(code("a"), ["account:eu", "account:na"], redeemed)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(codeUsed(code("a"), [], redeemed)).toBe(false);
|
||||
});
|
||||
|
||||
test("the Settings marker counts unseen codes that can still be used", () => {
|
||||
const feed: CodesFeed = {
|
||||
schemaVersion: 1,
|
||||
sources: [],
|
||||
codes: [
|
||||
code("seen"),
|
||||
code("fresh"),
|
||||
code("expired", { expiresAt: "2026-10-01T00:00:00.000Z" }),
|
||||
code("used"),
|
||||
code("other", { game: "hsr" }),
|
||||
],
|
||||
};
|
||||
const ids = unseenUsableCodes(
|
||||
feed,
|
||||
["seen"],
|
||||
"genshin",
|
||||
["account:eu"],
|
||||
{ "account:eu": { used: "2026-10-18T00:00:00.000Z" } },
|
||||
now,
|
||||
).map((c) => c.id);
|
||||
expect(ids).toEqual(["fresh"]);
|
||||
});
|
||||
Reference in New Issue
Block a user