mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 21:55:15 -05:00
Override SendouQ rejoin yes vote when queueing elsewhere
A player who voted to continue with their group and then queued up elsewhere still showed as a yes to the rest. They would wait for a vote that was never coming, and once everyone else had voted the rejoin fell through and dumped them back to /q instead of giving them their group.
This commit is contained in:
@@ -10,14 +10,18 @@ import * as GroupMatchContinueVoteRepository from "~/features/sendouq-match/Grou
|
||||
import { FULL_GROUP_SIZE } from "./q-constants";
|
||||
import * as SQGroupRepository from "./SQGroupRepository.server";
|
||||
|
||||
const setupConcludedMatch = async () => {
|
||||
const users = await UserFactory.createMany(FULL_GROUP_SIZE * 2);
|
||||
const alphaMembers = users.slice(0, FULL_GROUP_SIZE);
|
||||
const setupConcludedMatch = async (
|
||||
/** Give the same members another match, for a user with a match history. */
|
||||
returningAlphaMembers?: Array<{ id: number }>,
|
||||
) => {
|
||||
const bravoMembers = await UserFactory.createMany(FULL_GROUP_SIZE);
|
||||
const alphaMembers =
|
||||
returningAlphaMembers ?? (await UserFactory.createMany(FULL_GROUP_SIZE));
|
||||
|
||||
const match = await SQMatchFactory.create(
|
||||
{
|
||||
alphaUserIds: alphaMembers.map((member) => member.id),
|
||||
bravoUserIds: users.slice(FULL_GROUP_SIZE).map((member) => member.id),
|
||||
bravoUserIds: bravoMembers.map((member) => member.id),
|
||||
isMatchmade: true,
|
||||
},
|
||||
{ isConcluded: true },
|
||||
@@ -57,8 +61,9 @@ describe("insert", () => {
|
||||
expect(result.chatCodeToRevalidate).toBe(matchChatCode);
|
||||
});
|
||||
|
||||
test("preserves existing vote when user already voted yes on previous match", async () => {
|
||||
const { alphaGroupId, alphaMembers } = await setupConcludedMatch();
|
||||
test("overrides the user's own yes vote on the previous match", async () => {
|
||||
const { alphaGroupId, alphaMembers, matchChatCode } =
|
||||
await setupConcludedMatch();
|
||||
|
||||
await castYesVote(alphaMembers[0].id, alphaGroupId);
|
||||
|
||||
@@ -67,10 +72,12 @@ describe("insert", () => {
|
||||
userId: alphaMembers[0].id,
|
||||
});
|
||||
|
||||
// leaving a yes vote standing would let the rest of the group reach a
|
||||
// unanimous vote for a group the user is no longer available for
|
||||
const votes = await fetchVotes(alphaGroupId);
|
||||
expect(votes).toHaveLength(1);
|
||||
expect(votes[0].isContinuing).toBe(true);
|
||||
expect(result.chatCodeToRevalidate).toBeNull();
|
||||
expect(votes[0].isContinuing).toBe(false);
|
||||
expect(result.chatCodeToRevalidate).toBe(matchChatCode);
|
||||
});
|
||||
|
||||
test("clears other members' yes votes on the previous group when recording implicit no", async () => {
|
||||
@@ -92,6 +99,54 @@ describe("insert", () => {
|
||||
expect(votes[0].isContinuing).toBe(false);
|
||||
});
|
||||
|
||||
test("records the implicit no-vote on the newest matchmade group of many", async () => {
|
||||
const { alphaGroupId: olderGroupId, alphaMembers } =
|
||||
await setupConcludedMatch();
|
||||
const { alphaGroupId: newerGroupId, matchChatCode } =
|
||||
await setupConcludedMatch(alphaMembers);
|
||||
const olderVotesBefore = await fetchVotes(olderGroupId);
|
||||
|
||||
const result = await SQGroupRepository.insert({
|
||||
status: "ACTIVE",
|
||||
userId: alphaMembers[0].id,
|
||||
});
|
||||
|
||||
expect(await fetchVotes(olderGroupId)).toEqual(olderVotesBefore);
|
||||
|
||||
const votes = await fetchVotes(newerGroupId);
|
||||
expect(votes).toHaveLength(1);
|
||||
expect(votes[0].userId).toBe(alphaMembers[0].id);
|
||||
expect(votes[0].isContinuing).toBe(false);
|
||||
expect(result.chatCodeToRevalidate).toBe(matchChatCode);
|
||||
});
|
||||
|
||||
test("leaves the previous group's votes alone on a later, unrelated queue action", async () => {
|
||||
const { alphaGroupId, alphaMembers } = await setupConcludedMatch();
|
||||
|
||||
await SQGroupRepository.insert({
|
||||
status: "ACTIVE",
|
||||
userId: alphaMembers[0].id,
|
||||
});
|
||||
|
||||
// the three who stayed settle the vote among themselves
|
||||
for (const member of alphaMembers.slice(1)) {
|
||||
await castYesVote(member.id, alphaGroupId);
|
||||
}
|
||||
|
||||
// ...meanwhile the one who left gives up on queueing alone and queues again
|
||||
await SQGroupRepository.leaveGroup(alphaMembers[0].id);
|
||||
const result = await SQGroupRepository.insert({
|
||||
status: "ACTIVE",
|
||||
userId: alphaMembers[0].id,
|
||||
});
|
||||
|
||||
const votes = await fetchVotes(alphaGroupId);
|
||||
expect(votes.filter((vote) => vote.isContinuing)).toHaveLength(
|
||||
FULL_GROUP_SIZE - 1,
|
||||
);
|
||||
expect(result.chatCodeToRevalidate).toBeNull();
|
||||
});
|
||||
|
||||
test("does not record any vote when user has no previous matchmade group", async () => {
|
||||
const user = await UserFactory.create();
|
||||
|
||||
|
||||
@@ -1045,6 +1045,13 @@ async function deleteReadyCheckInTrx(
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the user as not continuing with the group they last played a matchmade
|
||||
* match with, taking that group's votes in favour down with it. Getting a group
|
||||
* elsewhere overrides a vote already cast in favour: the group can no longer
|
||||
* continue at the size that vote was for, so the rest have to vote again. Once
|
||||
* their no vote is in, that group is settled and later queue actions leave it be.
|
||||
*/
|
||||
async function recordImplicitRejoinNoVote(
|
||||
userId: number,
|
||||
trx: Transaction<DB>,
|
||||
@@ -1060,18 +1067,19 @@ async function recordImplicitRejoinNoVote(
|
||||
]),
|
||||
),
|
||||
)
|
||||
.leftJoin("GroupMatchContinueVote", (join) =>
|
||||
join
|
||||
.onRef("GroupMatchContinueVote.groupId", "=", "Group.id")
|
||||
.on("GroupMatchContinueVote.userId", "=", userId),
|
||||
)
|
||||
.select(["Group.id as groupId", "GroupMatch.chatCode as matchChatCode"])
|
||||
.select((eb) => [
|
||||
"Group.id as groupId",
|
||||
"GroupMatch.chatCode as matchChatCode",
|
||||
hasVotedNo(eb, userId).as("alreadySettled"),
|
||||
])
|
||||
.where("GroupMember.userId", "=", userId)
|
||||
.where("Group.matchmade", "=", 1)
|
||||
.where("GroupMatchContinueVote.id", "is", null)
|
||||
// only the group they came from is still voting, older ones are long settled
|
||||
.orderBy("Group.id", "desc")
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!candidate) return null;
|
||||
if (!candidate || candidate.alreadySettled) return null;
|
||||
|
||||
await trx
|
||||
.deleteFrom("GroupMatchContinueVote")
|
||||
@@ -1094,6 +1102,18 @@ async function recordImplicitRejoinNoVote(
|
||||
return candidate.matchChatCode;
|
||||
}
|
||||
|
||||
/** Matches the `Group` rows the given user has already voted against continuing with. */
|
||||
function hasVotedNo(eb: ExpressionBuilder<DB, "Group">, userId: number) {
|
||||
return eb.exists(
|
||||
eb
|
||||
.selectFrom("GroupMatchContinueVote")
|
||||
.select("GroupMatchContinueVote.id")
|
||||
.whereRef("GroupMatchContinueVote.groupId", "=", "Group.id")
|
||||
.where("GroupMatchContinueVote.userId", "=", userId)
|
||||
.where("GroupMatchContinueVote.isContinuing", "=", 0),
|
||||
);
|
||||
}
|
||||
|
||||
/** Matches the `GroupMember` rows that have no confirmation for the given ready check. */
|
||||
function didNotConfirmReadyCheck(
|
||||
eb: ExpressionBuilder<DB, "GroupMember">,
|
||||
|
||||
@@ -242,6 +242,55 @@ test.describe("SendouQ match page", () => {
|
||||
await expect(match.locators.declinedText).toBeVisible();
|
||||
});
|
||||
|
||||
test("Rejoin vote: queueing solo after voting yes shows the rest a no", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const { matchId, alpha } = await createMatch(factories, {
|
||||
isMatchmade: true,
|
||||
isConcluded: true,
|
||||
});
|
||||
|
||||
const [owner, impatient, memberC, memberD] = alpha;
|
||||
|
||||
await impersonate(page, owner.id);
|
||||
const match = new SendouQMatchPage(page);
|
||||
await match.goto(matchId);
|
||||
await match.voteYes();
|
||||
|
||||
await impersonate(page, impatient.id);
|
||||
await match.goto(matchId);
|
||||
await match.voteYes();
|
||||
|
||||
// ...and then gives up on waiting for the rest and queues up alone instead
|
||||
const q = new SendouQPage(page);
|
||||
await q.goto();
|
||||
await q.joinSolo();
|
||||
|
||||
await impersonate(page, owner.id);
|
||||
await match.goto(matchId);
|
||||
|
||||
// their checkmark turned into a cross, taking the yes votes cast for a
|
||||
// group of four with it
|
||||
await expect(match.locators.votedNo).toHaveCount(1);
|
||||
await expect(match.locators.votedYes).toHaveCount(0);
|
||||
await expect(match.locators.pendingVotes).toHaveCount(3);
|
||||
|
||||
for (const member of [owner, memberC, memberD]) {
|
||||
await impersonate(page, member.id);
|
||||
await match.goto(matchId);
|
||||
await match.voteYes();
|
||||
}
|
||||
|
||||
// the three who stayed get their group, rather than being sent back to /q
|
||||
await impersonate(page, owner.id);
|
||||
await q.goto();
|
||||
await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
|
||||
|
||||
const looking = new SendouQLookingPage(page);
|
||||
await expect(looking.ownGroupCard.members).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("Rejoin vote: cascade wipes yes on no, revote completes and rejoins", async ({
|
||||
page,
|
||||
factories,
|
||||
|
||||
Reference in New Issue
Block a user