User page Renders/u/:identifier /u/:identifier
User page Arrange
Create badge display name “Sunken Scroll” owner ids N-ZAP
await factories.BadgeFactory .create (
{ displayName: "Sunken Scroll" },
{ ownerIds: [NZAP_TEST_ID ] },
);enough badges for two pages: 1 big + 9 small on the first page
Create 12 × badge (index) => ({ displayName: `Badge ${index + 1}` }) owner ids Sendou
await factories.BadgeFactory .createMany (
12 ,
(index) => ({ displayName: `Badge ${index + 1}` }),
{ ownerIds: [ADMIN_ID ] },
);User page Act
Open the user page N-ZAP
await userPage.goto (NZAP_TEST_ID );Assert
badge display is visible
await expect (userPage.locators.badgeDisplay).toBeVisible ();badge pagination buttons is not visible
await isNotVisible (userPage.locators.badgePaginationButtons);Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Assert
badges are shown newest first, so the last created one is the big badge
badge image “Badge 12” is visible
await expect (userPage.badgeImage ("Badge 12" )).toBeVisible ();Act
Click badge pagination buttons #1
await userPage.locators.badgePaginationButtons.nth (1 ).click ();Click badge image “Badge 1”
await userPage.badgeImage ("Badge 1" ).click ();Assert
badge image “Badge 1” has “width” = “125”
await expect (userPage.badgeImage ("Badge 1" )).toHaveAttribute (
"width" ,
"125" ,
);Source test ("uses badge pagination" , async ({ page, factories }) => {
await factories.BadgeFactory .create (
{ displayName: "Sunken Scroll" },
{ ownerIds: [NZAP_TEST_ID ] },
);
// enough badges for two pages: 1 big + 9 small on the first page
await factories.BadgeFactory .createMany (
12 ,
(index) => ({ displayName: `Badge ${index + 1}` }),
{ ownerIds: [ADMIN_ID ] },
);
const userPage = new UserPage (page);
await userPage.goto (NZAP_TEST_ID );
await expect (userPage.locators.badgeDisplay).toBeVisible ();
await isNotVisible (userPage.locators.badgePaginationButtons);
await userPage.goto (ADMIN_ID );
// badges are shown newest first, so the last created one is the big badge
await expect (userPage.badgeImage ("Badge 12" )).toBeVisible ();
await userPage.locators.badgePaginationButtons.nth (1 ).click ();
await userPage.badgeImage ("Badge 1" ).click ();
await expect (userPage.badgeImage ("Badge 1" )).toHaveAttribute (
"width" ,
"125" ,
);
})customize which badge is shown as big by default as normal user Renders/u/:identifier /u/:identifier /u/:identifier/edit-widgets
User edit widgets page User page Arrange
Create 2 × badge (index) => ({ displayName: `Badge ${index + 1}` }) owner ids N-ZAP → [firstBadge]
const [firstBadge] = await factories.BadgeFactory .createMany (
2 ,
(index) => ({ displayName: `Badge ${index + 1}` }),
{ ownerIds: [NZAP_TEST_ID ] },
);User edit widgets page Act
Open the user edit widgets page N-ZAP
await editWidgets.goto (NZAP_TEST_ID );Open widget settings “badges-owned”
await editWidgets.openWidgetSettings ("badges-owned" );Select favorite badge firstBadge.id
await editWidgets.selectFavoriteBadge (firstBadge.id);Save
await editWidgets.save ();User page Act
Open the user page N-ZAP
await userPage.goto (NZAP_TEST_ID );Assert
badge image “Badge 1” has “width” = “125”
await expect (userPage.badgeImage ("Badge 1" )).toHaveAttribute (
"width" ,
"125" ,
);Source test ("customize which badge is shown as big by default as normal user" , async ({
page,
factories,
}) => {
const [firstBadge] = await factories.BadgeFactory .createMany (
2 ,
(index) => ({ displayName: `Badge ${index + 1}` }),
{ ownerIds: [NZAP_TEST_ID ] },
);
await impersonate (page, NZAP_TEST_ID );
const editWidgets = new UserEditWidgetsPage (page);
await editWidgets.goto (NZAP_TEST_ID );
await editWidgets.openWidgetSettings ("badges-owned" );
await editWidgets.selectFavoriteBadge (firstBadge.id);
await editWidgets.save ();
const userPage = new UserPage (page);
await userPage.goto (NZAP_TEST_ID );
await expect (userPage.badgeImage ("Badge 1" )).toHaveAttribute (
"width" ,
"125" ,
);
})customize big badge + small badge first page order as supporter Renders/u/:identifier /u/:identifier /u/:identifier/edit-widgets
User edit widgets page User page Arrange
Create 3 × badge (index) => ({ displayName: `Badge ${index + 1}` }) owner ids Sendou → badges
const badges = await factories.BadgeFactory .createMany (
3 ,
(index) => ({ displayName: `Badge ${index + 1}` }),
{ ownerIds: [ADMIN_ID ] },
);Grant user Sendou patron tier 2
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });User edit widgets page Act
Open the user edit widgets page Sendou
await editWidgets.goto (ADMIN_ID );Open widget settings “badges-owned”
await editWidgets.openWidgetSettings ("badges-owned" );Select favorite badge badges[0 ].id
await editWidgets.selectFavoriteBadge (badges[0 ].id);Assert
badge display is visible
await expect (editWidgets.locators.badgeDisplay).toBeVisible ();Act
Select favorite badge badges[1 ].id
await editWidgets.selectFavoriteBadge (badges[1 ].id);Save
await editWidgets.save ();User page Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Assert
badge image “Badge 1” has “width” = “125”
await expect (userPage.badgeImage ("Badge 1" )).toHaveAttribute (
"width" ,
"125" ,
);badge image “Badge 2” is visible
await expect (userPage.badgeImage ("Badge 2" )).toBeVisible ();Source test ("customize big badge + small badge first page order as supporter" , async ({
page,
factories,
}) => {
const badges = await factories.BadgeFactory .createMany (
3 ,
(index) => ({ displayName: `Badge ${index + 1}` }),
{ ownerIds: [ADMIN_ID ] },
);
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });
await impersonate (page);
const editWidgets = new UserEditWidgetsPage (page);
await editWidgets.goto (ADMIN_ID );
await editWidgets.openWidgetSettings ("badges-owned" );
await editWidgets.selectFavoriteBadge (badges[0 ].id);
await expect (editWidgets.locators.badgeDisplay).toBeVisible ();
await editWidgets.selectFavoriteBadge (badges[1 ].id);
await editWidgets.save ();
const userPage = new UserPage (page);
await userPage.goto (ADMIN_ID );
await expect (userPage.badgeImage ("Badge 1" )).toHaveAttribute (
"width" ,
"125" ,
);
await expect (userPage.badgeImage ("Badge 2" )).toBeVisible ();
})Renders/u/:identifier /u/:identifier /u/:identifier/edit
User page User edit profile page User page Arrange
Update profile user Sendou country “FI”
await factories.UserFactory .updateProfile (ADMIN_ID , { country: "FI" });User page Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Assert
flag “FI” is visible
await expect (userPage.flag ("FI" )).toBeVisible ();Act
Open edit profile → editProfile
const editProfile = await userPage.openEditProfile ();User edit profile page Act
Fill In-game name with “Lean#1234”
await editProfile.form.fill ("inGameName" , "Lean#1234" );Select country “Sweden”
await editProfile.selectCountry ("Sweden" );Save
await editProfile.save ();User page Assert
flag “SE” is visible
await expect (userPage.flag ("SE" )).toBeVisible ();text “Lean#1234” is visible
await expect (userPage.text ("Lean#1234" )).toBeVisible ();Source test ("edits user profile" , async ({ page, factories }) => {
await factories.UserFactory .updateProfile (ADMIN_ID , { country: "FI" });
await impersonate (page);
const userPage = new UserPage (page);
await userPage.goto (ADMIN_ID );
await expect (userPage.flag ("FI" )).toBeVisible ();
const editProfile = await userPage.openEditProfile ();
await editProfile.form.fill ("inGameName" , "Lean#1234" );
await editProfile.selectCountry ("Sweden" );
await editProfile.save ();
await expect (userPage.flag ("SE" )).toBeVisible ();
await expect (userPage.text ("Lean#1234" )).toBeVisible ();
})sets friend code from the edit profile page Renders/u/:identifier /u/:identifier/edit
User edit profile page User edit profile page Act
Open the user edit profile page N-ZAP
await editProfile.goto (NZAP_TEST_ID );Set friend code “0123-4567-8901”
await editProfile.setFriendCode ("0123-4567-8901" );Assert
friend code button “0123-4567-8901” is visible
await expect (editProfile.friendCodeButton ("0123-4567-8901" )).toBeVisible ();Source test ("sets friend code from the edit profile page" , async ({ page }) => {
await impersonate (page, NZAP_TEST_ID );
const editProfile = new UserEditProfilePage (page);
await editProfile.goto (NZAP_TEST_ID );
await editProfile.setFriendCode ("0123-4567-8901" );
await expect (editProfile.friendCodeButton ("0123-4567-8901" )).toBeVisible ();
})customizes theme colors and resets them Renders/settings
Settings page Arrange
custom theme colors are a supporter perk
Grant user Sendou patron tier 2
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });Settings page Act
Open the settings page “theme”
await settings.goto ("theme" );Assert
has custom theme is false
await expect (settings.hasCustomTheme ()).resolves.toBe (false );Act
Set base hue “120”
await settings.setBaseHue ("120" );Save theme
await settings.saveTheme ();Reload
await settings.reload ();Assert
has custom theme is true
await expect (settings.hasCustomTheme ()).resolves.toBe (true );Act
Reset theme
await settings.resetTheme ();Reload
await settings.reload ();Assert
has custom theme is false
await expect (settings.hasCustomTheme ()).resolves.toBe (false );Source test ("customizes theme colors and resets them" , async ({
page,
factories,
}) => {
// custom theme colors are a supporter perk
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });
await impersonate (page);
const settings = new SettingsPage (page);
await settings.goto ("theme" );
await expect (settings.hasCustomTheme ()).resolves.toBe (false );
await settings.setBaseHue ("120" );
await settings.saveTheme ();
await settings.reload ();
await expect (settings.hasCustomTheme ()).resolves.toBe (true );
await settings.resetTheme ();
await settings.reload ();
await expect (settings.hasCustomTheme ()).resolves.toBe (false );
})exports season summary image as a supporter Renders/u/:identifier /u/:identifier/seasons /u/:identifier/seasons
User seasons page Arrange
Grant user Sendou patron tier 2
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });Play finished season Sendou
await playFinishedSeason (factories, ADMIN_ID );User seasons page Act
Open the user seasons page Sendou
await seasonsPage.goto (ADMIN_ID );Open export dialog
await seasonsPage.openExportDialog ();Assert
export dialog text “Best win streak” is visible
await expect (seasonsPage.exportDialogText ("Best win streak" )).toBeVisible ();Act
Download exported image → download
const download = await seasonsPage.downloadExportedImage ();Assert
download.suggestedFilename () is `season-${FINISHED_SEASON}-summary.png`
expect (download.suggestedFilename ()).toBe (
`season-${FINISHED_SEASON}-summary.png` ,
);Source test ("exports season summary image as a supporter" , async ({
page,
factories,
}) => {
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });
await playFinishedSeason (factories, ADMIN_ID );
await impersonate (page);
const seasonsPage = new UserSeasonsPage (page);
await seasonsPage.goto (ADMIN_ID );
await seasonsPage.openExportDialog ();
await expect (seasonsPage.exportDialogText ("Best win streak" )).toBeVisible ();
const download = await seasonsPage.downloadExportedImage ();
expect (download.suggestedFilename ()).toBe (
`season-${FINISHED_SEASON}-summary.png` ,
);
})shows supporter perk explanation instead of exporting for non-supporter mid-season Renders/u/:identifier /u/:identifier/seasons /u/:identifier/seasons
User seasons page Arrange
Play finished season N-ZAP
await playFinishedSeason (factories, NZAP_TEST_ID );User seasons page Act
Open the user seasons page N-ZAP
await seasonsPage.goto (NZAP_TEST_ID );Open export dialog
await seasonsPage.openExportDialog ();Assert
supporter perk explanation is visible
await expect (seasonsPage.locators.supporterPerkExplanation).toBeVisible ();download button is not visible
await isNotVisible (seasonsPage.locators.downloadButton);Source test ("shows supporter perk explanation instead of exporting for non-supporter mid-season" , async ({
page,
factories,
}) => {
await playFinishedSeason (factories, NZAP_TEST_ID );
await impersonate (page, NZAP_TEST_ID );
const seasonsPage = new UserSeasonsPage (page);
await seasonsPage.goto (NZAP_TEST_ID );
await seasonsPage.openExportDialog ();
await expect (seasonsPage.locators.supporterPerkExplanation).toBeVisible ();
await isNotVisible (seasonsPage.locators.downloadButton);
})shows the match profile weapon pool Renders/u/:identifier /u/:identifier
User page Arrange
Grant user Sendou match profile weapon pool ([200 , 1100 , 2000 , 4000 ] as const ).map ((id) => ({ id, isFavorite: false , }))
await factories.UserFactory .grant (ADMIN_ID , {
matchProfile: {
weaponPool: ([200 , 1100 , 2000 , 4000 ] as const ).map ((id) => ({
id,
isFavorite: false ,
})),
},
});User page Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Assert
For each [i, id] of [200 , 1100 , 2000 , 4000 ].entries ()
for (const [i, id] of [200 , 1100 , 2000 , 4000 ].entries ()) {
await expect (userPage.weaponPoolImage (id, i + 1 )).toBeVisible ();
}weapon pool image id, i + 1 is visible
await expect (userPage.weaponPoolImage (id, i + 1 )).toBeVisible ();Source test ("shows the match profile weapon pool" , async ({ page, factories }) => {
await factories.UserFactory .grant (ADMIN_ID , {
matchProfile: {
weaponPool: ([200 , 1100 , 2000 , 4000 ] as const ).map ((id) => ({
id,
isFavorite: false ,
})),
},
});
await impersonate (page);
const userPage = new UserPage (page);
await userPage.goto (ADMIN_ID );
for (const [i, id] of [200 , 1100 , 2000 , 4000 ].entries ()) {
await expect (userPage.weaponPoolImage (id, i + 1 )).toBeVisible ();
}
})shows the weapon pool widget's own list over the match profile pool Renders/u/:identifier /u/:identifier /u/:identifier/edit-widgets
User edit widgets page User page Arrange
Grant user Sendou match profile weapon pool id 1100 is favorite false
await factories.UserFactory .grant (ADMIN_ID , {
matchProfile: {
weaponPool: [{ id: 1100 , isFavorite: false }],
},
});User edit widgets page Act
Open the user edit widgets page Sendou
await editWidgetsPage.goto (ADMIN_ID );Open widget settings “weapon-pool”
await editWidgetsPage.openWidgetSettings ("weapon-pool" );Select weapon pool weapon “Luna Blaster”
await editWidgetsPage.selectWeaponPoolWeapon ("Luna Blaster" );Select weapon pool weapon “Splattershot”
await editWidgetsPage.selectWeaponPoolWeapon ("Splattershot" );Save
await editWidgetsPage.save ();User page Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Assert
weapon pool image 200 , 1 is visible
await expect (userPage.weaponPoolImage (200 , 1 )).toBeVisible ();weapon pool image 40 , 2 is visible
await expect (userPage.weaponPoolImage (40 , 2 )).toBeVisible ();weapon pool image 1100 , 1 is not visible
await isNotVisible (userPage.weaponPoolImage (1100 , 1 ));Source test ("shows the weapon pool widget's own list over the match profile pool" , async ({
page,
factories,
}) => {
await factories.UserFactory .grant (ADMIN_ID , {
matchProfile: {
weaponPool: [{ id: 1100 , isFavorite: false }],
},
});
await impersonate (page);
const editWidgetsPage = new UserEditWidgetsPage (page);
await editWidgetsPage.goto (ADMIN_ID );
await editWidgetsPage.openWidgetSettings ("weapon-pool" );
await editWidgetsPage.selectWeaponPoolWeapon ("Luna Blaster" );
await editWidgetsPage.selectWeaponPoolWeapon ("Splattershot" );
await editWidgetsPage.save ();
const userPage = new UserPage (page);
await userPage.goto (ADMIN_ID );
await expect (userPage.weaponPoolImage (200 , 1 )).toBeVisible ();
await expect (userPage.weaponPoolImage (40 , 2 )).toBeVisible ();
await isNotVisible (userPage.weaponPoolImage (1100 , 1 ));
})chooses result highlights which the results list then shows by default Renders/u/:identifier /u/:identifier /u/:identifier/results /u/:identifier/results/highlights
User page User results page User results highlights page User results page Arrange
Create calendar event author Sendou name “In The Zone 30” → zonesEvent
const zonesEvent = await factories.CalendarEventFactory .create ({
authorId: ADMIN_ID ,
name: "In The Zone 30" ,
});Create calendar event author Sendou name “Paddling Pool 253” → poolEvent
const poolEvent = await factories.CalendarEventFactory .create ({
authorId: ADMIN_ID ,
name: "Paddling Pool 253" ,
});For each [event, placement] of zonesEvent, 2 , poolEvent, 1
for (const [event, placement] of [
[zonesEvent, 2 ],
[poolEvent, 1 ],
] as const ) {
await factories.CalendarEventResultFactory .create ({
eventId: event.id,
participantCount: 16 ,
results: [
{
teamName: "Team Olive" ,
placement,
players: [
{ userId: ADMIN_ID , name: null },
{ userId: null , name: "Mako" },
{ userId: null , name: "Marie" },
{ userId: null , name: "Callie" },
],
},
],
});
}Create calendar event result event id event.id participant count 16 results team name “Team Olive” placement players user Sendou name null , user id null name “Mako” , user id null name “Marie” , user id null name “Callie”
await factories.CalendarEventResultFactory .create ({
eventId: event.id,
participantCount: 16 ,
results: [
{
teamName: "Team Olive" ,
placement,
players: [
{ userId: ADMIN_ID , name: null },
{ userId: null , name: "Mako" },
{ userId: null , name: "Marie" },
{ userId: null , name: "Callie" },
],
},
],
});User page Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Open results → resultsPage
const resultsPage = await userPage.openResults ();User results page Assert
event name “In The Zone 30” is visible
await expect (resultsPage.eventName ("In The Zone 30" )).toBeVisible ();event name “Paddling Pool 253” is visible
await expect (resultsPage.eventName ("Paddling Pool 253" )).toBeVisible ();Act
Open choose highlights → highlightsPage
const highlightsPage = await resultsPage.openChooseHighlights ();User results highlights page Act
Check result checkbox /In The Zone 30 /
await highlightsPage.resultCheckbox (/In The Zone 30 /).check ();Save
await highlightsPage.save ();User results page Assert
event name “In The Zone 30” is visible
await expect (resultsPage.eventName ("In The Zone 30" )).toBeVisible ();event name “Paddling Pool 253” is not visible
await isNotVisible (resultsPage.eventName ("Paddling Pool 253" ));Act
Clear cookies
await page.context ().clearCookies ();Open the user results page Sendou
await resultsPage.goto (ADMIN_ID );Assert
event name “Paddling Pool 253” is not visible
await isNotVisible (resultsPage.eventName ("Paddling Pool 253" ));Act
Toggle highlights only
await resultsPage.toggleHighlightsOnly ();Assert
event name “Paddling Pool 253” is visible
await expect (resultsPage.eventName ("Paddling Pool 253" )).toBeVisible ();Act
Toggle highlights only
await resultsPage.toggleHighlightsOnly ();Assert
event name “Paddling Pool 253” is not visible
await isNotVisible (resultsPage.eventName ("Paddling Pool 253" ));Source test ("chooses result highlights which the results list then shows by default" , async ({
page,
factories,
}) => {
const zonesEvent = await factories.CalendarEventFactory .create ({
authorId: ADMIN_ID ,
name: "In The Zone 30" ,
});
const poolEvent = await factories.CalendarEventFactory .create ({
authorId: ADMIN_ID ,
name: "Paddling Pool 253" ,
});
for (const [event, placement] of [
[zonesEvent, 2 ],
[poolEvent, 1 ],
] as const ) {
await factories.CalendarEventResultFactory .create ({
eventId: event.id,
participantCount: 16 ,
results: [
{
teamName: "Team Olive" ,
placement,
players: [
{ userId: ADMIN_ID , name: null },
{ userId: null , name: "Mako" },
{ userId: null , name: "Marie" },
{ userId: null , name: "Callie" },
],
},
],
});
}
await impersonate (page);
const userPage = new UserPage (page);
await userPage.goto (ADMIN_ID );
const resultsPage = await userPage.openResults ();
await expect (resultsPage.eventName ("In The Zone 30" )).toBeVisible ();
await expect (resultsPage.eventName ("Paddling Pool 253" )).toBeVisible ();
const highlightsPage = await resultsPage.openChooseHighlights ();
await highlightsPage.resultCheckbox (/In The Zone 30 /).check ();
await highlightsPage.save ();
await expect (resultsPage.eventName ("In The Zone 30" )).toBeVisible ();
await isNotVisible (resultsPage.eventName ("Paddling Pool 253" ));
await page.context ().clearCookies ();
await resultsPage.goto (ADMIN_ID );
await isNotVisible (resultsPage.eventName ("Paddling Pool 253" ));
await resultsPage.toggleHighlightsOnly ();
await expect (resultsPage.eventName ("Paddling Pool 253" )).toBeVisible ();
await resultsPage.toggleHighlightsOnly ();
await isNotVisible (resultsPage.eventName ("Paddling Pool 253" ));
})edits profile widgets, lists vods and shows season stats Renders/u/:identifier /u/:identifier /u/:identifier/edit-widgets /u/:identifier/seasons /u/:identifier/seasons /u/:identifier/seasons/stats /u/:identifier/vods
User page User edit widgets page User page User vods page User seasons page Arrange
Create 2 × vod (index) => ({ submitterUserId: ADMIN_ID , pov: { type : "USER" as const , userId…
await factories.VodFactory .createMany (2 , (index) => ({
submitterUserId: ADMIN_ID ,
pov: { type : "USER" as const , userId: ADMIN_ID },
title: `Ranked grind episode ${index + 1}` ,
}));Create FULL_GROUP_SIZE - 2 × user → mates
const mates = await factories.UserFactory .createMany (FULL_GROUP_SIZE - 2 );Create FULL_GROUP_SIZE × user → enemies
const enemies = await factories.UserFactory .createMany (FULL_GROUP_SIZE );Create SQ match alpha user ids Sendou , N-ZAP , ...mates.map ((mate) => mate.id) bravo user ids enemies.map ((enemy) => enemy.id) map list Array .from ({ length: SENDOUQ_BEST_OF }, () => ({ mode: "SZ" as const , stageId… is concluded true → match
const match = await factories.SQMatchFactory .create (
{
alphaUserIds: [ADMIN_ID , NZAP_TEST_ID , ...mates.map ((mate) => mate.id)],
bravoUserIds: enemies.map ((enemy) => enemy.id),
mapList: Array .from ({ length: SENDOUQ_BEST_OF }, () => ({
mode: "SZ" as const ,
stageId: SCORCH_GORGE ,
source: "BOTH" as const ,
})),
},
{ isConcluded: true },
);Create PLAYED_MAPS_COUNT × SQ reported weapon (index) => ({ groupMatchId: match.id, mapIndex: index, userId: ADMIN_ID , weap…
await factories.SQReportedWeaponFactory .createMany (
PLAYED_MAPS_COUNT ,
(index) => ({
groupMatchId: match.id,
mapIndex: index,
userId: ADMIN_ID ,
weaponSplId: LUNA_BLASTER ,
}),
);User page Act
Open the user page Sendou
await userPage.goto (ADMIN_ID );Assert
no team, so the default layout's teams widget has nothing to show
widget heading “Teams” is not visible
await isNotVisible (userPage.widgetHeading ("Teams" ));Act
Open edit widgets → editWidgets
const editWidgets = await userPage.openEditWidgets ();User edit widgets page Act
the default layout is what an untouched profile starts editing from
Open widget settings “bio”
await editWidgets.openWidgetSettings ("bio" );Fill bio “Reformed Hydra main”
await editWidgets.fillBio ("Reformed Hydra main" );Save
await editWidgets.save ();User page Assert
widget heading “Bio” is visible
await expect (userPage.widgetHeading ("Bio" )).toBeVisible ();text “Reformed Hydra main” is visible
await expect (userPage.text ("Reformed Hydra main" )).toBeVisible ();widget heading “Member #” is visible
await expect (userPage.widgetHeading ("Member #" )).toBeVisible ();admin is the first user created, so their join order is 1
exact text “#1” is visible
await expect (userPage.exactText ("#1" )).toBeVisible ();Act
Open vods → vodsPage
const vodsPage = await userPage.openVods ();User vods page Assert
vod title “Ranked grind episode 1” is visible
await expect (vodsPage.vodTitle ("Ranked grind episode 1" )).toBeVisible ();vod title “Ranked grind episode 2” is visible
await expect (vodsPage.vodTitle ("Ranked grind episode 2" )).toBeVisible ();User seasons page Act
Open the user seasons page Sendou
await seasonsPage.goto (ADMIN_ID );Open stats
await seasonsPage.openStats ();Open stats tab “Weapons”
await seasonsPage.openStatsTab ("Weapons" );Assert
weapon usage image “Luna Blaster” is visible
await expect (seasonsPage.weaponUsageImage ("Luna Blaster" )).toBeVisible ();weapon usage share `${PLAYED_MAPS_COUNT} maps · 100%` is visible
await expect (
seasonsPage.weaponUsageShare (`${PLAYED_MAPS_COUNT} maps · 100%` ),
).toBeVisible ();Act
Open stats tab “Stages”
await seasonsPage.openStatsTab ("Stages" );Assert
stage record `${PLAYED_MAPS_COUNT}–0` is visible
await expect (
seasonsPage.stageRecord (`${PLAYED_MAPS_COUNT}–0` ),
).toBeVisible ();Act
Open stats tab “Teammates”
await seasonsPage.openStatsTab ("Teammates" );Assert
player link “N-ZAP” is visible
await expect (seasonsPage.playerLink ("N-ZAP" )).toBeVisible ();Source test ("edits profile widgets, lists vods and shows season stats" , async ({
page,
factories,
}) => {
await factories.VodFactory .createMany (2 , (index) => ({
submitterUserId: ADMIN_ID ,
pov: { type : "USER" as const , userId: ADMIN_ID },
title: `Ranked grind episode ${index + 1}` ,
}));
const mates = await factories.UserFactory .createMany (FULL_GROUP_SIZE - 2 );
const enemies = await factories.UserFactory .createMany (FULL_GROUP_SIZE );
const match = await factories.SQMatchFactory .create (
{
alphaUserIds: [ADMIN_ID , NZAP_TEST_ID , ...mates.map ((mate) => mate.id)],
bravoUserIds: enemies.map ((enemy) => enemy.id),
mapList: Array .from ({ length: SENDOUQ_BEST_OF }, () => ({
mode: "SZ" as const ,
stageId: SCORCH_GORGE ,
source: "BOTH" as const ,
})),
},
{ isConcluded: true },
);
await factories.SQReportedWeaponFactory .createMany (
PLAYED_MAPS_COUNT ,
(index) => ({
groupMatchId: match.id,
mapIndex: index,
userId: ADMIN_ID ,
weaponSplId: LUNA_BLASTER ,
}),
);
await impersonate (page);
const userPage = new UserPage (page);
await userPage.goto (ADMIN_ID );
// no team, so the default layout's teams widget has nothing to show
await isNotVisible (userPage.widgetHeading ("Teams" ));
const editWidgets = await userPage.openEditWidgets ();
// the default layout is what an untouched profile starts editing from
await editWidgets.openWidgetSettings ("bio" );
await editWidgets.fillBio ("Reformed Hydra main" );
await editWidgets.save ();
await expect (userPage.widgetHeading ("Bio" )).toBeVisible ();
await expect (userPage.text ("Reformed Hydra main" )).toBeVisible ();
await expect (userPage.widgetHeading ("Member #" )).toBeVisible ();
// admin is the first user created, so their join order is 1
await expect (userPage.exactText ("#1" )).toBeVisible ();
const vodsPage = await userPage.openVods ();
await expect (vodsPage.vodTitle ("Ranked grind episode 1" )).toBeVisible ();
await expect (vodsPage.vodTitle ("Ranked grind episode 2" )).toBeVisible ();
const seasonsPage = new UserSeasonsPage (page);
await seasonsPage.goto (ADMIN_ID );
await seasonsPage.openStats ();
await seasonsPage.openStatsTab ("Weapons" );
await expect (seasonsPage.weaponUsageImage ("Luna Blaster" )).toBeVisible ();
await expect (
seasonsPage.weaponUsageShare (`${PLAYED_MAPS_COUNT} maps · 100%` ),
).toBeVisible ();
await seasonsPage.openStatsTab ("Stages" );
await expect (
seasonsPage.stageRecord (`${PLAYED_MAPS_COUNT}–0` ),
).toBeVisible ();
await seasonsPage.openStatsTab ("Teammates" );
await expect (seasonsPage.playerLink ("N-ZAP" )).toBeVisible ();
})gates supporter only widgets behind supporter status Renders/u/:identifier /u/:identifier /u/:identifier/edit-widgets
User edit widgets page User page User edit widgets page Act
Open the user edit widgets page Sendou
await editWidgets.goto (ADMIN_ID );Assert
supporter only label “bio-md” is visible
await expect (editWidgets.supporterOnlyLabel ("bio-md" )).toBeVisible ();add widget button “bio-md” is not visible
await isNotVisible (editWidgets.addWidgetButton ("bio-md" ));Arrange
Grant user Sendou patron tier 2
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });Act
Open the user edit widgets page Sendou
await editWidgets.goto (ADMIN_ID );Assert
supporter only label “bio-md” is not visible
await isNotVisible (editWidgets.supporterOnlyLabel ("bio-md" ));Act
Remove widget “bio”
await editWidgets.removeWidget ("bio" );Add widget “bio-md”
await editWidgets.addWidget ("bio-md" );Fill bio “**Reformed** Hydra main”
await editWidgets.fillBio ("**Reformed** Hydra main" );Save
await editWidgets.save ();User page Assert
widget “bio-md” › “strong” reads “Reformed”
await expect (userPage.widget ("bio-md" ).locator ("strong" )).toHaveText (
"Reformed" ,
);Source test ("gates supporter only widgets behind supporter status" , async ({
page,
factories,
}) => {
await impersonate (page);
const editWidgets = new UserEditWidgetsPage (page);
await editWidgets.goto (ADMIN_ID );
await expect (editWidgets.supporterOnlyLabel ("bio-md" )).toBeVisible ();
await isNotVisible (editWidgets.addWidgetButton ("bio-md" ));
await factories.UserFactory .grant (ADMIN_ID , { patronTier: 2 });
await editWidgets.goto (ADMIN_ID );
await isNotVisible (editWidgets.supporterOnlyLabel ("bio-md" ));
await editWidgets.removeWidget ("bio" );
await editWidgets.addWidget ("bio-md" );
await editWidgets.fillBio ("**Reformed** Hydra main" );
await editWidgets.save ();
const userPage = new UserPage (page);
await expect (userPage.widget ("bio-md" ).locator ("strong" )).toHaveText (
"Reformed" ,
);
})redirects to the preferred identifier Renders/u/:identifier /u/:identifier
User page Arrange
customUrl = "zapfish"
const customUrl = "zapfish" ;Update profile user N-ZAP customUrl
await factories.UserFactory .updateProfile (NZAP_TEST_ID , { customUrl });User page Act
Go to with identifier N-ZAP
await userPage.gotoWithIdentifier (NZAP_TEST_ID );Assert
the page URL is `/u/${customUrl}`
await expect (page).toHaveURL (`/u/${customUrl}` );Act
Go to with identifier NZAP_TEST_DISCORD_ID
await userPage.gotoWithIdentifier (NZAP_TEST_DISCORD_ID );Assert
the page URL is `/u/${customUrl}`
await expect (page).toHaveURL (`/u/${customUrl}` );Act
Go to with identifier customUrl
await userPage.gotoWithIdentifier (customUrl);Assert
the page URL is `/u/${customUrl}`
await expect (page).toHaveURL (`/u/${customUrl}` );Act
without a custom URL the user id is the preferred identifier
Go to with identifier ADMIN_DISCORD_ID
await userPage.gotoWithIdentifier (ADMIN_DISCORD_ID );Assert
the page URL is `/u/${ADMIN_ID}`
await expect (page).toHaveURL (`/u/${ADMIN_ID}` );Source test ("redirects to the preferred identifier" , async ({ page, factories }) => {
const customUrl = "zapfish" ;
await factories.UserFactory .updateProfile (NZAP_TEST_ID , { customUrl });
const userPage = new UserPage (page);
await userPage.gotoWithIdentifier (NZAP_TEST_ID );
await expect (page).toHaveURL (`/u/${customUrl}` );
await userPage.gotoWithIdentifier (NZAP_TEST_DISCORD_ID );
await expect (page).toHaveURL (`/u/${customUrl}` );
await userPage.gotoWithIdentifier (customUrl);
await expect (page).toHaveURL (`/u/${customUrl}` );
// without a custom URL the user id is the preferred identifier
await userPage.gotoWithIdentifier (ADMIN_DISCORD_ID );
await expect (page).toHaveURL (`/u/${ADMIN_ID}` );
})UserPage.gotoe2e/pages/user/user-page.ts:35
No JSDoc
async goto (userId: number ) {
await navigate ({ page: this .page, url: userPage ({ id: userId }) });
}UserPage.locators.badgeDisplaye2e/pages/user/user-page.ts:19
No JSDoc
page.getByTestId ("badge-display" ) UserPage.locators.badgePaginationButtonse2e/pages/user/user-page.ts:20
No JSDoc
page.getByTestId ("badge-pagination-button" ) isNotVisiblee2e/helpers/playwright.ts:745
No JSDoc
export function isNotVisible (locator: Locator ) {
return expect (locator).toHaveCount (0 );
}UserPage.badgeImagee2e/pages/user/user-page.ts:58
No JSDoc
badgeImage (displayName: string ) {
return this .page.getByAltText (displayName, { exact: true });
}UserPagee2e/pages/user/user-page.ts:12
No JSDoc
UserEditWidgetsPage.gotoe2e/pages/user/user-edit-widgets-page.ts:19
No JSDoc
async goto (userId: number ) {
await navigate ({
page: this .page,
url: `${userPage({ id: userId })}/edit-widgets` ,
});
}UserEditWidgetsPage.openWidgetSettingse2e/pages/user/user-edit-widgets-page.ts:46
async openWidgetSettings (widgetId: string ) {
await this .page.getByTestId (`widget-settings-${widgetId}` ).click ();
}UserEditWidgetsPage.selectFavoriteBadgee2e/pages/user/user-edit-widgets-page.ts:51
async selectFavoriteBadge (badgeId: number ) {
await this .locators.badgesSelector.selectOption (String (badgeId));
}UserEditWidgetsPage.savee2e/pages/user/user-edit-widgets-page.ts:70
No JSDoc
async save () {
await submit (this .page, this .locators.saveButton);
}UserEditWidgetsPagee2e/pages/user/user-edit-widgets-page.ts:6
UserEditWidgetsPage.locators.badgeDisplaye2e/pages/user/user-edit-widgets-page.ts:15
No JSDoc
page.getByTestId ("badge-display" ) UserPage.flage2e/pages/user/user-page.ts:62
No JSDoc
flag (countryCode: string ) {
return this .page.getByTestId (`flag-${countryCode}` );
}UserPage.openEditProfilee2e/pages/user/user-page.ts:106
No JSDoc
async openEditProfile () {
await this .locators.editProfileButton.click ();
return new UserEditProfilePage (this .page);
}userEditProfileBaseSchema.inGameNameapp/features/user-page/user-page-schemas.ts:85
inGameName: inGameName ({
label: "labels.inGameName" ,
bottomText: "bottomTexts.profileInGameName" ,
}) UserEditProfilePage.selectCountrye2e/pages/user/user-edit-profile-page.ts:24
No JSDoc
async selectCountry (name: string ) {
await this .page.getByLabel ("Country" ).click ();
const search = this .page.getByRole ("combobox" , { name: "Search" });
await search.fill (name);
// not a click: the popover moves as the list filters and Playwright's retry scrolls the trigger off screen, hiding it
const optionId = await this .page
.getByRole ("option" , { name })
.getAttribute ("id" );
await expect (search).toHaveAttribute (
"aria-activedescendant" ,
optionId ?? "" ,
);
await search.press ("Enter" );
// … UserEditProfilePage.savee2e/pages/user/user-edit-profile-page.ts:58
No JSDoc
async save () {
await submit (this .page);
}UserPage.texte2e/pages/user/user-page.ts:71
No JSDoc
text (content: string ) {
return this .page.getByText (content);
}UserEditProfilePagee2e/pages/user/user-edit-profile-page.ts:8
UserEditProfilePage.gotoe2e/pages/user/user-edit-profile-page.ts:17
No JSDoc
async goto (userId: number ) {
await navigate ({
page: this .page,
url: userEditProfilePage ({ id: userId }),
});
}UserEditProfilePage.setFriendCodee2e/pages/user/user-edit-profile-page.ts:44
No JSDoc
async setFriendCode (friendCode: string ) {
await this .page.getByRole ("button" , { name: "Set friend code" }).click ();
const dialog = this .page.getByRole ("dialog" , { name: "Friend code" });
await dialog.getByLabel ("Friend code" ).fill (friendCode);
await submit (this .page, dialog.getByTestId ("submit-button" ));
}UserEditProfilePage.friendCodeButtone2e/pages/user/user-edit-profile-page.ts:40
No JSDoc
friendCodeButton (friendCode: string ) {
return this .page.getByRole ("button" , { name: `SW-${friendCode}` });
}SettingsPage.gotoe2e/pages/settings/settings-page.ts:52
No JSDoc
async goto (tab?: SettingsTab ) {
await navigate ({
page: this .page,
url: tab ? `${SETTINGS_PAGE}?tab=${tab}` : SETTINGS_PAGE ,
});
}SettingsPage.hasCustomThemee2e/pages/settings/settings-page.ts:140
No JSDoc
hasCustomTheme () {
return this .page
.locator ("html" )
.evaluate ((el) => el.style.getPropertyValue ("--_base-h" ) !== "" );
}SettingsPage.setBaseHuee2e/pages/settings/settings-page.ts:124
No JSDoc
async setBaseHue (value: string ) {
await this .locators.baseHueSlider.fill (value);
}SettingsPage.saveThemee2e/pages/settings/settings-page.ts:128
No JSDoc
async saveTheme () {
await waitForPOSTResponse (this .page, () =>
this .locators.saveThemeButton.click (),
);
}SettingsPage.reloade2e/pages/settings/settings-page.ts:146
No JSDoc
async reload () {
await this .page.reload ();
await expectIsHydrated (this .page);
}SettingsPage.resetThemee2e/pages/settings/settings-page.ts:134
No JSDoc
async resetTheme () {
await waitForPOSTResponse (this .page, () =>
this .locators.resetThemeButton.click (),
);
}SettingsPagee2e/pages/settings/settings-page.ts:18
playFinishedSeasone2e/user-page.spec.ts:447
async function playFinishedSeason (factories: Factories , userId: number ) {
const mates = await factories.UserFactory .createMany (FULL_GROUP_SIZE - 1 );
const enemies = await factories.UserFactory .createMany (FULL_GROUP_SIZE );
const ownGroup = [userId, ...mates.map ((mate) => mate.id)];
const opposingGroup = enemies.map ((enemy) => enemy.id);
const { starts } = Seasons .nthToDateRange (FINISHED_SEASON );
for (let index = 0 ; index < MATCHES_COUNT_NEEDED_FOR_LEADERBOARD ; index++) {
// alpha wins every map, so which side the user is on decides the set
const userWon = index % 3 !== 0 ;
await factories.SQMatchFactory .create (
{
// … UserSeasonsPage.gotoe2e/pages/user/user-seasons-page.ts:20
No JSDoc
async goto (userId: number , season?: number ) {
await navigate ({
page: this .page,
url: userSeasonsPage ({ user: { id: userId }, season }),
});
}UserSeasonsPage.openExportDialoge2e/pages/user/user-seasons-page.ts:27
No JSDoc
async openExportDialog () {
await this .locators.exportImageButton.click ();
}UserSeasonsPage.exportDialogTexte2e/pages/user/user-seasons-page.ts:31
No JSDoc
exportDialogText (content: string ) {
return this .locators.exportDialog.getByText (content);
}UserSeasonsPage.downloadExportedImagee2e/pages/user/user-seasons-page.ts:35
No JSDoc
async downloadExportedImage () {
const downloadPromise = this .page.waitForEvent ("download" );
await this .locators.exportDialog
.getByRole ("button" , { name: "Download" })
.click ();
return downloadPromise;
}UserSeasonsPagee2e/pages/user/user-seasons-page.ts:6
UserSeasonsPage.locators.supporterPerkExplanatione2e/pages/user/user-seasons-page.ts:16
No JSDoc
page.getByText (/supporter perk/) UserSeasonsPage.locators.downloadButtone2e/pages/user/user-seasons-page.ts:15
No JSDoc
page.getByRole ("button" , { name: "Download" }) UserPage.weaponPoolImagee2e/pages/user/user-page.ts:67
weaponPoolImage (weaponSplId: number , position: number ) {
return this .page.getByTestId (`${weaponSplId}-${position}` );
}UserEditWidgetsPage.selectWeaponPoolWeapone2e/pages/user/user-edit-widgets-page.ts:56
async selectWeaponPoolWeapon (weaponName: string ) {
await this .page.getByTestId ("weapon-select" ).click ();
await this .page.getByPlaceholder ("Search weapons..." ).fill (weaponName);
await this .page
.getByRole ("listbox" )
.getByTestId (`weapon-select-option-${weaponName}` )
.click ();
}UserPage.openResultse2e/pages/user/user-page.ts:131
No JSDoc
async openResults () {
await this .locators.resultsTab.click ();
return new UserResultsPage (this .page);
}UserResultsPage.eventNamee2e/pages/user/user-results-page.ts:43
eventName (name: string ) {
return this .page.getByRole ("link" , { name });
}UserResultsPage.openChooseHighlightse2e/pages/user/user-results-page.ts:47
No JSDoc
async openChooseHighlights () {
await this .locators.chooseHighlightsButton.click ();
return new UserResultsHighlightsPage (this .page);
}UserResultsHighlightsPage.resultCheckboxe2e/pages/user/user-results-highlights-page.ts:21
resultCheckbox (resultName: RegExp ) {
return this .page.getByRole ("checkbox" , { name: resultName });
}UserResultsHighlightsPage.savee2e/pages/user/user-results-highlights-page.ts:25
No JSDoc
async save () {
await submit (this .page);
}UserResultsPage.gotoe2e/pages/user/user-results-page.ts:34
No JSDoc
async goto (userId: number ) {
await navigate ({ page: this .page, url: userResultsPage ({ id: userId }) });
}UserResultsPage.toggleHighlightsOnlye2e/pages/user/user-results-page.ts:27
async toggleHighlightsOnly () {
await this .locators.highlightsFilter.click ();
// the switch indicator covers its input, like everywhere else this one is clicked
await this .locators.highlightsOnlySwitch.click ({ force: true });
await this .page.keyboard.press ("Escape" );
}UserResultsPagee2e/pages/user/user-results-page.ts:7
UserResultsHighlightsPagee2e/pages/user/user-results-highlights-page.ts:6
UserPage.widgetHeadinge2e/pages/user/user-page.ts:80
widgetHeading (name: string ) {
return this .page.getByRole ("heading" , { name, exact: true });
}UserPage.openEditWidgetse2e/pages/user/user-page.ts:141
No JSDoc
async openEditWidgets () {
await this .locators.editWidgetsButton.click ();
return new UserEditWidgetsPage (this .page);
}UserEditWidgetsPage.fillBioe2e/pages/user/user-edit-widgets-page.ts:66
async fillBio (text: string ) {
await this .page.getByRole ("textbox" , { name: "Bio" }).fill (text);
}UserPage.exactTexte2e/pages/user/user-page.ts:75
No JSDoc
exactText (content: string ) {
return this .page.getByText (content, { exact: true });
}UserPage.openVodse2e/pages/user/user-page.ts:136
No JSDoc
async openVods () {
await this .locators.vodsTab.click ();
return new UserVodsPage (this .page);
}UserVodsPage.vodTitlee2e/pages/user/user-vods-page.ts:17
No JSDoc
vodTitle (title: string ) {
return this .page.getByRole ("heading" , { name: title });
}UserSeasonsPage.openStatse2e/pages/user/user-seasons-page.ts:45
async openStats () {
await this .page.getByRole ("link" , { name: "See all stats" }).click ();
}UserSeasonsPage.openStatsTabe2e/pages/user/user-seasons-page.ts:49
No JSDoc
async openStatsTab (
name: "Overview" | "Weapons" | "Stages" | "Teammates" | "Opponents" ,
) {
await this .page.getByRole ("tab" , { name }).click ();
}UserSeasonsPage.weaponUsageImagee2e/pages/user/user-seasons-page.ts:56
weaponUsageImage (weaponName: string ) {
return this .page.getByRole ("img" , { name: weaponName });
}UserSeasonsPage.weaponUsageSharee2e/pages/user/user-seasons-page.ts:61
weaponUsageShare (share: string ) {
return this .page.getByText (share, { exact: true });
}UserSeasonsPage.stageRecorde2e/pages/user/user-seasons-page.ts:66
stageRecord (record: string ) {
return this .page.getByText (record, { exact: true });
}UserSeasonsPage.playerLinke2e/pages/user/user-seasons-page.ts:71
playerLink (username: string ) {
return this .page.getByRole ("link" , { name: username });
}UserVodsPagee2e/pages/user/user-vods-page.ts:6
UserEditWidgetsPage.supporterOnlyLabele2e/pages/user/user-edit-widgets-page.ts:36
supporterOnlyLabel (widgetId: string ) {
return this .page.getByTestId (`supporter-only-${widgetId}` );
}UserEditWidgetsPage.addWidgetButtone2e/pages/user/user-edit-widgets-page.ts:31
No JSDoc
addWidgetButton (widgetId: string ) {
return this .page.getByTestId (`add-widget-${widgetId}` );
}UserEditWidgetsPage.removeWidgete2e/pages/user/user-edit-widgets-page.ts:41
async removeWidget (widgetId: string ) {
await this .page.getByTestId (`remove-widget-${widgetId}` ).click ();
}UserEditWidgetsPage.addWidgete2e/pages/user/user-edit-widgets-page.ts:27
async addWidget (widgetId: string ) {
await this .addWidgetButton (widgetId).click ();
}UserPage.widgete2e/pages/user/user-page.ts:85
widget (widgetId: string ) {
return this .page.getByTestId (`widget-${widgetId}` );
}UserPage.gotoWithIdentifiere2e/pages/user/user-page.ts:40
async gotoWithIdentifier (identifier: string | number ) {
await navigate ({ page: this .page, url: `/u/${identifier}` });
}