Skip to content
Merged
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
95 changes: 65 additions & 30 deletions frontend/__tests__/hooks/createEvent.spec.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,81 @@
import { createRoot } from "solid-js";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { createEvent } from "../../src/ts/hooks/createEvent";

describe("createEvent", () => {
it("initial value is 0", () => {
createRoot((dispose) => {
const [event] = createEvent();
expect(event()).toBe(0);
dispose();
});
it("dispatch notifies subscribers", () => {
const event = createEvent<string>();
const fn = vi.fn();
event.subscribe(fn);
event.dispatch("hello");
expect(fn).toHaveBeenCalledWith("hello");
});

it("dispatch increments the value by 1", () => {
createRoot((dispose) => {
const [event, dispatch] = createEvent();
dispatch();
expect(event()).toBe(1);
dispose();
});
it("dispatch notifies multiple subscribers", () => {
const event = createEvent<number>();
const fn1 = vi.fn();
const fn2 = vi.fn();
event.subscribe(fn1);
event.subscribe(fn2);
event.dispatch(42);
expect(fn1).toHaveBeenCalledWith(42);
expect(fn2).toHaveBeenCalledWith(42);
});

it("each dispatch increments independently", () => {
createRoot((dispose) => {
const [event, dispatch] = createEvent();
dispatch();
dispatch();
dispatch();
expect(event()).toBe(3);
dispose();
});
it("dispatch with no type arg requires no arguments", () => {
const event = createEvent();
const fn = vi.fn();
event.subscribe(fn);
event.dispatch();
expect(fn).toHaveBeenCalledTimes(1);
});

it("subscribe returns an unsubscribe function", () => {
const event = createEvent<string>();
const fn = vi.fn();
const unsub = event.subscribe(fn);
event.dispatch("a");
unsub();
event.dispatch("b");
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("a");
});

it("two independent events do not share state", () => {
const eventA = createEvent<string>();
const eventB = createEvent<string>();
const fnA = vi.fn();
const fnB = vi.fn();
eventA.subscribe(fnA);
eventB.subscribe(fnB);
eventA.dispatch("a");
expect(fnA).toHaveBeenCalledWith("a");
expect(fnB).not.toHaveBeenCalled();
});

it("useListener auto-unsubscribes on dispose", () => {
const event = createEvent<string>();
const fn = vi.fn();
createRoot((dispose) => {
const [eventA, dispatchA] = createEvent();
const [eventB, dispatchB] = createEvent();
dispatchA();
dispatchA();
dispatchB();
expect(eventA()).toBe(2);
expect(eventB()).toBe(1);
event.useListener(fn);
event.dispatch("inside");
dispose();
});
event.dispatch("outside");
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("inside");
});

it("subscriber errors do not prevent other subscribers from running", () => {
const event = createEvent<string>();
const fn1 = vi.fn(() => {
throw new Error("oops");
});
const fn2 = vi.fn();
event.subscribe(fn1);
event.subscribe(fn2);
event.dispatch("test");
expect(fn1).toHaveBeenCalled();
expect(fn2).toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions frontend/__tests__/root/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@monkeytype/schemas/configs";
import * as FunboxValidation from "../../src/ts/config/funbox-validation";
import * as ConfigValidation from "../../src/ts/config/validation";
import * as ConfigEvent from "../../src/ts/observables/config-event";
import { configEvent } from "../../src/ts/events/config";
Comment thread
Miodec marked this conversation as resolved.
import * as ApeConfig from "../../src/ts/ape/config";
import * as Notifications from "../../src/ts/states/notifications";
const { replaceConfig, getConfig } = __testing;
Expand All @@ -33,7 +33,7 @@ describe("Config", () => {
ConfigValidation,
"isConfigValueValid",
);
const dispatchConfigEventMock = vi.spyOn(ConfigEvent, "dispatch");
const dispatchConfigEventMock = vi.spyOn(configEvent, "dispatch");
const dbSaveConfigMock = vi.spyOn(ApeConfig, "saveConfig");
const notificationAddMock = vi.spyOn(
Notifications,
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/ts/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Ape from "./ape";
import { showRegisterCaptchaModal } from "./components/modals/RegisterCaptchaModal";
import { updateFromServer as updateConfigFromServer } from "./config/remote";
import * as DB from "./db";
import { authEvent } from "./events/auth";
import {
isAuthAvailable,
getAuthenticatedUser,
Expand All @@ -23,7 +24,6 @@ import {
resetIgnoreAuthCallback,
} from "./firebase";
import { showPopup } from "./modals/simple-modals-base";
import * as AuthEvent from "./observables/auth-event";
import * as Sentry from "./sentry";
import { addBanner } from "./states/banners";
import { showLoaderBar, hideLoaderBar } from "./states/loader-bar";
Expand Down Expand Up @@ -131,7 +131,7 @@ export async function loadUser(_user: UserType): Promise<void> {
signOut();
return;
}
AuthEvent.dispatch({ type: "snapshotUpdated", data: { isInitial: true } });
authEvent.dispatch({ type: "snapshotUpdated", data: { isInitial: true } });
}

export async function onAuthStateChanged(
Expand All @@ -155,7 +155,7 @@ export async function onAuthStateChanged(
void Sentry.clearUser();
}

AuthEvent.dispatch({
authEvent.dispatch({
type: "authStateChanged",
data: { isUserSignedIn: user !== null, loadPromise: userPromise },
});
Expand Down Expand Up @@ -231,7 +231,7 @@ async function addAuthProvider(
await linkWithPopup(user, provider);
hideLoaderBar();
showSuccessNotification(`${providerName} authentication added`);
AuthEvent.dispatch({ type: "authConfigUpdated" });
authEvent.dispatch({ type: "authConfigUpdated" });
} catch (error) {
hideLoaderBar();
showErrorNotification(`Failed to add ${providerName} authentication`, {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/commandline/lists/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as ThemeController from "../../controllers/theme-controller";
import { Command, CommandsSubgroup } from "../types";
import { ThemesList, ThemeWithName } from "../../constants/themes";
import { not } from "@monkeytype/util/predicates";
import * as ConfigEvent from "../../observables/config-event";
import { configEvent } from "../../events/config";
import * as getErrorMessage from "../../utils/error";

const isFavorite = (theme: ThemeWithName): boolean =>
Expand Down Expand Up @@ -77,7 +77,7 @@ export function update(themes: ThemeWithName[]): void {
}

// subscribe to theme-related config events to update the theme command list
ConfigEvent.subscribe(({ key }) => {
configEvent.subscribe(({ key }) => {
if (key === "favThemes") {
// update themes list when favorites change
try {
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/ts/components/layout/header/AccountXpBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { XpBreakdown } from "@monkeytype/schemas/results";
import { isSafeNumber } from "@monkeytype/util/numbers";
import {
createMemo,
createSignal,
For,
JSXElement,
Expand All @@ -14,7 +13,7 @@ import { createSignalWithSetters } from "../../../hooks/createSignalWithSetters"
import { createEffectOn } from "../../../hooks/effects";
import { getFocus } from "../../../states/core";
import {
getSkipBreakdownEvent,
skipBreakdownEvent,
getXpBarData,
setAnimatedLevel,
} from "../../../states/header";
Expand All @@ -38,11 +37,11 @@ export function AccountXpBar(): JSXElement {
const [getBarAnimationDuration, setBarAnimationDuration] = createSignal(0);
const [getBarAnimationEase, setBarAnimationEase] = createSignal("out(5)");

const [getAnimationEvent, fireAnimationEvent] = createEvent();
const animationEvent = createEvent();
const [getTotal, { setTotal }] = createSignalWithSetters(0)({
setTotal: (set, value: number) => {
set(value);
fireAnimationEvent();
animationEvent.dispatch();
},
});

Expand All @@ -54,23 +53,29 @@ export function AccountXpBar(): JSXElement {
let skipped = false;
let runId = 0;

const flashAnimation = createMemo(() => {
getAnimationEvent(); // trigger on every total update, even if value unchanged
const [flashAnimation, setFlashAnimation] = createSignal({
scale: [1, 1],
rotate: [0, 0],
duration: 2000,
ease: "out(5)",
});

animationEvent.useListener(() => {
const rand = (Math.random() * 2 - 1) / 4;
const rand2 = (Math.random() + 1) / 2;
return {
setFlashAnimation({
scale: [1 + 0.5 * rand2, 1],
rotate: [10 * rand, 0],
duration: 2000,
ease: "out(5)",
};
});
});

const addItem = (label: string, amount: number | string): void => {
setBreakdownItems((items) => [...items, { label, amount }]);
};

createEffectOn(getSkipBreakdownEvent, async () => {
skipBreakdownEvent.useListener(async () => {
if (skipped || !canSkip) return;

const myId = runId; // capture before first await
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/components/layout/header/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { JSXElement } from "solid-js";

import {
dispatchRestartTest,
restartTestEvent,
getActivePage,
getFocus,
} from "../../../states/core";
Expand All @@ -21,7 +21,7 @@ export function Logo(): JSXElement {
}}
data-ui-element="logo"
onClick={() => {
if (getActivePage() === "test") dispatchRestartTest();
if (getActivePage() === "test") restartTestEvent.dispatch();
}}
>
<svg
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/components/layout/header/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createMemo, JSXElement, Show } from "solid-js";
import { createEffectOn } from "../../../hooks/effects";
import { getServerConfigurationQueryOptions } from "../../../queries/server-configuration";
import {
dispatchRestartTest,
restartTestEvent,
getActivePage,
getFocus,
} from "../../../states/core";
Expand Down Expand Up @@ -80,7 +80,7 @@ export function Nav(): JSXElement {
"data-nav-item": "test",
}}
onClick={() => {
if (getActivePage() === "test") dispatchRestartTest();
if (getActivePage() === "test") restartTestEvent.dispatch();
}}
/>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { UserNameSchema } from "@monkeytype/schemas/users";
import { createForm } from "@tanstack/solid-form";
import { createEffect, createSignal, JSXElement, Show } from "solid-js";

import { navigationEvent } from "../../../events/navigation";
import { useRefWithUtils } from "../../../hooks/useRefWithUtils";
import * as NavigationEvent from "../../../observables/navigation-event";
import { queryClient } from "../../../queries";
import { getUserProfile } from "../../../queries/profile";
import { getActivePage } from "../../../states/core";
Expand All @@ -27,7 +27,10 @@ export function ProfileSearchPage(): JSXElement {
onSubmit: async ({ value }) => {
setEditable(false);
try {
NavigationEvent.dispatch(`/profile/${value.username}`, {});
navigationEvent.dispatch({
url: `/profile/${value.username}`,
options: {},
});
} finally {
setEditable(true);
}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/ts/config/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "./persistence";
import { Config, setConfigStore } from "./store";
import { getDefaultConfig } from "../constants/default-config";
import * as ConfigEvent from "../observables/config-event";
import { configEvent } from "../events/config";
import { migrateConfig } from "./utils";
import { promiseWithResolvers, typedKeys } from "../utils/misc";
import { setConfig } from "./setters";
Expand Down Expand Up @@ -76,7 +76,7 @@ export async function applyConfig(
//migrate old values if needed, remove additional keys and merge with default config
const fullConfig: ConfigSchemas.Config = migrateConfig(partialConfig);

ConfigEvent.dispatch({ key: "fullConfigChange" });
configEvent.dispatch({ key: "fullConfigChange" });

const defaultConfig = getDefaultConfig();
for (const key of typedKeys(fullConfig)) {
Expand Down Expand Up @@ -107,7 +107,7 @@ export async function applyConfig(
saveToLocalStorage(key);
}

ConfigEvent.dispatch({ key: "fullConfigChangeFinished" });
configEvent.dispatch({ key: "fullConfigChangeFinished" });
setConfigStore(reconcile(Config));
}

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/ts/config/setters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ZodType as ZodSchema } from "zod";
import { saveToLocalStorage } from "../config/persistence";
import { configMetadata, ConfigMetadataObject } from "./metadata";
import { isConfigValueValid } from "./validation";
import * as ConfigEvent from "../observables/config-event";
import { configEvent } from "../events/config";
import { showNoticeNotification } from "../states/notifications";
import {
canSetConfigWithCurrentFunboxes,
Expand Down Expand Up @@ -124,7 +124,7 @@ export function setConfig<T extends keyof ConfigSchemas.Config>(
if (!options?.nosave) saveToLocalStorage(key, options?.nosave);

// @ts-expect-error i can't figure this out
ConfigEvent.dispatch({
configEvent.dispatch({
key: key,
newValue: value,
nosave: options?.nosave ?? false,
Expand Down Expand Up @@ -195,7 +195,7 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean {

Config.funbox = newConfig;
saveToLocalStorage("funbox", nosave);
ConfigEvent.dispatch({
configEvent.dispatch({
key: "funbox",
newValue: Config.funbox,
nosave,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/controllers/ad-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* oxlint-disable no-unsafe-member-access */
import { debounce } from "throttle-debounce";
import * as ConfigEvent from "../observables/config-event";
import { configEvent } from "../events/config";
import { Config } from "../config/store";
import * as TestState from "../test/test-state";
import * as EG from "./eg-ad-controller";
Expand Down Expand Up @@ -286,7 +286,7 @@ window.addEventListener("resize", () => {
debouncedBreakpoint2Update();
});

ConfigEvent.subscribe(({ key, newValue }) => {
configEvent.subscribe(({ key, newValue }) => {
if (key === "ads") {
if (newValue === "off") {
removeAll();
Expand Down
Loading
Loading