From 723c2f7767410c0728d193add16c1f76633f29b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Wed, 19 Mar 2025 11:07:27 -0400 Subject: [PATCH] unit test achievement manager --- .../achievements/achievement-manager.spec.ts | 152 ++++++++++++++++++ .../tools/achievements/achievement-manager.ts | 2 +- .../examples/example-validators.ts | 21 ++- 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/tools/achievements/achievement-manager.spec.ts diff --git a/libs/common/src/tools/achievements/achievement-manager.spec.ts b/libs/common/src/tools/achievements/achievement-manager.spec.ts new file mode 100644 index 00000000000..23540f62fa5 --- /dev/null +++ b/libs/common/src/tools/achievements/achievement-manager.spec.ts @@ -0,0 +1,152 @@ +import { BehaviorSubject, ReplaySubject, Subject, firstValueFrom } from "rxjs"; + +import { active } from "./achievement-manager"; +import { + ItemCreatedTracker, + TotallyAttachedValidator, + UnboundItemCreatedTracker, +} from "./examples/example-validators"; +import { AchievementId, AchievementValidator, MetricId } from "./types"; + +describe("active", () => { + it("passes through empty achievement sets", async () => { + const metrics$ = new BehaviorSubject>(new Map()); + const earned$ = new BehaviorSubject>(new Set()); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([]); + + await expect(results).resolves.toEqual([]); + }); + + it("passes through until-earned validators when earned$ omits the achievement id", async () => { + const metrics$ = new BehaviorSubject>(new Map()); + const earned$ = new BehaviorSubject>(new Set()); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([TotallyAttachedValidator]); + + await expect(results).resolves.toEqual([TotallyAttachedValidator]); + }); + + it("filters until-earned validators when earned$ includes the achievement id", async () => { + const metrics$ = new BehaviorSubject>(new Map()); + const earned$ = new BehaviorSubject>( + new Set([TotallyAttachedValidator.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([TotallyAttachedValidator]); + + await expect(results).resolves.toEqual([]); + }); + + it("passes through threshold validators when metric$ omits its metric and the low threshold isn't defined", async () => { + const metrics$ = new BehaviorSubject>(new Map()); + const earned$ = new BehaviorSubject>( + new Set([ItemCreatedTracker.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([ItemCreatedTracker]); + + await expect(results).resolves.toEqual([ItemCreatedTracker]); + }); + + it("passes through threshold validators when metric$ includes a metric below the validator's `high` threshold", async () => { + const metrics$ = new BehaviorSubject>( + new Map([[ItemCreatedTracker.active.metric, 0]]), + ); + const earned$ = new BehaviorSubject>( + new Set([ItemCreatedTracker.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([ItemCreatedTracker]); + + await expect(results).resolves.toEqual([ItemCreatedTracker]); + }); + + it("filters threshold validators when metric$ includes a metric equal to the validator's `high` threshold", async () => { + const metrics$ = new BehaviorSubject>( + new Map([[ItemCreatedTracker.active.metric, 1]]), + ); + const earned$ = new BehaviorSubject>( + new Set([ItemCreatedTracker.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([ItemCreatedTracker]); + + await expect(results).resolves.toEqual([]); + }); + + it("filters threshold validators when metric$ includes a metric greater than the validator's `high` threshold", async () => { + const metrics$ = new BehaviorSubject>( + new Map([[ItemCreatedTracker.active.metric, 2]]), + ); + const earned$ = new BehaviorSubject>( + new Set([ItemCreatedTracker.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([ItemCreatedTracker]); + + await expect(results).resolves.toEqual([]); + }); + + it("passes through threshold validators when metric$ includes a metric equal to the validator's `low` threshold", async () => { + const metrics$ = new BehaviorSubject>( + new Map([[UnboundItemCreatedTracker.active.metric, 2]]), + ); + const earned$ = new BehaviorSubject>( + new Set([UnboundItemCreatedTracker.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([UnboundItemCreatedTracker]); + + await expect(results).resolves.toEqual([UnboundItemCreatedTracker]); + }); + + it("filters threshold validators when metric$ includes a metric below to the validator's `low` threshold", async () => { + const metrics$ = new BehaviorSubject>( + new Map([[UnboundItemCreatedTracker.active.metric, 0]]), + ); + const earned$ = new BehaviorSubject>( + new Set([UnboundItemCreatedTracker.achievement]), + ); + const validators$ = new Subject(); + const results$ = new ReplaySubject(1); + + validators$.pipe(active(metrics$, earned$)).subscribe(results$); + const results = firstValueFrom(results$); + validators$.next([UnboundItemCreatedTracker]); + + await expect(results).resolves.toEqual([]); + }); +}); diff --git a/libs/common/src/tools/achievements/achievement-manager.ts b/libs/common/src/tools/achievements/achievement-manager.ts index b57a54e449c..afc44c8754f 100644 --- a/libs/common/src/tools/achievements/achievement-manager.ts +++ b/libs/common/src/tools/achievements/achievement-manager.ts @@ -25,7 +25,7 @@ function active( // monitor disabled if outside of threshold const progress = (m.active.metric && metrics.get(m.active.metric)) || 0; - if (progress > (m.active.high ?? Number.POSITIVE_INFINITY)) { + if (progress >= (m.active.high ?? Number.POSITIVE_INFINITY)) { return false; } else if (progress < (m.active.low ?? 0)) { return false; diff --git a/libs/common/src/tools/achievements/examples/example-validators.ts b/libs/common/src/tools/achievements/examples/example-validators.ts index a2ca1687646..34da205406f 100644 --- a/libs/common/src/tools/achievements/examples/example-validators.ts +++ b/libs/common/src/tools/achievements/examples/example-validators.ts @@ -7,6 +7,7 @@ const CredentialGeneratedProgress = "credential-generated" as MetricId; const TotallyAttachedAchievement = "totally-attached" as AchievementId; const ItemCreatedMetric = "item-created-metric" as AchievementId; +const UnboundItemCreatedMetric = "unbound-item-created-metric" as AchievementId; const ItemCreatedAchievement = "item-created" as AchievementId; const ThreeItemsCreatedAchievement = "three-vault-items-created" as AchievementId; const FiveItemsCreatedAchievement = "five-vault-items-created" as AchievementId; @@ -49,6 +50,22 @@ const ItemCreatedTracker = { }, } satisfies AchievementValidator; +const UnboundItemCreatedTracker = { + achievement: UnboundItemCreatedMetric, + name: `[TRACKER] ${ItemCreatedProgress}`, + description: `Measures ${ItemCreatedProgress}`, + validator: Type.Threshold, + active: { metric: ItemCreatedProgress, low: 1 }, + hidden: true, + trigger(item) { + return item.action === "vault-item-added"; + }, + measure(item, progress) { + const value = 1 + (progress.get(ItemCreatedProgress) ?? 0); + return [progressEvent(ItemCreatedProgress, value)]; + }, +} satisfies AchievementValidator; + const ItemCreatedValidator = { achievement: ItemCreatedAchievement, name: "What an item!", @@ -92,7 +109,7 @@ const FiveItemsCreatedValidator = { name: "fiiivvve GoOoOoOolllllllD RIIIIIINGS!!!!!!", description: "Add five items to your vault", validator: Type.Threshold, - active: { metric: ItemCreatedProgress, low: 4, high: 5 }, + active: { metric: ItemCreatedProgress, low: 3, high: 5 }, hidden: false, trigger(item) { return item.action === "vault-item-added"; @@ -112,6 +129,8 @@ export { TotallyAttachedValidator, ItemCreatedMetric, ItemCreatedTracker, + UnboundItemCreatedMetric, + UnboundItemCreatedTracker, ItemCreatedProgress, ItemCreatedAchievement, ItemCreatedValidator,