SendouQ match page Score reporting: report, undo, weapon report, confirm Renders/q/match/:id
SendouQ match page SendouQ match page Arrange
Create match → { matchId, alpha, bravo }
const { matchId, alpha, bravo } = await createMatch (factories);SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Report map winner “ALPHA”
await match.reportMapWinner ("ALPHA" );Report map winner “ALPHA”
await match.reportMapWinner ("ALPHA" );Assert
undo report button is visible
await expect (match.locators.undoReportButton).toBeVisible ();Act
Undo report
await match.undoReport ();Report map winner “ALPHA”
await match.reportMapWinner ("ALPHA" );Report weapon “Splattershot”
await match.reportWeapon ("Splattershot" );Assert
undo weapon button is visible
await expect (match.locators.undoWeaponButton).toBeVisible ();Act
Report map winner “BRAVO”
await match.reportMapWinner ("BRAVO" );Report map winner “ALPHA”
await match.reportMapWinner ("ALPHA" );Report set ending map “ALPHA”
await match.reportSetEndingMap ("ALPHA" );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Confirm score
await match.confirmScore ();Assert
score 4 , 1 is visible
await expect (match.score (4 , 1 )).toBeVisible ();Act
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
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
SendouQ match page Arrange
Create match → { matchId }
const { matchId } = await createMatch (factories);Create staff user → staff
const staff = await factories.UserFactory .createStaff ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Report sweep “ALPHA”
await match.reportSweep ("ALPHA" );Assert
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 Renders/q/match/:id
SendouQ match page SendouQ match page SendouQ match page Arrange
Create match → { matchId, alpha, bravo }
const { matchId, alpha, bravo } = await createMatch (factories);Create staff user → staff
const staff = await factories.UserFactory .createStaff ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Report sweep “ALPHA”
await match.reportSweep ("ALPHA" );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Confirm score
await match.confirmScore ();Assert
score 4 , 0 is visible
await expect (match.score (4 , 0 )).toBeVisible ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
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 Renders/q/match/:id
SendouQ match page SendouQ match page SendouQ match page SendouQ match page Arrange
Create match → { matchId, alpha, bravo }
const { matchId, alpha, bravo } = await createMatch (factories);SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Request cancel reason “First cancel reason”
await match.requestCancel ({ reason: "First cancel reason" });Assert
cancel pending text is visible
await expect (match.locators.cancelPendingText).toBeVisible ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
cancel prompt is visible
await expect (match.locators.cancelPrompt).toBeVisible ();Act
Refuse cancel
await match.refuseCancel ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Request cancel reason “Requester network issues”
await match.requestCancel ({ reason: "Requester network issues" });SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);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
canceled text is visible
await expect (match.locators.canceledText).toBeVisible ();webhook.embeds has 1 items
expect (webhook.embeds).toHaveLength (1 );embed = webhook.embeds[0 ]
const embed = webhook.embeds[0 ];embed.title is “SendouQ match canceled”
expect (embed.title).toBe ("SendouQ match canceled" );fieldValue = (name: string ) => embed.fields.find ((field) => field.name.startsWith (name))?.…
const fieldValue = (name: string ) =>
embed.fields.find ((field) => field.name.startsWith (name))?.value;fieldValue ("Match" ) contains `#${matchId}`
expect (fieldValue ("Match" )).toContain (`#${matchId}` );fieldValue ("Requesting team's reason" ) is “Requester network issues”
expect (fieldValue ("Requesting team's reason" )).toBe (
"Requester network issues" ,
);fieldValue ("Accepting team's reason" ) is “Accepter agrees to cancel”
expect (fieldValue ("Accepting team's reason" )).toBe (
"Accepter agrees to cancel" ,
);both teams nominated the first listed player
fieldValue ("Teams nominated the same players" ) is “Yes”
expect (fieldValue ("Teams nominated the same players" )).toBe ("Yes" );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
SendouQ match page Arrange
Create match created at sub (new Date (), { hours: 25 }) → { matchId, alpha }
const { matchId, alpha } = await createMatch (factories, {
createdAt: sub (new Date (), { hours: 25 }),
});Act
Run routine “ResolveStaleSQMatches”
await runRoutine (page, "ResolveStaleSQMatches" );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
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
SendouQ match page Arrange
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 }),
});Act
Run routine “ResolveStaleSQMatches”
await runRoutine (page, "ResolveStaleSQMatches" );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
score 4 , 0 is visible
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
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
SendouQ match page SendouQ page Arrange
Create match is concluded true → { matchId, bravo }
const { matchId, bravo } = await createMatch (factories, {
isConcluded: true ,
});SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Look again
await match.lookAgain ();SendouQ page Act
Open the SendouQ page
await new SendouQPage (page).goto ();Assert
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 Renders/q/looking /q/match/:id
SendouQ match page SendouQ match page SendouQ match page SendouQ page SendouQ looking page SendouQ match page Arrange
Create match → { matchId, alpha, bravo }
const { matchId, alpha, bravo } = await createMatch (factories);[owner, blamed] = alpha
const [owner, blamed] = alpha;SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);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,
});SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);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,
});SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Look again
await match.lookAgain ();SendouQ page Act
Open the SendouQ page
await new SendouQPage (page).goto ();Assert
the page URL is SENDOUQ_LOOKING_PAGE
await expect (page).toHaveURL (SENDOUQ_LOOKING_PAGE );SendouQ looking page Assert
members count is FULL_GROUP_SIZE
await expect (looking.ownGroupCard.members).toHaveCount (FULL_GROUP_SIZE );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
look again button is hidden
await expect (match.locators.lookAgainButton).toBeHidden ();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
SendouQ match page Arrange
Create match is matchmade true is concluded true → { matchId, alpha }
const { matchId, alpha } = await createMatch (factories, {
isMatchmade: true ,
isConcluded: true ,
});SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Vote no
await match.voteNo ();Assert
declined text is visible
await expect (match.locators.declinedText).toBeVisible ();Act
Rejoin queue
await match.rejoinQueue ();Assert
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
SendouQ match page Arrange
Create match is matchmade true is concluded true → { matchId, alpha }
const { matchId, alpha } = await createMatch (factories, {
isMatchmade: true ,
isConcluded: true ,
});SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();Assert
voted yes count is 1
await expect (match.locators.votedYes).toHaveCount (1 );Act
Vote no
await match.voteNo ();Assert
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
SendouQ match page SendouQ match page SendouQ page SendouQ match page SendouQ page SendouQ looking page Arrange
Create match is matchmade true is concluded true → { matchId, alpha }
const { matchId, alpha } = await createMatch (factories, {
isMatchmade: true ,
isConcluded: true ,
});[owner, impatient, memberC, memberD] = alpha
const [owner, impatient, memberC, memberD] = alpha;SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();SendouQ page Act
Open the SendouQ page
await q.goto ();Join solo
await q.joinSolo ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
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 );voted yes count is 0
await expect (match.locators.votedYes).toHaveCount (0 );pending votes count is 3
await expect (match.locators.pendingVotes).toHaveCount (3 );Act
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 ();
}
await impersonate (page, member.id);Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();SendouQ page Act
Open the SendouQ page
await q.goto ();Assert
the page URL is SENDOUQ_LOOKING_PAGE
await expect (page).toHaveURL (SENDOUQ_LOOKING_PAGE );SendouQ looking page Assert
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 Renders/q/looking /q/match/:id
SendouQ match page SendouQ match page SendouQ match page SendouQ page SendouQ looking page Arrange
Create match is matchmade true → { matchId, alpha, bravo }
const { matchId, alpha, bravo } = await createMatch (factories, {
isMatchmade: true ,
});[owner, blamed, memberC, memberD] = alpha
const [owner, blamed, memberC, memberD] = alpha;SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);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,
});SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);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
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 );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
declined text is visible
await expect (match.locators.declinedText).toBeVisible ();Act
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 ();
}
await impersonate (page, member.id);Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();SendouQ page Act
Open the SendouQ page
await new SendouQPage (page).goto ();Assert
the page URL is SENDOUQ_LOOKING_PAGE
await expect (page).toHaveURL (SENDOUQ_LOOKING_PAGE );SendouQ looking page Assert
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
SendouQ match page SendouQ match page SendouQ match page SendouQ page SendouQ looking page Arrange
Create match is matchmade true is concluded true → { matchId, alpha }
const { matchId, alpha } = await createMatch (factories, {
isMatchmade: true ,
isConcluded: true ,
});[owner, memberB, memberC, memberD] = alpha
const [owner, memberB, memberC, memberD] = alpha;SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();Assert
voted yes count is 1
await expect (match.locators.votedYes).toHaveCount (1 );pending votes count is 3
await expect (match.locators.pendingVotes).toHaveCount (3 );SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Vote no
await match.voteNo ();SendouQ match page Act
Open the SendouQ match page matchId
await match.goto (matchId);Assert
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 );voted yes count is 0
await expect (match.locators.votedYes).toHaveCount (0 );Act
Vote yes
await match.voteYes ();For each member of memberC, memberD
for (const member of [memberC, memberD]) {
await impersonate (page, member.id);
await match.goto (matchId);
await match.voteYes ();
}
await impersonate (page, member.id);Open the SendouQ match page matchId
await match.goto (matchId);Vote yes
await match.voteYes ();SendouQ page Act
Open the SendouQ page
await new SendouQPage (page).goto ();Assert
the page URL is SENDOUQ_LOOKING_PAGE
await expect (page).toHaveURL (SENDOUQ_LOOKING_PAGE );SendouQ looking page Assert
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 );
})createMatche2e/sendouq-match.spec.ts:466
No JSDoc
async function createMatch (
factories: Factories ,
{
isMatchmade,
isConcluded,
isReported,
createdAt,
}: {
isMatchmade?: boolean ;
isConcluded?: boolean ;
isReported?: boolean ;
createdAt?: Date ;
} = {},
) {
// … SendouQMatchPage.gotoe2e/pages/sendouq/sendouq-match-page.ts:70
No JSDoc
async goto (matchId: number , tab: Tab = "action" ) {
await navigate ({
page: this .page,
url: `${sendouQMatchPage(matchId)}?tab=${tab}` ,
});
}SendouQMatchPage.reportMapWinnere2e/pages/sendouq/sendouq-match-page.ts:125
No JSDoc
async reportMapWinner (winner: Side ) {
await this .selectMapWinner (winner);
await waitForPOSTResponse (this .page, async () => {
await this .locators.reportScoreButton.click ();
});
// waitForPOSTResponse doesn't cover the loader revalidation, which remounts
// MatchActionTab (keyed on reportedCount) and the nested WeaponReporter; a
// follow-up click landing on the about-to-unmount instance loses its local state
await expect (this .locators.selectedWinner).toHaveCount (0 );
}SendouQMatchPage.locators.undoReportButtone2e/pages/sendouq/sendouq-match-page.ts:46
No JSDoc
page.getByRole ("button" , { name: "Undo report" }) SendouQMatchPage.undoReporte2e/pages/sendouq/sendouq-match-page.ts:145
No JSDoc
async undoReport () {
await waitForPOSTResponse (this .page, async () => {
await this .locators.undoReportButton.click ();
});
}SendouQMatchPage.reportWeapone2e/pages/sendouq/sendouq-match-page.ts:151
No JSDoc
async reportWeapon (name: string ) {
// The reporter is already expanded when the viewer's preference says so,
// and clicking then would collapse it instead.
if (await this .locators.reportWeaponsButton.isVisible ()) {
await waitForPOSTResponse (this .page, async () => {
await this .locators.reportWeaponsButton.click ();
});
}
await selectWeapon ({ page: this .page, name });
await waitForPOSTResponse (this .page, async () => {
await this .locators.submitWeaponButton.click ();
});
}SendouQMatchPage.locators.undoWeaponButtone2e/pages/sendouq/sendouq-match-page.ts:48
No JSDoc
page.getByRole ("button" , { name: "Undo weapon" }) SendouQMatchPage.reportSetEndingMape2e/pages/sendouq/sendouq-match-page.ts:137
async reportSetEndingMap (winner: Side ) {
await this .selectMapWinner (winner);
await this .locators.reportScoreButton.click ();
await waitForPOSTResponse (this .page, async () => {
await this .locators.confirmSetEndButton.click ();
});
}SendouQMatchPage.confirmScoree2e/pages/sendouq/sendouq-match-page.ts:165
No JSDoc
async confirmScore () {
await expect (this .locators.confirmScoreButton).toBeEnabled ({
timeout: 10_000 ,
});
// only a loss needs a second tap; any other first tap already submits, so
// the response wait has to be armed before it
const isLoss = (
await this .locators.confirmScoreButton.textContent ()
)?.includes ("loss" );
if (isLoss) {
await this .locators.confirmScoreButton.click ();
const armedLossButton = this .locators.confirmScoreButton.filter ({
hasText: "Tap again" ,
// … SendouQMatchPage.scoree2e/pages/sendouq/sendouq-match-page.ts:77
No JSDoc
score (alpha: number , bravo: number ) {
return this .page.getByText (new RegExp (`${alpha}\\s*-\\s*${bravo}` )).first ();
}SendouQMatchPage.reportedWeaponImagee2e/pages/sendouq/sendouq-match-page.ts:106
No JSDoc
reportedWeaponImage (name: string ) {
return this .page.getByRole ("img" , { name }).first ();
}SendouQMatchPagee2e/pages/sendouq/sendouq-match-page.ts:33
No JSDoc
SendouQMatchPage.reportSweepe2e/pages/sendouq/sendouq-match-page.ts:118
async reportSweep (winner: Side ) {
for (let i = 0 ; i < MAPS_TO_WIN - 1 ; i++) {
await this .reportMapWinner (winner);
}
await this .reportSetEndingMap (winner);
}SendouQMatchPage.locators.lookAgainButtone2e/pages/sendouq/sendouq-match-page.ts:54
No JSDoc
page.getByRole ("button" , {
name: "Look again with same group" ,
}) SendouQMatchPage.requestCancele2e/pages/sendouq/sendouq-match-page.ts:193
No JSDoc
async requestCancel (report: CancelReport ) {
await this .locators.requestCancelButton.click ();
await this .submitCancelDialog (report);
}SendouQMatchPage.locators.cancelPendingTexte2e/pages/sendouq/sendouq-match-page.ts:51
No JSDoc
page.getByText ("Pending other team's confirmation" ) SendouQMatchPage.locators.cancelPrompte2e/pages/sendouq/sendouq-match-page.ts:52
No JSDoc
page.getByText ("Accept canceling the set?" ) SendouQMatchPage.refuseCancele2e/pages/sendouq/sendouq-match-page.ts:198
No JSDoc
async refuseCancel () {
await waitForPOSTResponse (this .page, async () => {
await this .page.getByRole ("button" , { name: "Refuse" }).click ();
});
}captureCancelWebhooke2e/sendouq-match.spec.ts:434
async function captureCancelWebhook (port: number , run: () => Promise <void >) {
const payloads: WebhookPayload [] = [];
const server = http.createServer ((req, res) => {
let body = "" ;
req.on ("data" , (chunk) => {
body += chunk;
});
req.on ("end" , () => {
payloads.push (JSON .parse (body));
res.statusCode = 204 ;
res.end ();
});
});
await new Promise <void >((resolve, reject) => {
// … SendouQMatchPage.locators.canceledTexte2e/pages/sendouq/sendouq-match-page.ts:53
No JSDoc
page.getByText ("Match canceled" ) runRoutinee2e/helpers/playwright.ts:497
export async function runRoutine (page: Page , name: string ) {
const response = await retryPost (page, "/run-routine" , {
form: { name },
});
if (!response?.ok ()) {
throw new Error (
`Running routine ${name} failed with status ${response?.status()}` ,
);
}
}SendouQMatchPage.lookAgaine2e/pages/sendouq/sendouq-match-page.ts:234
No JSDoc
async lookAgain () {
await waitForPOSTResponse (this .page, async () => {
await this .locators.lookAgainButton.click ();
});
}SendouQPage.gotoe2e/pages/sendouq/sendouq-page.ts:23
No JSDoc
async goto () {
await navigate ({ page: this .page, url: SENDOUQ_PAGE });
}SendouQPagee2e/pages/sendouq/sendouq-page.ts:8
No JSDoc
SendouQMatchPage.acceptCancele2e/pages/sendouq/sendouq-match-page.ts:204
No JSDoc
async acceptCancel (report: CancelReport ) {
await this .page.getByRole ("button" , { name: "Accept" }).click ();
await this .submitCancelDialog (report);
}SendouQMatchPage.locators.backToQueueButtone2e/pages/sendouq/sendouq-match-page.ts:58
No JSDoc
page.getByRole ("button" , { name: "Back to queue" }) SendouQLookingPagee2e/pages/sendouq/sendouq-looking-page.ts:13
No JSDoc
SendouQMatchPage.voteNoe2e/pages/sendouq/sendouq-match-page.ts:229
No JSDoc
async voteNo () {
await this .page.getByRole ("button" , { name: "No, I'm done" }).click ();
await this .confirmDialog ();
}SendouQMatchPage.locators.declinedTexte2e/pages/sendouq/sendouq-match-page.ts:59
No JSDoc
page.getByText ("You are not continuing with this group" ) SendouQMatchPage.rejoinQueuee2e/pages/sendouq/sendouq-match-page.ts:240
No JSDoc
async rejoinQueue () {
await waitForPOSTResponse (this .page, async () => {
await this .locators.rejoinQueueButton.click ();
});
}SendouQMatchPage.voteYese2e/pages/sendouq/sendouq-match-page.ts:223
No JSDoc
async voteYes () {
await waitForPOSTResponse (this .page, async () => {
await this .page.getByRole ("button" , { name: "Yes, continue" }).click ();
});
}SendouQMatchPage.locators.votedYese2e/pages/sendouq/sendouq-match-page.ts:61
No JSDoc
page.getByLabel ("voted yes" ) SendouQPage.joinSoloe2e/pages/sendouq/sendouq-page.ts:36
No JSDoc
async joinSolo () {
await submit (this .page, "join-solo-button" );
return new SendouQLookingPage (this .page);
}SendouQMatchPage.locators.votedNoe2e/pages/sendouq/sendouq-match-page.ts:62
No JSDoc
page.getByLabel ("voted no" ) SendouQMatchPage.locators.pendingVotese2e/pages/sendouq/sendouq-match-page.ts:63
No JSDoc
page.getByLabel ("pending" )