mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-13 22:56:05 -05:00
Fix error on match profile submit when avoiding a mode that previously had pool
This commit is contained in:
@@ -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[]) => {
|
||||
|
||||
44
app/features/settings/match-profile-schemas.test.ts
Normal file
44
app/features/settings/match-profile-schemas.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
@@ -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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user