test("Changing match preferences cancels pending requests", async ({
page,
factories,
}) => {
const ownGroup = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
const otherGroup = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
await factories.SQGroupFactory.create({
memberUserIds: ownGroup.map((member) => member.id),
});
await factories.SQGroupFactory.create({
memberUserIds: otherGroup.map((member) => member.id),
});
// challenge the other full group as the owner of our own full groupawaitimpersonate(page, ownGroup[0].id);
const looking = newSendouQLookingPage(page);
await looking.goto();
await looking.pressGroupAction();
// the challenge is now pending and can be undoneawaitexpect(looking.locators.undoButtons).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 sawconst matchProfile = newMatchProfilePage(page);
await matchProfile.goto();
await matchProfile.form.check("noScreen");
await matchProfile.save();
// the pending challenge has been undoneawait looking.goto();
awaitexpect(looking.locators.undoButtons).toHaveCount(0);
})
Suggesting floats a group to the top and leaves a trail for the group
test("Suggesting floats a group to the top and leaves a trail for the group", async ({
page,
factories,
}) => {
const owner = await factories.UserFactory.create({
discordName: "Owner",
profile: null,
});
const teammate = await factories.UserFactory.create({
discordName: "Teammate",
profile: null,
});
const target = await factories.UserFactory.create({
discordName: "Target",
profile: null,
});
const other = await factories.UserFactory.create({
discordName: "Other",
profile: null,
});
await factories.SQGroupFactory.create({
memberUserIds: [owner.id, teammate.id],
});
const suggestedGroup = await factories.SQGroupFactory.create({
memberUserIds: [target.id],
});
await factories.SQGroupFactory.create({ memberUserIds: [other.id] });
// the least recently active group sorts last, so the suggestion has somewhere to move it fromawait factories.backdate("Group", suggestedGroup.id, {
latestActionAt: sub(newDate(), { minutes: 10 }),
});
awaitimpersonate(page, owner.id);
const looking = newSendouQLookingPage(page);
await looking.goto();
// the own group's card is always the first oneawaitexpect(looking.groupCard(2).root).toContainText("Target");
await looking.groupCard(2).pressSuggest();
awaitexpect(looking.groupCard(1).root).toContainText("Target");
awaitexpect(looking.groupCard(1).trail).toHaveText("Suggested by Owner");
// a suggestion can't be undone or repeatedawaitisNotVisible(looking.groupCard(1).suggestButton);
// the whole group sees the suggestion, and any of them can act on itawaitimpersonate(page, teammate.id);
await looking.goto();
awaitexpect(looking.groupCard(1).trail).toHaveText("Suggested by Owner");
await looking.groupCard(1).pressAction();
// inviting says everything the suggestion said, so it takes the trail overawaitexpect(looking.groupCard(1).trail).toHaveText("Invited by Teammate");
awaitisNotVisible(looking.groupCard(1).suggestButton);
// a solo queuer has no teammates to suggest anything toawaitimpersonate(page, other.id);
await looking.goto();
awaitisNotVisible(looking.locators.suggestButtons);
})
Previewing the queue shows the groups without any way to act on them
test("Previewing the queue shows the groups without any way to act on them", async ({
page,
factories,
}) => {
const supporter = await factories.UserFactory.create(null, {
patronTier: 2,
});
const [soloQueuer] = await factories.UserFactory.createMany(1);
const fullGroupMembers =
await factories.UserFactory.createMany(FULL_GROUP_SIZE);
await factories.SQGroupFactory.create({ memberUserIds: [soloQueuer.id] });
await factories.SQGroupFactory.create({
memberUserIds: fullGroupMembers.map((member) => member.id),
});
awaitimpersonate(page, supporter.id);
const looking = newSendouQLookingPage(page);
await looking.gotoPreview();
awaitexpect(looking.locators.groupCards).toHaveCount(2);
// without a group of their own there is nothing to invite, challenge or suggest withawaitisNotVisible(looking.locators.actionButtons);
awaitisNotVisible(looking.locators.suggestButtons);
})
Create team member user idsmembers.map((member) => member.id)map mode preferencesmodesmodeTEAM_MODEpreference“PREFER”poolmodeTEAM_MODEstagesTEAM_MAP_POOL.map((map) => map.stageId)
Create SQ ready check alpha group idteamGroup.idbravo group idopponentGroup.idconfirmed by user idmembers[0].idconfirmed by user ids...members.slice(1).map((member) => member.id), ...opponents.slice(0, -1).map((member) => member.id)
test("A team's map preferences decide its modes and the maps it is given", async ({
page,
factories,
}) => {
const members = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
// the opponents want the same mode as the team, so an overlap of exactly it// can only come from the team's own preferences: its members have noneconst opponents = await factories.UserFactory.createMany(
FULL_GROUP_SIZE,
null,
{
matchProfile: {
mapModePreferences: {
modes: [{ mode: TEAM_MODE, preference: "PREFER" }],
pool: [],
},
},
},
);
await factories.TeamFactory.create(
{ memberUserIds: members.map((member) => member.id) },
{
mapModePreferences: {
modes: [{ mode: TEAM_MODE, preference: "PREFER" }],
pool: [
{
mode: TEAM_MODE,
stages: TEAM_MAP_POOL.map((map) => map.stageId),
},
],
},
},
);
const teamGroup = await factories.SQGroupFactory.create({
memberUserIds: members.map((member) => member.id),
});
const opponentGroup = await factories.SQGroupFactory.create({
memberUserIds: opponents.map((member) => member.id),
});
awaitimpersonate(page, opponents[0].id);
const looking = newSendouQLookingPage(page);
await looking.goto();
// the modes the match would be played on are the team's, not its members'awaitexpect(looking.groupCard(1).modes).toHaveCount(1);
awaitexpect(looking.groupCard(1).mode(TEAM_MODE)).toBeVisible();
await factories.SQReadyCheckFactory.create(
{
alphaGroupId: teamGroup.id,
bravoGroupId: opponentGroup.id,
confirmedByUserId: members[0].id,
},
{
confirmedByUserIds: [
...members.slice(1).map((member) => member.id),
...opponents.slice(0, -1).map((member) => member.id),
],
},
);
// the last confirmation is what makes the match, and with it its map listawaitimpersonate(page, opponents.at(-1)!.id);
const ready = newSendouQReadyPage(page);
await ready.goto();
await ready.confirmReady();
awaitexpect(page).toHaveURL(/\/q\/match\/\d+/);
const match = newSendouQMatchPage(page);
awaitexpect(match.modeProgress(TEAM_MODE)).toBeVisible();
awaitexpect(match.mapCountText(SENDOUQ_BEST_OF)).toBeVisible();
awaitexpect(match.currentMap(TEAM_MAP_POOL)).toBeVisible();
})
Ready check flow - both groups confirm and the match starts
for (const member of restOfTheQueue.slice(0, -1)) {
awaitimpersonate(page, member.id);
await ready.goto();
// the ready check notification stays unseen until they respond to itawaitexpect(notifications.locators.bellDot).toBeVisible();
await ready.confirmReady();
awaitexpect(page).toHaveURL(SENDOUQ_READY_PAGE);
awaitexpect(ready.locators.confirmedText).toBeVisible();
awaitexpect(notifications.locators.bellDot).toBeHidden();
}
awaitimpersonate(page, member.id);
Open the SendouQ ready page
await ready.goto();
the ready check notification stays unseen until they respond to it
test("Ready check flow - both groups confirm and the match starts", async ({
page,
factories,
}) => {
// every one of the eight members logs in and confirms on their own
test.slow();
const challengers = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
const accepters = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
const challengerGroup = await factories.SQGroupFactory.create({
memberUserIds: challengers.map((member) => member.id),
});
await factories.SQGroupFactory.create(
{ memberUserIds: accepters.map((member) => member.id) },
{ likedByGroupIds: [challengerGroup.id] },
);
awaitimpersonate(page, accepters[0].id);
const looking = newSendouQLookingPage(page);
await looking.goto();
await looking.pressGroupAction();
// accepting the challenge starts the ready check instead of the matchawaitexpect(page).toHaveURL(SENDOUQ_READY_PAGE);
const ready = newSendouQReadyPage(page);
awaitexpect(ready.locators.countdown).toBeVisible();
// who they will be playing is not revealed yetawaitexpect(ready.locators.hiddenGroupCard).toBeVisible();
// accepting counted as being ready, so it is 1 of the 8awaitexpect(ready.locators.membersReady).toHaveCount(1);
awaitexpect(ready.locators.confirmedText).toBeVisible();
const notifications = newNotificationPopover(page);
const restOfTheQueue = [...accepters.slice(1), ...challengers];
for (const member of restOfTheQueue.slice(0, -1)) {
awaitimpersonate(page, member.id);
await ready.goto();
// the ready check notification stays unseen until they respond to itawaitexpect(notifications.locators.bellDot).toBeVisible();
await ready.confirmReady();
awaitexpect(page).toHaveURL(SENDOUQ_READY_PAGE);
awaitexpect(ready.locators.confirmedText).toBeVisible();
awaitexpect(notifications.locators.bellDot).toBeHidden();
}
// the last one to confirm gets everyone into the matchawaitimpersonate(page, restOfTheQueue.at(-1)!.id);
await ready.goto();
awaitexpect(notifications.locators.bellDot).toBeVisible();
await ready.confirmReady();
awaitexpect(page).toHaveURL(/\/q\/match\/\d+/);
// confirming resolved the ready check notification and the new match// notification arrives already seen for the one who created the matchawaitexpect(notifications.locators.bellDot).toBeHidden();
})
Ready check expiring sends the groups back to looking and lets them kick who missed it
everyone but the last member of the own group confirms
Create SQ ready check alpha group idownGroup.idbravo group idtheirGroup.idconfirmed by user idownMembers[0].idconfirmed by user ids...ownMembers.slice(1, -1).map((member) => member.id), ...theirMembers.map((member) => member.id)→ readyCheck
everyone but the last member of the own group confirmed
Create SQ ready check alpha group idownGroup.idbravo group idtheirGroup.idconfirmed by user idownMembers[0].idconfirmed by user ids...ownMembers.slice(1, -1).map((member) => member.id), ...theirMembers.map((member) => member.id)
there is no rated match left to make, so the ready check ends and both groups leave the queue instead of being sent to play
the page URL is SENDOUQ_PAGE
awaitexpect(page).toHaveURL(SENDOUQ_PAGE);
Source
test("A ready check outliving the season doesn't turn into a match", async ({
page,
factories,
}) => {
const ownMembers = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
const theirMembers =
await factories.UserFactory.createMany(FULL_GROUP_SIZE);
const ownGroup = await factories.SQGroupFactory.create({
memberUserIds: ownMembers.map((member) => member.id),
});
const theirGroup = await factories.SQGroupFactory.create({
memberUserIds: theirMembers.map((member) => member.id),
});
// everyone but the last member of the own group confirmedawait factories.SQReadyCheckFactory.create(
{
alphaGroupId: ownGroup.id,
bravoGroupId: theirGroup.id,
confirmedByUserId: ownMembers[0].id,
},
{
confirmedByUserIds: [
...ownMembers.slice(1, -1).map((member) => member.id),
...theirMembers.map((member) => member.id),
],
},
);
awaitimpersonate(page, ownMembers.at(-1)!.id);
const ready = newSendouQReadyPage(page);
await ready.goto();
// the season ends before the last one confirmsawaitendSeason(page);
await ready.confirmReady();
// there is no rated match left to make, so the ready check ends and both// groups leave the queue instead of being sent to playawaitexpect(page).toHaveURL(SENDOUQ_PAGE);
})
The season ending takes the groups out of the queue
test("The season ending takes the groups out of the queue", async ({
page,
factories,
}) => {
const members = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
await factories.SQGroupFactory.create({
memberUserIds: members.map((member) => member.id),
});
awaitimpersonate(page, members[0].id);
const looking = newSendouQLookingPage(page);
await looking.goto();
awaitexpect(looking.ownGroupCard.members).toHaveCount(FULL_GROUP_SIZE);
awaitendSeason(page);
// the group is taken out of the queue on the spot, so there is no// looking page left to be onawait looking.goto();
awaitexpect(page).toHaveURL(SENDOUQ_PAGE);
// and no way back in until the next season startsconst q = newSendouQPage(page);
awaitisNotVisible(q.locators.joinWithMatesButton);
})
Joining the queue is blocked when the season's initial powers were never seeded
text “Season's starting powers are not set yet” is in the page
awaitexpect(
page.getByText("Season's starting powers are not set yet"),
).toBeAttached();
the page URL is SENDOUQ_PAGE
awaitexpect(page).toHaveURL(SENDOUQ_PAGE);
Source
test("Joining the queue is blocked when the season's initial powers were never seeded", async ({
page,
factories,
}) => {
const [user] = await factories.UserFactory.createMany(1);
// the previous season concluded with skills but the current one has none,// meaning season-initial-powers was forgottenawait factories.SkillFactory.create({ userId: user.id, season: 0 });
awaitimpersonate(page, user.id);
const q = newSendouQPage(page);
await q.goto();
await q.joinSolo();
awaitexpect(
page.getByText("Season's starting powers are not set yet"),
).toBeAttached();
awaitexpect(page).toHaveURL(SENDOUQ_PAGE);
})
for (const [index, user] of otherUsers.entries()) {
const group = await factories.SQGroupFactory.create({
memberUserIds: [user.id],
});
// the least recently active group sorts last, giving the suggestion// the whole page to move the card upawait factories.backdate("Group", group.id, {
latestActionAt: sub(newDate(), { minutes: index }),
});
}
Create SQ group member user idsuser.id→ group
const group = await factories.SQGroupFactory.create({
memberUserIds: [user.id],
});
the least recently active group sorts last, giving the suggestion the whole page to move the card up
Backdate “Group”group.idlatest action atsub(newDate(), { minutes: index })
The scroll position the page was at when it was last pressed, for asserting that an action did not move the viewer. Playwright scrolls a click target into view itself, and a retried click force-scrolls it again, so a reading taken before the press can be stale by the time the press lands.
The notification bell of the layout, and the popover it opens.
runRoutinee2e/helpers/playwright.ts:497
Runs the named server Routine (normally cron-driven) in the worker's server process.
exportasyncfunctionrunRoutine(page: Page, name: string) {
const response = awaitretryPost(page, "/run-routine", {
form: { name },
});
if (!response?.ok()) {
thrownewError(
`Running routine ${name} failed with status ${response?.status()}`,
);
}
}
endSeasone2e/helpers/playwright.ts:475
Makes the worker's server resolve every season as over. Undone before the next test starts.
exportasyncfunctionendSeason(page: Page) {
const response = awaitretryPost(page, "/end-season");
if (!response?.ok()) {
thrownewError(
`Ending the season failed with status ${response?.status()}`,
);
}
}