Skip to content

SendouQ match · E2E tests

3 modules · 17 exports · 5 repositories · 13 e2e tests · 1 route

  • Users a test creates itself are coloured per test
  • Arrange data setup
  • Act what the user does
  • Assert what is expected

SendouQ match page

Score reporting: report, undo, weapon report, confirm

alpha[0]bravo[0]sendouq-match.spec.ts:18

Renders/q/match/:id

Setup
Arrange
  1. Create match → { matchId, alpha, bravo }
    const { matchId, alpha, bravo } = await createMatch(factories);
Aalpha[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Report map winner “ALPHA”
    await match.reportMapWinner("ALPHA");
  3. Report map winner “ALPHA”
    await match.reportMapWinner("ALPHA");
Assert
  1. undo report button is visible
    await expect(match.locators.undoReportButton).toBeVisible();
Act
  1. Undo report
    await match.undoReport();
  2. Report map winner “ALPHA”
    await match.reportMapWinner("ALPHA");
  3. Report weapon “Splattershot”
    await match.reportWeapon("Splattershot");
Assert
  1. undo weapon button is visible
    await expect(match.locators.undoWeaponButton).toBeVisible();
Act
  1. Report map winner “BRAVO”
    await match.reportMapWinner("BRAVO");
  2. Report map winner “ALPHA”
    await match.reportMapWinner("ALPHA");
  3. Report set ending map “ALPHA”
    await match.reportSetEndingMap("ALPHA");
Bbravo[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Confirm score
    await match.confirmScore();
Assert
  1. score 4, 1 is visible
    await expect(match.score(4, 1)).toBeVisible();
Act
  1. the compact action-tab timeline omits per-map weapons, the result tab's shows them

    Open the SendouQ match page matchId, “result”
    await match.goto(matchId, "result");
Assert
  1. reported weapon image “Splattershot” is visible
    await expect(match.reportedWeaponImage("Splattershot")).toBeVisible();
Source
test("Score reporting: report, undo, weapon report, confirm", async ({
	page,
	factories,
}) => {
	const { matchId, alpha, bravo } = await createMatch(factories);

	await impersonate(page, alpha[0].id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await match.reportMapWinner("ALPHA");
	await match.reportMapWinner("ALPHA");

	await expect(match.locators.undoReportButton).toBeVisible();
	await match.undoReport();

	await match.reportMapWinner("ALPHA");

	await match.reportWeapon("Splattershot");
	await expect(match.locators.undoWeaponButton).toBeVisible();

	await match.reportMapWinner("BRAVO");
	await match.reportMapWinner("ALPHA");
	await match.reportSetEndingMap("ALPHA");

	await impersonate(page, bravo[0].id);
	await match.goto(matchId);
	await match.confirmScore();

	await expect(match.score(4, 1)).toBeVisible();

	// the compact action-tab timeline omits per-map weapons, the result tab's shows them
	await match.goto(matchId, "result");
	await expect(match.reportedWeaponImage("Splattershot")).toBeVisible();
})

Staff score report: non-participant staff force-reports and locks match

Renders/q/match/:id

Setup
Arrange
  1. Create match → { matchId }
    const { matchId } = await createMatch(factories);
  2. Create staff user → staff
    const staff = await factories.UserFactory.createStaff();
Sstaff
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Report sweep “ALPHA”
    await match.reportSweep("ALPHA");
Assert
  1. score 4, 0 is visible
    await expect(match.score(4, 0)).toBeVisible();
Source
test("Staff score report: non-participant staff force-reports and locks match", async ({
	page,
	factories,
}) => {
	const { matchId } = await createMatch(factories);
	const staff = await factories.UserFactory.createStaff();

	await impersonate(page, staff.id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await match.reportSweep("ALPHA");

	await expect(match.score(4, 0)).toBeVisible();
})

Staff score confirm: confirms participant's 4-0 set-ender locks match

alpha[0]staffbravo[0]sendouq-match.spec.ts:70

Renders/q/match/:id

Setup
Arrange
  1. Create match → { matchId, alpha, bravo }
    const { matchId, alpha, bravo } = await createMatch(factories);
  2. Create staff user → staff
    const staff = await factories.UserFactory.createStaff();
Aalpha[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Report sweep “ALPHA”
    await match.reportSweep("ALPHA");
Sstaff
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Confirm score
    await match.confirmScore();
Assert
  1. score 4, 0 is visible
    await expect(match.score(4, 0)).toBeVisible();
Bbravo[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. look again button is visible
    await expect(match.locators.lookAgainButton).toBeVisible();
Source
test("Staff score confirm: confirms participant's 4-0 set-ender locks match", async ({
	page,
	factories,
}) => {
	const { matchId, alpha, bravo } = await createMatch(factories);
	const staff = await factories.UserFactory.createStaff();

	await impersonate(page, alpha[0].id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await match.reportSweep("ALPHA");

	await impersonate(page, staff.id);
	await match.goto(matchId);
	await match.confirmScore();

	await expect(match.score(4, 0)).toBeVisible();

	// match is locked; the other team now sees the rejoin button
	await impersonate(page, bravo[0].id);
	await match.goto(matchId);
	await expect(match.locators.lookAgainButton).toBeVisible();
})

Cancel flow: request, refused, re-request, accepted locks match and sends webhook

alpha[0]bravo[0]sendouq-match.spec.ts:95

Renders/q/match/:id

Setup
Arrange
  1. Create match → { matchId, alpha, bravo }
    const { matchId, alpha, bravo } = await createMatch(factories);
Aalpha[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Request cancel reason “First cancel reason”
    await match.requestCancel({ reason: "First cancel reason" });
Assert
  1. cancel pending text is visible
    await expect(match.locators.cancelPendingText).toBeVisible();
Bbravo[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. cancel prompt is visible
    await expect(match.locators.cancelPrompt).toBeVisible();
Act
  1. Refuse cancel
    await match.refuseCancel();
Aalpha[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Request cancel reason “Requester network issues”
    await match.requestCancel({ reason: "Requester network issues" });
Bbravo[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Capture cancel webhook e2eWebhookPort(testInfo.parallelIndex), async () => { await match.acceptCancel({ reason: "Accepter agrees to cancel" … → webhook
    const webhook = await captureCancelWebhook(
    	e2eWebhookPort(testInfo.parallelIndex),
    	async () => {
    		await match.acceptCancel({ reason: "Accepter agrees to cancel" });
    	},
    );
Assert
  1. canceled text is visible
    await expect(match.locators.canceledText).toBeVisible();
  2. webhook.embeds has 1 items
    expect(webhook.embeds).toHaveLength(1);
  3. embed = webhook.embeds[0]
    const embed = webhook.embeds[0];
  4. embed.title is “SendouQ match canceled”
    expect(embed.title).toBe("SendouQ match canceled");
  5. fieldValue = (name: string) => embed.fields.find((field) => field.name.startsWith(name))?.…
    const fieldValue = (name: string) =>
    	embed.fields.find((field) => field.name.startsWith(name))?.value;
  6. fieldValue("Match") contains `#${matchId}`
    expect(fieldValue("Match")).toContain(`#${matchId}`);
  7. fieldValue("Requesting team's reason") is “Requester network issues”
    expect(fieldValue("Requesting team's reason")).toBe(
    	"Requester network issues",
    );
  8. fieldValue("Accepting team's reason") is “Accepter agrees to cancel”
    expect(fieldValue("Accepting team's reason")).toBe(
    	"Accepter agrees to cancel",
    );
  9. both teams nominated the first listed player

    fieldValue("Teams nominated the same players") is “Yes”
    expect(fieldValue("Teams nominated the same players")).toBe("Yes");
  10. fieldValue("Times nominated in canceled matches") match /season: 1 • year: 1/
    expect(fieldValue("Times nominated in canceled matches")).toMatch(
    	/season: 1 • year: 1/,
    );
Source
test("Cancel flow: request, refused, re-request, accepted locks match and sends webhook", async ({
	page,
	factories,
}, testInfo) => {
	const { matchId, alpha, bravo } = await createMatch(factories);

	await impersonate(page, alpha[0].id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await match.requestCancel({ reason: "First cancel reason" });
	await expect(match.locators.cancelPendingText).toBeVisible();

	await impersonate(page, bravo[0].id);
	await match.goto(matchId);
	await expect(match.locators.cancelPrompt).toBeVisible();
	await match.refuseCancel();

	await impersonate(page, alpha[0].id);
	await match.goto(matchId);
	await match.requestCancel({ reason: "Requester network issues" });

	await impersonate(page, bravo[0].id);
	await match.goto(matchId);
	const webhook = await captureCancelWebhook(
		e2eWebhookPort(testInfo.parallelIndex),
		async () => {
			await match.acceptCancel({ reason: "Accepter agrees to cancel" });
		},
	);

	await expect(match.locators.canceledText).toBeVisible();

	expect(webhook.embeds).toHaveLength(1);
	const embed = webhook.embeds[0];
	expect(embed.title).toBe("SendouQ match canceled");

	const fieldValue = (name: string) =>
		embed.fields.find((field) => field.name.startsWith(name))?.value;
	expect(fieldValue("Match")).toContain(`#${matchId}`);
	expect(fieldValue("Requesting team's reason")).toBe(
		"Requester network issues",
	);
	expect(fieldValue("Accepting team's reason")).toBe(
		"Accepter agrees to cancel",
	);
	// both teams nominated the first listed player
	expect(fieldValue("Teams nominated the same players")).toBe("Yes");
	expect(fieldValue("Times nominated in canceled matches")).toMatch(
		/season: 1 • year: 1/,
	);
})

Auto-resolve: day-old match with no score reported is canceled

Renders/q/match/:id

Setup
Arrange
  1. Create match created at sub(new Date(), { hours: 25 }) → { matchId, alpha }
    const { matchId, alpha } = await createMatch(factories, {
    	createdAt: sub(new Date(), { hours: 25 }),
    });
Aalpha[0]
Act
  1. Run routine “ResolveStaleSQMatches”
    await runRoutine(page, "ResolveStaleSQMatches");
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. canceled text is visible
    await expect(match.locators.canceledText).toBeVisible();
Source
test("Auto-resolve: day-old match with no score reported is canceled", async ({
	page,
	factories,
}) => {
	const { matchId, alpha } = await createMatch(factories, {
		createdAt: sub(new Date(), { hours: 25 }),
	});

	await impersonate(page, alpha[0].id);
	await runRoutine(page, "ResolveStaleSQMatches");

	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await expect(match.locators.canceledText).toBeVisible();
})

Auto-resolve: day-old match the other team never confirmed is confirmed

Renders/q/match/:id

Setup
Arrange
  1. Create match is reported true created at sub(new Date(), { hours: 25 }) → { matchId, bravo }
    const { matchId, bravo } = await createMatch(factories, {
    	isReported: true,
    	createdAt: sub(new Date(), { hours: 25 }),
    });
Bbravo[0]
Act
  1. Run routine “ResolveStaleSQMatches”
    await runRoutine(page, "ResolveStaleSQMatches");
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. score 4, 0 is visible
    await expect(match.score(4, 0)).toBeVisible();
  2. the requeue window runs out a day after the match was made, so by the time the routine resolves one there is nothing left to requeue with

    look again button is hidden
    await expect(match.locators.lookAgainButton).toBeHidden();
Source
test("Auto-resolve: day-old match the other team never confirmed is confirmed", async ({
	page,
	factories,
}) => {
	const { matchId, bravo } = await createMatch(factories, {
		isReported: true,
		createdAt: sub(new Date(), { hours: 25 }),
	});

	await impersonate(page, bravo[0].id);
	await runRoutine(page, "ResolveStaleSQMatches");

	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await expect(match.score(4, 0)).toBeVisible();
	// the requeue window runs out a day after the match was made, so by the time
	// the routine resolves one there is nothing left to requeue with
	await expect(match.locators.lookAgainButton).toBeHidden();
})

Rejoin: trusted group one-click look again

Renders/q/looking/q/match/:id

Setup
Arrange
  1. Create match is concluded true → { matchId, bravo }
    const { matchId, bravo } = await createMatch(factories, {
    	isConcluded: true,
    });
Bbravo[1]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Look again
    await match.lookAgain();
SendouQ page
Act
  1. Open the SendouQ page
    await new SendouQPage(page).goto();
Assert
  1. the page URL is SENDOUQ_LOOKING_PAGE
    await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
Source
test("Rejoin: trusted group one-click look again", async ({
	page,
	factories,
}) => {
	const { matchId, bravo } = await createMatch(factories, {
		isConcluded: true,
	});

	// any member can re-queue the group, not only the member who created it
	await impersonate(page, bravo[1].id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);
	await match.lookAgain();

	await new SendouQPage(page).goto();
	await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
})

Rejoin after cancel: trusted group requeues with the blamed member

ownerbravo[0]blamedsendouq-match.spec.ts:204

Renders/q/looking/q/match/:id

Setup
Arrange
  1. Create match → { matchId, alpha, bravo }
    const { matchId, alpha, bravo } = await createMatch(factories);
  2. [owner, blamed] = alpha
    const [owner, blamed] = alpha;
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Request cancel reason “Our fourth never came back to the room” nominate user id blamed.id
    await match.requestCancel({
    	reason: "Our fourth never came back to the room",
    	nominateUserId: blamed.id,
    });
Bbravo[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Accept cancel reason “We waited for them for 15 minutes” nominate user id blamed.id
    await match.acceptCancel({
    	reason: "We waited for them for 15 minutes",
    	nominateUserId: blamed.id,
    });
Bblamed
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Look again
    await match.lookAgain();
SendouQ page
Act
  1. Open the SendouQ page
    await new SendouQPage(page).goto();
Assert
  1. the page URL is SENDOUQ_LOOKING_PAGE
    await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
SendouQ looking page
Assert
  1. members count is FULL_GROUP_SIZE
    await expect(looking.ownGroupCard.members).toHaveCount(FULL_GROUP_SIZE);
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. look again button is hidden
    await expect(match.locators.lookAgainButton).toBeHidden();
  2. back to queue button is visible
    await expect(match.locators.backToQueueButton).toBeVisible();
Source
test("Rejoin after cancel: trusted group requeues with the blamed member", async ({
	page,
	factories,
}) => {
	const { matchId, alpha, bravo } = await createMatch(factories);
	const [owner, blamed] = alpha;

	await impersonate(page, owner.id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);
	await match.requestCancel({
		reason: "Our fourth never came back to the room",
		nominateUserId: blamed.id,
	});

	await impersonate(page, bravo[0].id);
	await match.goto(matchId);
	await match.acceptCancel({
		reason: "We waited for them for 15 minutes",
		nominateUserId: blamed.id,
	});

	// a premade group is a team: the blamed player requeues with the rest of them
	await impersonate(page, blamed.id);
	await match.goto(matchId);
	await match.lookAgain();

	await new SendouQPage(page).goto();
	await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);

	const looking = new SendouQLookingPage(page);
	await expect(looking.ownGroupCard.members).toHaveCount(FULL_GROUP_SIZE);

	// the group is already back in the queue, so the rest of them are pointed at
	// it rather than at a requeue that can only fail
	await impersonate(page, owner.id);
	await match.goto(matchId);
	await expect(match.locators.lookAgainButton).toBeHidden();
	await expect(match.locators.backToQueueButton).toBeVisible();
})

Rejoin vote: 'no' shows rejoin queue button that rejoins directly

Renders/q/looking/q/match/:id

Setup
Arrange
  1. Create match is matchmade true is concluded true → { matchId, alpha }
    const { matchId, alpha } = await createMatch(factories, {
    	isMatchmade: true,
    	isConcluded: true,
    });
Aalpha[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Vote no
    await match.voteNo();
Assert
  1. declined text is visible
    await expect(match.locators.declinedText).toBeVisible();
Act
  1. Rejoin queue
    await match.rejoinQueue();
Assert
  1. the page URL is SENDOUQ_LOOKING_PAGE
    await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
Source
test("Rejoin vote: 'no' shows rejoin queue button that rejoins directly", async ({
	page,
	factories,
}) => {
	const { matchId, alpha } = await createMatch(factories, {
		isMatchmade: true,
		isConcluded: true,
	});

	await impersonate(page, alpha[0].id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);
	await match.voteNo();

	await expect(match.locators.declinedText).toBeVisible();
	await match.rejoinQueue();

	await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
})

Rejoin vote: changing a yes vote to no takes effect

Renders/q/match/:id

Setup
Arrange
  1. Create match is matchmade true is concluded true → { matchId, alpha }
    const { matchId, alpha } = await createMatch(factories, {
    	isMatchmade: true,
    	isConcluded: true,
    });
Aalpha[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Vote yes
    await match.voteYes();
Assert
  1. voted yes count is 1
    await expect(match.locators.votedYes).toHaveCount(1);
Act
  1. Vote no
    await match.voteNo();
Assert
  1. declined text is visible
    await expect(match.locators.declinedText).toBeVisible();
Source
test("Rejoin vote: changing a yes vote to no takes effect", async ({
	page,
	factories,
}) => {
	const { matchId, alpha } = await createMatch(factories, {
		isMatchmade: true,
		isConcluded: true,
	});

	await impersonate(page, alpha[0].id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);

	await match.voteYes();
	await expect(match.locators.votedYes).toHaveCount(1);

	await match.voteNo();

	await expect(match.locators.declinedText).toBeVisible();
})

Rejoin vote: queueing solo after voting yes shows the rest a no

Renders/q/q/looking/q/match/:id

Setup
Arrange
  1. Create match is matchmade true is concluded true → { matchId, alpha }
    const { matchId, alpha } = await createMatch(factories, {
    	isMatchmade: true,
    	isConcluded: true,
    });
  2. [owner, impatient, memberC, memberD] = alpha
    const [owner, impatient, memberC, memberD] = alpha;
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Vote yes
    await match.voteYes();
Iimpatient
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Vote yes
    await match.voteYes();
SendouQ page
Act
  1. Open the SendouQ page
    await q.goto();
  2. Join solo
    await q.joinSolo();
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. their checkmark turned into a cross, taking the yes votes cast for a group of four with it

    voted no count is 1
    await expect(match.locators.votedNo).toHaveCount(1);
  2. voted yes count is 0
    await expect(match.locators.votedYes).toHaveCount(0);
  3. pending votes count is 3
    await expect(match.locators.pendingVotes).toHaveCount(3);
Act
  1. For each member of owner, memberC, memberD
    for (const member of [owner, memberC, memberD]) {
    	await impersonate(page, member.id);
    	await match.goto(matchId);
    	await match.voteYes();
    }
    1. await impersonate(page, member.id);
    2. Open the SendouQ match page matchId
      await match.goto(matchId);
    3. Vote yes
      await match.voteYes();
Oowner
SendouQ page
Act
  1. Open the SendouQ page
    await q.goto();
Assert
  1. the page URL is SENDOUQ_LOOKING_PAGE
    await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
SendouQ looking page
Assert
  1. members count is 3
    await expect(looking.ownGroupCard.members).toHaveCount(3);
Source
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);
})

Rejoin after cancel: the player both teams blamed is voted out of the requeue

ownerbravo[0]blamedsendouq-match.spec.ts:335

Renders/q/looking/q/match/:id

Setup
Arrange
  1. Create match is matchmade true → { matchId, alpha, bravo }
    const { matchId, alpha, bravo } = await createMatch(factories, {
    	isMatchmade: true,
    });
  2. [owner, blamed, memberC, memberD] = alpha
    const [owner, blamed, memberC, memberD] = alpha;
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Request cancel reason “Our fourth never joined the room” nominate user id blamed.id
    await match.requestCancel({
    	reason: "Our fourth never joined the room",
    	nominateUserId: blamed.id,
    });
Bbravo[0]
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Accept cancel reason “We waited for them for 15 minutes” nominate user id blamed.id
    await match.acceptCancel({
    	reason: "We waited for them for 15 minutes",
    	nominateUserId: blamed.id,
    });
Assert
  1. nobody on the other team was blamed, so their requeue is a plain vote

    pending votes count is FULL_GROUP_SIZE
    await expect(match.locators.pendingVotes).toHaveCount(FULL_GROUP_SIZE);
Bblamed
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. declined text is visible
    await expect(match.locators.declinedText).toBeVisible();
Act
  1. For each member of owner, memberC, memberD
    for (const member of [owner, memberC, memberD]) {
    	await impersonate(page, member.id);
    	await match.goto(matchId);
    	await match.voteYes();
    }
    1. await impersonate(page, member.id);
    2. Open the SendouQ match page matchId
      await match.goto(matchId);
    3. Vote yes
      await match.voteYes();
Oowner
SendouQ page
Act
  1. Open the SendouQ page
    await new SendouQPage(page).goto();
Assert
  1. the page URL is SENDOUQ_LOOKING_PAGE
    await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
SendouQ looking page
Assert
  1. members count is 3
    await expect(looking.ownGroupCard.members).toHaveCount(3);
Source
test("Rejoin after cancel: the player both teams blamed is voted out of the requeue", async ({
	page,
	factories,
}) => {
	const { matchId, alpha, bravo } = await createMatch(factories, {
		isMatchmade: true,
	});
	const [owner, blamed, memberC, memberD] = alpha;

	await impersonate(page, owner.id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);
	await match.requestCancel({
		reason: "Our fourth never joined the room",
		nominateUserId: blamed.id,
	});

	await impersonate(page, bravo[0].id);
	await match.goto(matchId);
	await match.acceptCancel({
		reason: "We waited for them for 15 minutes",
		nominateUserId: blamed.id,
	});

	// nobody on the other team was blamed, so their requeue is a plain vote
	await expect(match.locators.pendingVotes).toHaveCount(FULL_GROUP_SIZE);

	await impersonate(page, blamed.id);
	await match.goto(matchId);
	await expect(match.locators.declinedText).toBeVisible();

	for (const member of [owner, memberC, memberD]) {
		await impersonate(page, member.id);
		await match.goto(matchId);
		await match.voteYes();
	}

	await impersonate(page, owner.id);
	await new SendouQPage(page).goto();
	await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);

	const looking = new SendouQLookingPage(page);
	await expect(looking.ownGroupCard.members).toHaveCount(3);
})

Rejoin vote: cascade wipes yes on no, revote completes and rejoins

Renders/q/looking/q/match/:id

Setup
Arrange
  1. Create match is matchmade true is concluded true → { matchId, alpha }
    const { matchId, alpha } = await createMatch(factories, {
    	isMatchmade: true,
    	isConcluded: true,
    });
  2. [owner, memberB, memberC, memberD] = alpha
    const [owner, memberB, memberC, memberD] = alpha;
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Vote yes
    await match.voteYes();
Assert
  1. voted yes count is 1
    await expect(match.locators.votedYes).toHaveCount(1);
  2. pending votes count is 3
    await expect(match.locators.pendingVotes).toHaveCount(3);
MBmember B
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
  2. Vote no
    await match.voteNo();
Oowner
SendouQ match page
Act
  1. Open the SendouQ match page matchId
    await match.goto(matchId);
Assert
  1. the owner's yes was wiped by member B's no → back to pending

    voted no count is 1
    await expect(match.locators.votedNo).toHaveCount(1);
  2. voted yes count is 0
    await expect(match.locators.votedYes).toHaveCount(0);
Act
  1. Vote yes
    await match.voteYes();
  2. For each member of memberC, memberD
    for (const member of [memberC, memberD]) {
    	await impersonate(page, member.id);
    	await match.goto(matchId);
    	await match.voteYes();
    }
    1. await impersonate(page, member.id);
    2. Open the SendouQ match page matchId
      await match.goto(matchId);
    3. Vote yes
      await match.voteYes();
Oowner
SendouQ page
Act
  1. Open the SendouQ page
    await new SendouQPage(page).goto();
Assert
  1. the page URL is SENDOUQ_LOOKING_PAGE
    await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);
SendouQ looking page
Assert
  1. members count is 3
    await expect(looking.ownGroupCard.members).toHaveCount(3);
Source
test("Rejoin vote: cascade wipes yes on no, revote completes and rejoins", async ({
	page,
	factories,
}) => {
	const { matchId, alpha } = await createMatch(factories, {
		isMatchmade: true,
		isConcluded: true,
	});

	const [owner, memberB, memberC, memberD] = alpha;

	await impersonate(page, owner.id);
	const match = new SendouQMatchPage(page);
	await match.goto(matchId);
	await match.voteYes();

	await expect(match.locators.votedYes).toHaveCount(1);
	await expect(match.locators.pendingVotes).toHaveCount(3);

	await impersonate(page, memberB.id);
	await match.goto(matchId);
	await match.voteNo();

	await impersonate(page, owner.id);
	await match.goto(matchId);
	// the owner's yes was wiped by member B's no → back to pending
	await expect(match.locators.votedNo).toHaveCount(1);
	await expect(match.locators.votedYes).toHaveCount(0);
	await match.voteYes();

	for (const member of [memberC, memberD]) {
		await impersonate(page, member.id);
		await match.goto(matchId);
		await match.voteYes();
	}

	await impersonate(page, owner.id);
	await new SendouQPage(page).goto();
	await expect(page).toHaveURL(SENDOUQ_LOOKING_PAGE);

	// the member who voted no was left behind
	const looking = new SendouQLookingPage(page);
	await expect(looking.ownGroupCard.members).toHaveCount(3);
})