Admin panel grants roles, friend code and API access to a user Renders/admin /api /art/new /vods/new
Api page Admin actions page New art page New vod page Api page Arrange
Create user ...ROLE_TARGET friend code null → target
const target = await factories.UserFactory .create ({
...ROLE_TARGET ,
friendCode: null ,
});Api page Act
Open the api page
await api.goto ();Assert
no access message is visible
await expect (api.locators.noAccessMessage).toBeVisible ();Admin actions page Act
Open the admin actions page
await adminActions.goto ();Update friend code ROLE_TARGET .discordName, “0123-4567-8901”
await adminActions.updateFriendCode (ROLE_TARGET .discordName, FRIEND_CODE );Give artist ROLE_TARGET .discordName
await adminActions.giveArtist (ROLE_TARGET .discordName);Give video adder ROLE_TARGET .discordName
await adminActions.giveVideoAdder (ROLE_TARGET .discordName);Give api access ROLE_TARGET .discordName
await adminActions.giveApiAccess (ROLE_TARGET .discordName);Open friend code look up
await adminActions.openFriendCodeLookUp ();Search friend code “0123-4567-8901”
await adminActions.searchFriendCode (FRIEND_CODE );Assert
found user link ROLE_TARGET .discordName is visible
await expect (
adminActions.foundUserLink (ROLE_TARGET .discordName),
).toBeVisible ();New art page Act
Open the new art page
await newArt.goto ();Assert
description input is visible
await expect (newArt.locators.descriptionInput).toBeVisible ();New vod page Act
Open the new vod page
await newVod.goto ();Assert
add match button is visible
await expect (newVod.locators.addMatchButton).toBeVisible ();Api page Act
Open the api page
await api.goto ();Assert
no access message is not visible
await isNotVisible (api.locators.noAccessMessage);Act
Generate token “read” → token
const token = await api.generateToken ("read" );Assert
token.length is more than 0
expect (token.length).toBeGreaterThan (0 );Source test ("grants roles, friend code and API access to a user" , async ({
page,
factories,
}) => {
const target = await factories.UserFactory .create ({
...ROLE_TARGET ,
friendCode: null ,
});
const api = new ApiPage (page);
await impersonate (page, target.id);
await api.goto ();
await expect (api.locators.noAccessMessage).toBeVisible ();
await impersonate (page, ADMIN_ID );
const adminActions = new AdminActionsPage (page);
await adminActions.goto ();
await adminActions.updateFriendCode (ROLE_TARGET .discordName, FRIEND_CODE );
await adminActions.giveArtist (ROLE_TARGET .discordName);
await adminActions.giveVideoAdder (ROLE_TARGET .discordName);
await adminActions.giveApiAccess (ROLE_TARGET .discordName);
await adminActions.openFriendCodeLookUp ();
await adminActions.searchFriendCode (FRIEND_CODE );
await expect (
adminActions.foundUserLink (ROLE_TARGET .discordName),
).toBeVisible ();
await impersonate (page, target.id);
const newArt = new NewArtPage (page);
await newArt.goto ();
await expect (newArt.locators.descriptionInput).toBeVisible ();
const newVod = new NewVodPage (page);
await newVod.goto ();
await expect (newVod.locators.addMatchButton).toBeVisible ();
await api.goto ();
await isNotVisible (api.locators.noAccessMessage);
const token = await api.generateToken ("read" );
expect (token.length).toBeGreaterThan (0 );
})grants tournament organizer role and patron status to a user Renders/ /admin /org/new
New organization page Top right buttons Admin actions page New organization page Arrange
Create user ROLE_TARGET → target
const target = await factories.UserFactory .create (ROLE_TARGET );New organization page Act
Open the new organization page
await newOrganization.goto ();Assert
no permissions alert is visible
await expect (newOrganization.locators.noPermissionsAlert).toBeVisible ();Act
Go to “/”
await navigate ({ page, url: "/" });Top right buttons Assert
support link is visible
await expect (topRightButtons.locators.supportLink).toBeVisible ();Admin actions page Act
Open the admin actions page
await adminActions.goto ();Give tournament organizer ROLE_TARGET .discordName
await adminActions.giveTournamentOrganizer (ROLE_TARGET .discordName);tier one so the tournament organizer grant above stays the only source of that role
Force patron ROLE_TARGET .discordName, tier “Support” expires at addYears (new Date (), 1 )
await adminActions.forcePatron (ROLE_TARGET .discordName, {
tier: "Support" ,
expiresAt: addYears (new Date (), 1 ),
});New organization page Act
Open the new organization page
await newOrganization.goto ();Assert
heading is visible
await expect (newOrganization.locators.heading).toBeVisible ();Act
Go to “/”
await navigate ({ page, url: "/" });Assert
support link is not visible
await isNotVisible (topRightButtons.locators.supportLink);Source test ("grants tournament organizer role and patron status to a user" , async ({
page,
factories,
}) => {
const target = await factories.UserFactory .create (ROLE_TARGET );
const newOrganization = new NewOrganizationPage (page);
const topRightButtons = new TopRightButtons (page);
await impersonate (page, target.id);
await newOrganization.goto ();
await expect (newOrganization.locators.noPermissionsAlert).toBeVisible ();
await navigate ({ page, url: "/" });
await expect (topRightButtons.locators.supportLink).toBeVisible ();
await impersonate (page, ADMIN_ID );
const adminActions = new AdminActionsPage (page);
await adminActions.goto ();
await adminActions.giveTournamentOrganizer (ROLE_TARGET .discordName);
// tier one so the tournament organizer grant above stays the only source of that role
await adminActions.forcePatron (ROLE_TARGET .discordName, {
tier: "Support" ,
expiresAt: addYears (new Date (), 1 ),
});
await impersonate (page, target.id);
await newOrganization.goto ();
await expect (newOrganization.locators.heading).toBeVisible ();
await navigate ({ page, url: "/" });
await isNotVisible (topRightButtons.locators.supportLink);
})links a player, refreshes plus tiers and migrates an account Renders/admin /u/:identifier /u/:identifier /*
Admin actions page User page Top search player page User page Arrange
linking a player syncs XP badges, which requires every XP badge to exist
For each value of SPLATOON_3_XP_BADGE_VALUES
for (const value of SPLATOON_3_XP_BADGE_VALUES ) {
await factories.BadgeFactory .create ({ code: String (value) });
}Create badge code String (value)
await factories.BadgeFactory .create ({ code: String (value) });Create X rank placement player spl id “e2e-unlinked-player” name “PlacedPlayer”
await factories.XRankPlacementFactory .create ({
playerSplId: "e2e-unlinked-player" ,
name: "PlacedPlayer" ,
});Create user ...KEEPER → keeper
const keeper = await factories.UserFactory .create ({ ...KEEPER });Create user ...LEAVER
await factories.UserFactory .create ({ ...LEAVER });Create plus vote author Sendou voted id keeper.id
await factories.PlusVoteFactory .create ({
authorId: ADMIN_ID ,
votedId: keeper.id,
});Admin actions page Act
Open the admin actions page
await adminActions.goto ();Link player “N-ZAP” , 1
await adminActions.linkPlayer ("N-ZAP" , LINKED_PLAYER_ID );Refresh plus tiers
await adminActions.refreshPlusTiers ();Migrate user old user name KEEPER .discordName new user name LEAVER .discordName
await adminActions.migrateUser ({
oldUserName: KEEPER .discordName,
newUserName: LEAVER .discordName,
});Assert
user search suggestion KEEPER .discordName contains “+1”
await expect (
await adminActions.userSearchSuggestion (KEEPER .discordName),
).toContainText ("+1" );User page Act
Open the user page N-ZAP
await userPage.goto (NZAP_TEST_ID );Assert
widget “x-rank-peaks” is visible
await expect (userPage.widget ("x-rank-peaks" )).toBeVisible ();Act
Open placements → playerPage
const playerPage = await userPage.openPlacements ();Top search player page Assert
heading is visible
await expect (playerPage.locators.heading).toBeVisible ();User page Act
the old user kept their account, now reached via the new user's Discord id
Go to with identifier LEAVER .discordId
await userPage.gotoWithIdentifier (LEAVER .discordId);Assert
username heading KEEPER .discordName is visible
await expect (userPage.usernameHeading (KEEPER .discordName)).toBeVisible ();Source test ("links a player, refreshes plus tiers and migrates an account" , async ({
page,
factories,
}) => {
// linking a player syncs XP badges, which requires every XP badge to exist
for (const value of SPLATOON_3_XP_BADGE_VALUES ) {
await factories.BadgeFactory .create ({ code: String (value) });
}
await factories.XRankPlacementFactory .create ({
playerSplId: "e2e-unlinked-player" ,
name: "PlacedPlayer" ,
});
const keeper = await factories.UserFactory .create ({ ...KEEPER });
await factories.UserFactory .create ({ ...LEAVER });
await factories.PlusVoteFactory .create ({
authorId: ADMIN_ID ,
votedId: keeper.id,
});
await impersonate (page, ADMIN_ID );
const adminActions = new AdminActionsPage (page);
await adminActions.goto ();
await adminActions.linkPlayer ("N-ZAP" , LINKED_PLAYER_ID );
await adminActions.refreshPlusTiers ();
await adminActions.migrateUser ({
oldUserName: KEEPER .discordName,
newUserName: LEAVER .discordName,
});
await expect (
await adminActions.userSearchSuggestion (KEEPER .discordName),
).toContainText ("+1" );
const userPage = new UserPage (page);
await userPage.goto (NZAP_TEST_ID );
await expect (userPage.widget ("x-rank-peaks" )).toBeVisible ();
const playerPage = await userPage.openPlacements ();
await expect (playerPage.locators.heading).toBeVisible ();
// the old user kept their account, now reached via the new user's Discord id
await userPage.gotoWithIdentifier (LEAVER .discordId);
await expect (userPage.usernameHeading (KEEPER .discordName)).toBeVisible ();
})Renders/admin
Admin actions page Arrange
Create user ...ROLE_TARGET friend code null
await factories.UserFactory .create ({ ...ROLE_TARGET , friendCode: null });Admin actions page Act
Open the admin actions page
await adminActions.goto ();the form is far down the page, so filling it scrolls there
Update friend code ROLE_TARGET .discordName, “0123-4567-8901”
await adminActions.updateFriendCode (ROLE_TARGET .discordName, FRIEND_CODE );Assert
evaluate () => window.scrollY is more than 0
expect (await page.evaluate (() => window.scrollY)).toBeGreaterThan (0 );Source test ("keeps the scroll position when an action shows a toast" , async ({
page,
factories,
}) => {
await factories.UserFactory .create ({ ...ROLE_TARGET , friendCode: null });
await impersonate (page, ADMIN_ID );
const adminActions = new AdminActionsPage (page);
await adminActions.goto ();
// the form is far down the page, so filling it scrolls there
await adminActions.updateFriendCode (ROLE_TARGET .discordName, FRIEND_CODE );
expect (await page.evaluate (() => window.scrollY)).toBeGreaterThan (0 );
})adds and deletes an external stream Renders/admin/streams
Admin streams page Admin streams page Act
Open the admin streams page
await streams.goto ();Assert
add stream heading is visible
await expect (streams.locators.addStreamHeading).toBeVisible ();no streams is visible
await expect (streams.locators.noStreams).toBeVisible ();Act
Create stream name “E2E Stream” url “https://www.twitch.tv/sendou” start time addHours (new Date (), 1 ) logo path TEST_IMAGE_PATH
await streams.createStream ({
name: "E2E Stream" ,
url: "https://www.twitch.tv/sendou" ,
startTime: addHours (new Date (), 1 ),
logoPath: TEST_IMAGE_PATH ,
});Assert
stream link “E2E Stream” is visible
await expect (streams.streamLink ("E2E Stream" )).toBeVisible ();no streams is not visible
await isNotVisible (streams.locators.noStreams);Act
Delete stream “E2E Stream”
await streams.deleteStream ("E2E Stream" );Assert
no streams is visible
await expect (streams.locators.noStreams).toBeVisible ();Source test ("adds and deletes an external stream" , async ({ page }) => {
await impersonate (page, ADMIN_ID );
const streams = new AdminStreamsPage (page);
await streams.goto ();
await expect (streams.locators.addStreamHeading).toBeVisible ();
await expect (streams.locators.noStreams).toBeVisible ();
await streams.createStream ({
name: "E2E Stream" ,
url: "https://www.twitch.tv/sendou" ,
startTime: addHours (new Date (), 1 ),
logoPath: TEST_IMAGE_PATH ,
});
await expect (streams.streamLink ("E2E Stream" )).toBeVisible ();
await isNotVisible (streams.locators.noStreams);
await streams.deleteStream ("E2E Stream" );
await expect (streams.locators.noStreams).toBeVisible ();
})ApiPage.gotoe2e/pages/api/api-page.ts:25
No JSDoc
async goto () {
await navigate ({ page: this .page, url: API_PAGE });
}ApiPage.locators.noAccessMessagee2e/pages/api/api-page.ts:19
No JSDoc
page.getByText ("You do not have access to the API" ) AdminActionsPage.gotoe2e/pages/admin/admin-actions-page.ts:20
No JSDoc
async goto () {
await navigate ({ page: this .page, url: ADMIN_PAGE });
}AdminActionsPage.updateFriendCodee2e/pages/admin/admin-actions-page.ts:24
No JSDoc
async updateFriendCode (userName: string , friendCode: string ) {
const form = this .form ("Update friend code" );
await this .selectFormUser (form, userName);
await form.getByLabel ("Friend code" ).fill (friendCode);
await this .submitForm (form, "Submit" );
}AdminActionsPage.giveArtiste2e/pages/admin/admin-actions-page.ts:38
No JSDoc
async giveArtist (userName: string ) {
const form = this .form ("Add as artist" );
await this .selectFormUser (form, userName);
await this .submitForm (form, "Add as artist" );
}AdminActionsPage.giveVideoAddere2e/pages/admin/admin-actions-page.ts:44
No JSDoc
async giveVideoAdder (userName: string ) {
const form = this .form ("Give video adder" );
await this .selectFormUser (form, userName);
await this .submitForm (form, "Add as video adder" );
}AdminActionsPage.giveApiAccesse2e/pages/admin/admin-actions-page.ts:56
No JSDoc
async giveApiAccess (userName: string ) {
const form = this .form ("Give API access" );
await this .selectFormUser (form, userName);
await this .submitForm (form, "Grant API access" );
}AdminActionsPage.openFriendCodeLookUpe2e/pages/admin/admin-actions-page.ts:115
No JSDoc
async openFriendCodeLookUp () {
await this .page.getByRole ("tab" , { name: "Friend code look-up" }).click ();
}AdminActionsPage.searchFriendCodee2e/pages/admin/admin-actions-page.ts:119
No JSDoc
async searchFriendCode (friendCode: string ) {
await this .page
.getByRole ("textbox" , { name: "Friend code" })
.fill (friendCode);
// scoped by test id: a role query for "Search" would also hit the header's global search
await this .page.getByTestId ("submit-button" ).click ();
}AdminActionsPage.foundUserLinke2e/pages/admin/admin-actions-page.ts:127
No JSDoc
foundUserLink (userName: string ) {
return this .page.getByRole ("link" , { name: userName });
}NewArtPage.gotoe2e/pages/art/new-art-page.ts:27
async goto (artId?: Tables ["Art" ]["id" ]) {
await navigate ({ page: this .page, url: newArtPage (artId) });
}NewArtPage.locators.descriptionInpute2e/pages/art/new-art-page.ts:22
No JSDoc
this .page.getByLabel (this .form.getLabel ("description" ))NewVodPage.gotoe2e/pages/vods/new-vod-page.ts:31
No JSDoc
async goto () {
await navigate ({ page: this .page, url: newVodPage () });
}NewVodPage.locators.addMatchButtone2e/pages/vods/new-vod-page.ts:24
No JSDoc
this .page.getByRole ("button" , {
name: "Add" ,
exact: true ,
})isNotVisiblee2e/helpers/playwright.ts:745
No JSDoc
export function isNotVisible (locator: Locator ) {
return expect (locator).toHaveCount (0 );
}ApiPage.generateTokene2e/pages/api/api-page.ts:29
No JSDoc
async generateToken (type : ApiTokenType ) {
await this .section (type )
.getByRole ("button" , { name: "Generate Token" })
.click ();
await this .page.waitForURL (API_PAGE );
return this .revealToken (type );
}ApiPagee2e/pages/api/api-page.ts:12
AdminActionsPagee2e/pages/admin/admin-actions-page.ts:13
NewArtPagee2e/pages/art/new-art-page.ts:10
NewVodPagee2e/pages/vods/new-vod-page.ts:15
NewOrganizationPage.gotoe2e/pages/org/new-organization-page.ts:21
No JSDoc
async goto () {
await navigate ({ page: this .page, url: ORGANIZATION_NEW_PAGE });
}NewOrganizationPage.locators.noPermissionsAlerte2e/pages/org/new-organization-page.ts:17
No JSDoc
page.getByText ("No permissions to add organizations" ) TopRightButtons.locators.supportLinke2e/pages/layout/top-right-buttons.ts:12
No JSDoc
page
.getByRole ("banner" )
.getByRole ("link" , { name: "Support" }) AdminActionsPage.giveTournamentOrganizere2e/pages/admin/admin-actions-page.ts:50
No JSDoc
async giveTournamentOrganizer (userName: string ) {
const form = this .form ("Give tournament organizer" );
await this .selectFormUser (form, userName);
await this .submitForm (form, "Add as tournament organizer" );
}AdminActionsPage.forcePatrone2e/pages/admin/admin-actions-page.ts:62
No JSDoc
async forcePatron (
userName: string ,
{ tier, expiresAt }: { tier: PatronTierLabel ; expiresAt: Date },
) {
const form = this .form ("Force patron" );
await this .selectFormUser (form, userName);
await form.getByLabel ("Patron tier" ).selectOption ({ label: tier });
await fillDateTimeField ({
scope: form,
label: "Patron until" ,
date: expiresAt,
});
await this .submitForm (form, "Save" );
}NewOrganizationPage.locators.headinge2e/pages/org/new-organization-page.ts:16
No JSDoc
page.getByRole ("heading" , { name: "New Organization" }) NewOrganizationPagee2e/pages/org/new-organization-page.ts:7
No JSDoc
TopRightButtonse2e/pages/layout/top-right-buttons.ts:4
AdminActionsPage.linkPlayere2e/pages/admin/admin-actions-page.ts:31
No JSDoc
async linkPlayer (userName: string , playerId: number ) {
const form = this .form ("Link player" );
await this .selectFormUser (form, userName);
await form.getByLabel ("Player ID" ).fill (String (playerId));
await this .submitForm (form, "Link player" );
}AdminActionsPage.refreshPlusTierse2e/pages/admin/admin-actions-page.ts:100
No JSDoc
async refreshPlusTiers () {
await this .submitForm (this .form ("Refresh Plus Tiers" ), "Refresh" );
}AdminActionsPage.migrateUsere2e/pages/admin/admin-actions-page.ts:77
No JSDoc
async migrateUser ({
oldUserName,
newUserName,
}: {
oldUserName: string ;
newUserName: string ;
}) {
const form = this .form ("Migrate user data" );
await selectUser ({
page: this .page,
userName: oldUserName,
labelName: "Old user" ,
within: form,
});
// … AdminActionsPage.userSearchSuggestione2e/pages/admin/admin-actions-page.ts:105
async userSearchSuggestion (userName: string ) {
const form = this .form ("Add as artist" );
await form.getByLabel ("User" ).click ();
await this .page.getByTestId ("user-search-input" ).fill (userName);
return this .page
.getByRole ("listbox" )
.getByTestId ("user-search-item" )
.first ();
}UserPage.gotoe2e/pages/user/user-page.ts:35
No JSDoc
async goto (userId: number ) {
await navigate ({ page: this .page, url: userPage ({ id: userId }) });
}UserPage.widgete2e/pages/user/user-page.ts:85
widget (widgetId: string ) {
return this .page.getByTestId (`widget-${widgetId}` );
}UserPage.openPlacementse2e/pages/user/user-page.ts:117
async openPlacements () {
await this .widget ("x-rank-peaks" ).getByRole ("link" ).click ();
return new TopSearchPlayerPage (this .page);
}TopSearchPlayerPage.locators.headinge2e/pages/top-search/top-search-player-page.ts:18
No JSDoc
this .page.getByRole ("heading" , { name: /placements$/ })UserPage.gotoWithIdentifiere2e/pages/user/user-page.ts:40
async gotoWithIdentifier (identifier: string | number ) {
await navigate ({ page: this .page, url: `/u/${identifier}` });
}UserPage.usernameHeadinge2e/pages/user/user-page.ts:94
No JSDoc
usernameHeading (username: string ) {
return this .page.getByRole ("heading" , { name: username });
}UserPagee2e/pages/user/user-page.ts:12
No JSDoc
TopSearchPlayerPagee2e/pages/top-search/top-search-player-page.ts:8
AdminStreamsPage.gotoe2e/pages/admin/admin-streams-page.ts:25
No JSDoc
async goto () {
await navigate ({ page: this .page, url: "/admin/streams" });
}AdminStreamsPage.locators.addStreamHeadinge2e/pages/admin/admin-streams-page.ts:18
No JSDoc
page.getByRole ("heading" , {
name: "Add external stream" ,
}) AdminStreamsPage.locators.noStreamse2e/pages/admin/admin-streams-page.ts:21
No JSDoc
page.getByText ("No external streams" ) AdminStreamsPage.createStreame2e/pages/admin/admin-streams-page.ts:33
No JSDoc
async createStream ({
name,
url,
startTime,
logoPath,
}: {
name: string ;
url: string ;
startTime: Date ;
logoPath: string ;
}) {
const form = this .form ();
await form.getByLabel ("Name" ).fill (name);
await form.getByLabel ("Link" ).fill (url);
// … AdminStreamsPage.streamLinke2e/pages/admin/admin-streams-page.ts:29
No JSDoc
streamLink (name: string ) {
return this .page.getByRole ("link" , { name });
}AdminStreamsPage.deleteStreame2e/pages/admin/admin-streams-page.ts:62
No JSDoc
async deleteStream (name: string ) {
await this .page
.getByRole ("listitem" )
.filter ({ hasText: name })
.getByRole ("button" , { name: "Delete" })
.click ();
await modalClickConfirmButton (this .page);
}AdminStreamsPagee2e/pages/admin/admin-streams-page.ts:11