mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-08 20:26:08 -05:00
Undo requests when changing map prefrences / noScreen as full group member
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { db } from "~/db/sql";
|
||||
import type { UserMapModePreferences } from "~/db/tables";
|
||||
import { dbInsertUsers, dbReset, withUserId } from "~/utils/Test";
|
||||
import * as MatchProfileRepository from "./MatchProfileRepository.server";
|
||||
|
||||
const USER_ID = 1;
|
||||
|
||||
const PREFERENCES: UserMapModePreferences = {
|
||||
modes: [{ mode: "SZ", preference: "PREFER" }],
|
||||
pool: [{ mode: "SZ", stages: [1, 2, 3, 4] }],
|
||||
};
|
||||
|
||||
const OTHER_PREFERENCES: UserMapModePreferences = {
|
||||
modes: [{ mode: "SZ", preference: "PREFER" }],
|
||||
pool: [{ mode: "SZ", stages: [5, 6, 7, 8] }],
|
||||
};
|
||||
|
||||
const updateProfile = (
|
||||
args: Partial<
|
||||
Parameters<typeof MatchProfileRepository.updateOwnMatchProfile>[0]
|
||||
> = {},
|
||||
) =>
|
||||
withUserId(USER_ID, () =>
|
||||
MatchProfileRepository.updateOwnMatchProfile({
|
||||
mapModePreferences: PREFERENCES,
|
||||
vc: "NO",
|
||||
languages: [],
|
||||
weaponPool: [],
|
||||
noScreen: 0,
|
||||
...args,
|
||||
}),
|
||||
);
|
||||
|
||||
describe("updateOwnMatchProfile", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(1);
|
||||
await db
|
||||
.updateTable("User")
|
||||
.set({ mapModePreferences: JSON.stringify(PREFERENCES), noScreen: 0 })
|
||||
.where("id", "=", USER_ID)
|
||||
.execute();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("reports no change when nothing matchmaking-relevant changed", async () => {
|
||||
const result = await updateProfile({ vc: "YES", languages: ["en"] });
|
||||
|
||||
expect(result.mapModePreferencesChanged).toBe(false);
|
||||
expect(result.noScreenChanged).toBe(false);
|
||||
});
|
||||
|
||||
test("detects a noScreen change", async () => {
|
||||
const result = await updateProfile({ noScreen: 1 });
|
||||
|
||||
expect(result.noScreenChanged).toBe(true);
|
||||
expect(result.mapModePreferencesChanged).toBe(false);
|
||||
});
|
||||
|
||||
test("detects a map/mode preferences change", async () => {
|
||||
const result = await updateProfile({
|
||||
mapModePreferences: OTHER_PREFERENCES,
|
||||
});
|
||||
|
||||
expect(result.mapModePreferencesChanged).toBe(true);
|
||||
expect(result.noScreenChanged).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as R from "remeda";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables, UserMapModePreferences } from "~/db/tables";
|
||||
import { actorId } from "~/features/auth/core/user.server";
|
||||
@@ -56,20 +57,29 @@ export async function updateOwnMatchProfile({
|
||||
noScreen: number;
|
||||
}) {
|
||||
const userId = actorId();
|
||||
const currentPreferences = (
|
||||
await db
|
||||
.selectFrom("User")
|
||||
.select("mapModePreferences")
|
||||
.where("id", "=", userId)
|
||||
.executeTakeFirstOrThrow()
|
||||
).mapModePreferences;
|
||||
const current = await db
|
||||
.selectFrom("User")
|
||||
.select(["mapModePreferences", "noScreen"])
|
||||
.where("id", "=", userId)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const mergedPool = mergeExcludedModePreferences(
|
||||
mapModePreferences.pool,
|
||||
currentPreferences?.pool,
|
||||
current.mapModePreferences?.pool,
|
||||
);
|
||||
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const newMapModePreferences: UserMapModePreferences = {
|
||||
...mapModePreferences,
|
||||
pool: mergedPool,
|
||||
};
|
||||
|
||||
const mapModePreferencesChanged = !R.isDeepEqual(
|
||||
newMapModePreferences,
|
||||
current.mapModePreferences,
|
||||
);
|
||||
const noScreenChanged = current.noScreen !== noScreen;
|
||||
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx
|
||||
.deleteFrom("UserWeaponPool")
|
||||
.where("userId", "=", userId)
|
||||
@@ -92,10 +102,7 @@ export async function updateOwnMatchProfile({
|
||||
await trx
|
||||
.updateTable("User")
|
||||
.set({
|
||||
mapModePreferences: JSON.stringify({
|
||||
...mapModePreferences,
|
||||
pool: mergedPool,
|
||||
}),
|
||||
mapModePreferences: JSON.stringify(newMapModePreferences),
|
||||
vc,
|
||||
languages: languages.length > 0 ? languages.join(",") : null,
|
||||
noScreen,
|
||||
@@ -103,6 +110,8 @@ export async function updateOwnMatchProfile({
|
||||
.where("id", "=", userId)
|
||||
.execute();
|
||||
});
|
||||
|
||||
return { mapModePreferencesChanged, noScreenChanged };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -675,6 +675,11 @@ export function deleteLike({
|
||||
});
|
||||
}
|
||||
|
||||
/** Deletes every like where the given group is the liker or the target. */
|
||||
export function deleteAllLikesByGroupId(groupId: number) {
|
||||
return db.transaction().execute((trx) => deleteLikesByGroupId(groupId, trx));
|
||||
}
|
||||
|
||||
export function leaveGroup(userId: number) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const userGroup = await trx
|
||||
|
||||
44
app/features/sendouq/core/likes.server.ts
Normal file
44
app/features/sendouq/core/likes.server.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as R from "remeda";
|
||||
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
|
||||
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
|
||||
import {
|
||||
FULL_GROUP_SIZE,
|
||||
SENDOUQ_LOOKING_ROOM,
|
||||
sqGroupWebsocketRoom,
|
||||
} from "../q-constants";
|
||||
import { refreshSendouQInstance, SendouQ } from "./SendouQ.server";
|
||||
|
||||
/**
|
||||
* Cancels every pending challenge (both given and received) involving the user's
|
||||
* active and full SendouQ group. Only full groups are affected: partial groups
|
||||
* merge (rather than start a match) when a request is accepted, so their members'
|
||||
* preferences are not yet locked in.
|
||||
*/
|
||||
export async function cancelActiveGroupLikes(userId: number) {
|
||||
const ownGroup = SendouQ.findOwnGroup(userId);
|
||||
if (!ownGroup) return;
|
||||
if (ownGroup.status !== "ACTIVE" || ownGroup.matchId) return;
|
||||
if (ownGroup.members.length !== FULL_GROUP_SIZE) return;
|
||||
|
||||
const likes = await SQGroupRepository.allLikesByGroupId(ownGroup.id);
|
||||
const affectedGroupIds = R.unique([
|
||||
...likes.given.map((like) => like.groupId),
|
||||
...likes.received.map((like) => like.groupId),
|
||||
]);
|
||||
if (affectedGroupIds.length === 0) return;
|
||||
|
||||
await SQGroupRepository.deleteAllLikesByGroupId(ownGroup.id);
|
||||
|
||||
await refreshSendouQInstance();
|
||||
|
||||
ChatSystemMessage.send([
|
||||
...[...affectedGroupIds, ownGroup.id].map((groupId) => ({
|
||||
room: sqGroupWebsocketRoom(groupId),
|
||||
revalidateOnly: true,
|
||||
})),
|
||||
{
|
||||
room: SENDOUQ_LOOKING_ROOM,
|
||||
revalidateOnly: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ActionFunctionArgs } from "react-router";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server";
|
||||
import { cancelActiveGroupLikes } from "~/features/sendouq/core/likes.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { isSupporter } from "~/modules/permissions/utils";
|
||||
import { clampThemeToGamut } from "~/utils/oklch-gamut";
|
||||
@@ -59,13 +60,20 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
break;
|
||||
}
|
||||
case "UPDATE_MATCH_PROFILE": {
|
||||
await MatchProfileRepository.updateOwnMatchProfile({
|
||||
mapModePreferences: data.mapModePreferences,
|
||||
vc: data.vc,
|
||||
languages: data.languages,
|
||||
weaponPool: data.weaponPool,
|
||||
noScreen: Number(data.noScreen),
|
||||
});
|
||||
const { mapModePreferencesChanged, noScreenChanged } =
|
||||
await MatchProfileRepository.updateOwnMatchProfile({
|
||||
mapModePreferences: data.mapModePreferences,
|
||||
vc: data.vc,
|
||||
languages: data.languages,
|
||||
weaponPool: data.weaponPool,
|
||||
noScreen: Number(data.noScreen),
|
||||
});
|
||||
|
||||
// Challenges are made based on the modes/preferences shown at that
|
||||
// moment, so changing them must undo pending requests to/from the group.
|
||||
if (mapModePreferencesChanged || noScreenChanged) {
|
||||
await cancelActiveGroupLikes(user.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
SENDOUQ_LOOKING_PAGE,
|
||||
SENDOUQ_PAGE,
|
||||
SENDOUQ_PREPARING_PAGE,
|
||||
SETTINGS_PAGE,
|
||||
sendouQInviteLink,
|
||||
} from "~/utils/urls";
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
seed,
|
||||
submit,
|
||||
test,
|
||||
waitForPOSTResponse,
|
||||
} from "./helpers/playwright";
|
||||
|
||||
test.describe("SendouQ", () => {
|
||||
@@ -140,4 +142,32 @@ test.describe("SendouQ", () => {
|
||||
combinedGroup.getByTestId("sendouq-group-card-member"),
|
||||
).toHaveCount(2);
|
||||
});
|
||||
|
||||
test("Changing match preferences cancels pending requests", async ({
|
||||
page,
|
||||
}) => {
|
||||
await seed(page);
|
||||
|
||||
// Sendou (ADMIN) is in a full group. Challenge another full group.
|
||||
await impersonate(page, ADMIN_ID);
|
||||
await navigate({ page, url: SENDOUQ_LOOKING_PAGE });
|
||||
await waitForPOSTResponse(page, () =>
|
||||
page.getByRole("button", { name: "Challenge" }).first().click(),
|
||||
);
|
||||
|
||||
// The challenge is now pending and can be undone
|
||||
await expect(page.getByRole("button", { name: "Undo" })).toHaveCount(1);
|
||||
|
||||
// Changing a matchmaking preference (noScreen) last second must undo the
|
||||
// pending request so it can't be matched on terms the challenger never saw
|
||||
await navigate({ page, url: `${SETTINGS_PAGE}?tab=match-profile` });
|
||||
await page
|
||||
.getByRole("switch", { name: /Avoid Splattercolor Screen/i })
|
||||
.click({ force: true });
|
||||
await submit(page);
|
||||
|
||||
// The pending challenge has been undone
|
||||
await navigate({ page, url: SENDOUQ_LOOKING_PAGE });
|
||||
await expect(page.getByRole("button", { name: "Undo" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user