Fix error on match profile submit when avoiding a mode that previously had pool

This commit is contained in:
Kalle
2026-06-02 08:17:47 +03:00
parent d278a15504
commit 1bae38522e
4 changed files with 147 additions and 14 deletions

View File

@@ -89,11 +89,7 @@ function MapModePreferencesField({
if (preference !== "NEUTRAL") {
newModePreferences.push({ mode, preference });
}
const newPool =
preference === "AVOID"
? value.pool.filter((p) => p.mode !== mode)
: value.pool;
onChange({ modes: newModePreferences, pool: newPool });
onChange({ modes: newModePreferences, pool: value.pool });
};
const handlePoolChange = (mode: ModeShort, stages: StageId[]) => {

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { mapModePreferencesValueSchema } from "./match-profile-schemas";
describe("mapModePreferencesValueSchema", () => {
it("strips pools for avoided modes", () => {
const result = mapModePreferencesValueSchema.parse({
modes: [
{ mode: "SZ", preference: "PREFER" },
{ mode: "TC", preference: "AVOID" },
],
pool: [
{ mode: "SZ", stages: [1, 2] },
{ mode: "TC", stages: [3, 4] },
],
});
expect(result.pool).toEqual([{ mode: "SZ", stages: [1, 2] }]);
});
it("keeps pools for preferred and neutral modes", () => {
const result = mapModePreferencesValueSchema.parse({
modes: [{ mode: "SZ", preference: "PREFER" }],
pool: [
{ mode: "SZ", stages: [1] },
{ mode: "TC", stages: [2] },
],
});
expect(result.pool).toEqual([
{ mode: "SZ", stages: [1] },
{ mode: "TC", stages: [2] },
]);
});
it("does not mutate the modes selection", () => {
const result = mapModePreferencesValueSchema.parse({
modes: [{ mode: "TC", preference: "AVOID" }],
pool: [{ mode: "TC", stages: [1] }],
});
expect(result.modes).toEqual([{ mode: "TC", preference: "AVOID" }]);
expect(result.pool).toEqual([]);
});
});

View File

@@ -22,7 +22,7 @@ export const LANGUAGE_OPTIONS = languagesUnified.map((lang) => ({
const preferenceSchema = z.enum(["AVOID", "PREFER"]).optional();
const mapModePreferencesValueSchema = z
export const mapModePreferencesValueSchema = z
.object({
modes: z.array(z.object({ mode: modeShort, preference: preferenceSchema })),
pool: z.array(
@@ -32,14 +32,16 @@ const mapModePreferencesValueSchema = z
}),
),
})
.refine(
(val) =>
val.pool.every((pool) => {
const mp = val.modes.find((m) => m.mode === pool.mode);
return mp?.preference !== "AVOID";
}),
"Can't have map pool for a mode that was avoided",
);
// Pools for avoided modes are kept in the client form state so they can be
// restored if the user later un-avoids the mode, but they must not be
// persisted as active pools. Strip them out before the value reaches the action.
.transform((val) => ({
...val,
pool: val.pool.filter((pool) => {
const mp = val.modes.find((m) => m.mode === pool.mode);
return mp?.preference !== "AVOID";
}),
}));
export const updateMatchProfileSchema = z.object({
_action: stringConstant("UPDATE_MATCH_PROFILE"),

View File

@@ -16,6 +16,7 @@ import {
isNotVisible,
navigate,
seed,
submit,
test,
waitForPOSTResponse,
} from "./helpers/playwright";
@@ -94,6 +95,96 @@ test.describe("Settings", () => {
});
});
const AVOIDED_MODE_POOL_ERROR =
"Can't have map pool for a mode that was avoided";
const setModePreference = (
page: Page,
mode: string,
preference: "Avoid" | "Neutral" | "Prefer",
) => {
const name =
preference === "Neutral"
? "Neutral towards the mode"
: `${preference} the mode`;
return page
.getByRole("radiogroup", { name: `Select preference towards ${mode}` })
.getByRole("radio", { name })
.click({ force: true });
};
const mapButton = (page: Page, mode: string, stageId: number) =>
page.getByTestId(`map-pool-${mode}-${stageId}`);
const SELECTED_MAP_CLASS = /mapButtonGreyedOut/;
// The seeded user already has random map pools, so empty the mode's pool to get
// a known starting state. Only currently selected (greyed, non-banned) stages
// are clickable to deselect.
const clearMapPool = async (page: Page, mode: string) => {
const picked = page.locator(
`[data-testid^="map-pool-${mode}-"][class*="mapButtonGreyedOut"]:not([disabled])`,
);
for (let count = await picked.count(); count > 0; count--) {
// the selected-state check icon overlays the button and intercepts clicks
await picked.first().click({ force: true });
await expect(picked).toHaveCount(count - 1);
}
};
test.describe("Match profile map preferences", () => {
test("retains map selection when toggling a mode prefer -> avoid -> prefer", async ({
page,
}) => {
await seed(page);
await impersonate(page);
await navigate({ page, url: SETTINGS_PAGE });
await setModePreference(page, "SZ", "Prefer");
await clearMapPool(page, "SZ");
await mapButton(page, "SZ", 1).click();
await expect(mapButton(page, "SZ", 1)).toHaveClass(SELECTED_MAP_CLASS);
// avoiding hides the picker, but the selection should be remembered
await setModePreference(page, "SZ", "Avoid");
await isNotVisible(mapButton(page, "SZ", 1));
await setModePreference(page, "SZ", "Prefer");
await expect(mapButton(page, "SZ", 1)).toHaveClass(SELECTED_MAP_CLASS);
});
test("can save 'zones only' after a now-avoided mode previously had a map pool", async ({
page,
}) => {
await seed(page);
await impersonate(page);
await navigate({ page, url: SETTINGS_PAGE });
// Save a map pool for both SZ and TC (stage 2 is not banned in TC).
await setModePreference(page, "SZ", "Prefer");
await clearMapPool(page, "SZ");
await mapButton(page, "SZ", 1).click();
await setModePreference(page, "TC", "Prefer");
await clearMapPool(page, "TC");
await mapButton(page, "TC", 2).click();
await submit(page);
// Switch to "zones only" by avoiding every mode except SZ, then save.
await setModePreference(page, "TW", "Avoid");
await setModePreference(page, "TC", "Avoid");
await setModePreference(page, "RM", "Avoid");
await setModePreference(page, "CB", "Avoid");
await submit(page);
// Reload so the form loads the persisted preferences. The previously saved
// TC pool must not resurface as an invalid "pool for an avoided mode".
await navigate({ page, url: SETTINGS_PAGE });
await submit(page);
await isNotVisible(page.getByText(AVOIDED_MODE_POOL_ERROR));
});
});
const enableSpoilerFreeMode = async (page: Page) => {
await navigate({ page, url: `${SETTINGS_PAGE}?tab=preferences` });
const form = createFormHelpers(page, spoilerFreeModeSchema);