Created
March 30, 2025 20:30
-
-
Save janhesters/a386d2d64917dfb23b2939315f2e8be5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AxeBuilder from '@axe-core/playwright'; | |
import { faker } from '@faker-js/faker'; | |
import type { Page } from '@playwright/test'; | |
import { expect, test } from '@playwright/test'; | |
import type { Organization, UserAccount } from '@prisma/client'; | |
import { OrganizationMembershipRole } from '@prisma/client'; | |
import { | |
createPopulatedOrganization, | |
createPopulatedOrganizationInviteLink, | |
} from '~/features/organizations/organizations-factories.server'; | |
import { | |
retrieveLatestInviteLinkFromDatabaseByOrganizationId, | |
saveOrganizationInviteLinkToDatabase, | |
} from '~/features/organizations/organization-invite-link-model.server'; | |
import { | |
addMembersToOrganizationInDatabaseById, | |
deleteOrganizationFromDatabaseById, | |
retrieveOrganizationFromDatabaseById, // Needed for checking owner counts etc. | |
} from '~/features/organizations/organizations-model.server'; | |
import { retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId } from '~/features/organizations/organization-membership-model.server'; | |
import { createPopulatedUserAccount } from '~/features/user-accounts/user-accounts-factories.server'; | |
import { | |
deleteUserAccountFromDatabaseById, | |
saveUserAccountToDatabase, | |
} from '~/features/user-accounts/user-accounts-model.server'; | |
import { teardownOrganizationAndMember } from '~/test/test-utils'; // Keep this simple teardown | |
import { asyncForEach } from '~/utils/async-for-each.server'; // Keep if used | |
import { | |
getPath, | |
loginAndSaveUserAccountToDatabase, | |
setupOrganizationAndLoginAsMember, | |
} from '../../utils'; | |
/** Helper to create multiple members with specific roles */ | |
async function setupMultipleMembers({ | |
page, | |
requestingUserRole, | |
otherMemberRoles = [], // Array of roles for other members | |
activeInviteLink = false, // Whether to create an active invite link initially | |
}: { | |
page: Page; | |
requestingUserRole: OrganizationMembershipRole; | |
otherMemberRoles?: OrganizationMembershipRole[]; | |
activeInviteLink?: boolean; | |
}) { | |
// Create the main user and log them in with the specified role | |
const { organization, user: requestingUser } = | |
await setupOrganizationAndLoginAsMember({ | |
page, | |
role: requestingUserRole, | |
}); | |
// Create other users and add them to the organization with specified roles | |
const otherUsers = await Promise.all( | |
otherMemberRoles.map(async (role, index) => { | |
const otherUser = createPopulatedUserAccount({ | |
// Ensure unique emails if needed | |
email: `test-member-${role}-${index}-${faker.string.uuid()}@example.com`, | |
name: faker.person.fullName(), // Give them names for easier identification | |
}); | |
await saveUserAccountToDatabase(otherUser); | |
await addMembersToOrganizationInDatabaseById({ | |
id: organization.id, | |
members: [otherUser.id], | |
role: role, | |
}); | |
return otherUser; | |
}), | |
); | |
// Optionally create an active invite link | |
let inviteLink = undefined; | |
if (activeInviteLink) { | |
inviteLink = createPopulatedOrganizationInviteLink({ | |
organizationId: organization.id, | |
creatorId: requestingUser.id, | |
}); | |
await saveOrganizationInviteLinkToDatabase(inviteLink); | |
} | |
// Combine all users for teardown | |
const allUsers = [requestingUser, ...otherUsers]; | |
return { organization, requestingUser, otherUsers, allUsers, inviteLink }; | |
} | |
/** Teardown helper for multiple members */ | |
async function teardownMultipleMembers({ | |
organization, | |
allUsers, | |
}: { | |
organization: Organization; | |
allUsers: UserAccount[]; | |
}) { | |
// Delete the organization (cascades memberships and invite links) | |
await deleteOrganizationFromDatabaseById(organization.id); | |
// Delete all created users | |
await asyncForEach(allUsers, async user => { | |
await deleteUserAccountFromDatabaseById(user.id); | |
}); | |
} | |
const getMembersPagePath = (slug: string) => | |
`/organizations/${slug}/settings/members`; | |
test.describe('organization settings members page', () => { | |
// ======================================================================== | |
// Authentication & Authorization Tests | |
// ======================================================================== | |
test('given: a logged out user, should: redirect to login page', async ({ | |
page, | |
}) => { | |
const { slug } = createPopulatedOrganization(); | |
const path = getMembersPagePath(slug); | |
await page.goto(path); | |
const searchParameters = new URLSearchParams(); | |
searchParameters.append('redirectTo', path); | |
expect(getPath(page)).toEqual(`/login?${searchParameters.toString()}`); | |
}); | |
test('given: a logged in user who is NOT onboarded, should: redirect to onboarding', async ({ | |
page, | |
}) => { | |
// Setup user without name (implies not onboarded) | |
const { userAccount } = await loginAndSaveUserAccountToDatabase({ | |
user: createPopulatedUserAccount({ name: '' }), | |
page, | |
}); | |
// Create an org manually they _could_ belong to, but they aren't onboarded | |
const organization = createPopulatedOrganization(); | |
await saveOrganizationToDatabase(organization); | |
const path = getMembersPagePath(organization.slug); | |
await page.goto(path); | |
// Expect redirect to user profile onboarding step (or org step depending on flow) | |
expect(getPath(page)).toMatch(/\/onboarding\//); | |
await deleteOrganizationFromDatabaseById(organization.id); | |
await deleteUserAccountFromDatabaseById(userAccount.id); | |
}); | |
test('given: a user who is NOT a member of the organization, should: show 404 page', async ({ | |
page, | |
}) => { | |
// User 1 and their org | |
const { organization: org1, user: user1 } = | |
await setupOrganizationAndLoginAsMember({ page }); | |
// User 2 and their org | |
const { organization: org2, user: user2 } = | |
await setupOrganizationAndLoginAsMember({ createNewUser: true }); // Ensure different user | |
// User 1 tries to access User 2's org settings | |
await page.goto(getMembersPagePath(org2.slug)); | |
// Assert 404 content | |
await expect(page).toHaveTitle(/404/i); | |
await expect( | |
page.getByRole('heading', { name: /page not found/i, level: 1 }), | |
).toBeVisible(); | |
// Cleanup | |
await teardownOrganizationAndMember({ organization: org1, user: user1 }); | |
await teardownOrganizationAndMember({ organization: org2, user: user2 }); | |
}); | |
test('given: a member who has been deactivated, should: redirect to organization onboarding', async ({ | |
page, | |
}) => { | |
const { organization, user } = await setupOrganizationAndLoginAsMember({ | |
page, | |
role: OrganizationMembershipRole.member, // Start as active member | |
}); | |
// Deactivate the user in the DB | |
const membership = | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: user.id, organizationId: organization.id }, | |
); | |
await prisma.organizationMembership.update({ | |
where: { memberId_organizationId: membership!.memberId_organizationId }, | |
data: { deactivatedAt: new Date() }, | |
}); | |
// Attempt to access the members page | |
await page.goto(getMembersPagePath(organization.slug)); | |
// Assert redirection (likely to a page asking to join/create org) | |
expect(getPath(page)).toEqual('/onboarding/organization'); | |
await teardownOrganizationAndMember({ organization, user }); | |
}); | |
// ======================================================================== | |
// Basic Page Structure Tests | |
// ======================================================================== | |
test('given: any valid member (owner), should: show correct title, headings, and navigation', async ({ | |
page, | |
isMobile, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
}); | |
const { organization } = data; | |
const path = getMembersPagePath(organization.slug); | |
await page.goto(path); | |
// Title | |
await expect(page).toHaveTitle(/Team Members Settings/i); // Adjust based on your actual title | |
// Headings | |
await expect( | |
page.getByRole('heading', { name: /Organization Settings/i, level: 1 }), | |
).toBeVisible(); | |
await expect( | |
page.getByRole('heading', { name: /Team Members/i, level: 2 }), | |
).toBeVisible(); | |
// Settings Nav Tabs | |
const settingsNav = page.getByRole('navigation', { name: /Settings/i }); // Adjust name if needed | |
await expect( | |
settingsNav.getByRole('link', { name: /General/i }), | |
).toHaveAttribute( | |
'href', | |
`/organizations/${organization.slug}/settings/profile`, | |
); | |
await expect( | |
settingsNav.getByRole('link', { name: /Team Members/i }), | |
).toHaveAttribute('href', path); | |
await expect( | |
settingsNav.getByRole('link', { name: /Team Members/i }), | |
).toHaveAttribute('aria-current', 'page'); | |
// Main Sidebar (example, adjust based on your layout) | |
// eslint-disable-next-line playwright/no-conditional-in-test | |
if (isMobile) { | |
await page.getByRole('button', { name: /open sidebar/i }).click(); | |
} | |
const mainSidebar = page.getByRole('navigation', { name: /Sidebar/i }); // Adjust name | |
await expect( | |
mainSidebar.getByRole('link', { name: /Dashboard/i }), // Example link | |
).toHaveAttribute('href', `/organizations/${organization.slug}/dashboard`); | |
await expect( | |
mainSidebar.getByRole('link', { name: /Settings/i }), | |
).toHaveAttribute('href', `/organizations/${organization.slug}/settings`); | |
// eslint-disable-next-line playwright/no-conditional-in-test | |
if (isMobile) { | |
await page.getByRole('button', { name: /close sidebar/i }).click(); | |
} | |
// User Menu | |
await page.getByRole('button', { name: /open user menu/i }).click(); | |
await expect( | |
page.getByRole('menuitem', { name: /Your Settings/i }), | |
).toBeVisible(); | |
await expect( | |
page.getByRole('menuitem', { name: /Log out/i }), | |
).toBeVisible(); | |
await page.keyboard.press('Escape'); // Close menu | |
await teardownMultipleMembers(data); | |
}); | |
// ======================================================================== | |
// Role: Member - UI & Functionality Tests | |
// ======================================================================== | |
test.describe('as Member', () => { | |
test('should: show member list, hide invite cards, and disable role changes', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.member, | |
otherMemberRoles: [OrganizationMembershipRole.owner], // Include an owner to see their role | |
}); | |
const { organization, requestingUser, otherUsers } = data; | |
await page.goto(getMembersPagePath(organization.slug)); | |
// Verify Invite Cards are Hidden | |
await expect( | |
page.getByRole('heading', { name: /Invite by Email/i }), | |
).toBeHidden(); | |
await expect( | |
page.getByRole('heading', { name: /Invite Link/i }), | |
).toBeHidden(); | |
await expect( | |
page.getByRole('button', { name: /Send Invite/i }), | |
).toBeHidden(); | |
await expect( | |
page.getByRole('button', { name: /Create new invite link/i }), | |
).toBeHidden(); | |
// Verify Member Table | |
const table = page.getByRole('table'); | |
await expect(table).toBeVisible(); | |
// Check requesting user's row (cannot change own role) | |
const userRow = table.getByRole('row', { name: requestingUser.email }); | |
await expect( | |
userRow.getByRole('cell', { name: requestingUser.name }), | |
).toBeVisible(); | |
await expect( | |
userRow.getByRole('cell', { name: /Member/i }), | |
).toBeVisible(); // Shows text, not button | |
await expect( | |
userRow.getByRole('button', { name: /Member/i }), | |
).toBeHidden(); // No button to change role | |
// Check other user's row (member cannot change others' roles) | |
const otherUser = otherUsers[0]; | |
const otherRow = table.getByRole('row', { name: otherUser.email }); | |
await expect( | |
otherRow.getByRole('cell', { name: otherUser.name }), | |
).toBeVisible(); | |
await expect( | |
otherRow.getByRole('cell', { name: /Owner/i }), | |
).toBeVisible(); // Shows text, not button | |
await expect( | |
otherRow.getByRole('button', { name: /Owner/i }), | |
).toBeHidden(); // No button to change role | |
await teardownMultipleMembers(data); | |
}); | |
}); | |
// ======================================================================== | |
// Role: Admin - UI & Functionality Tests | |
// ======================================================================== | |
test.describe('as Admin', () => { | |
test('should: show member list, show invite cards, allow changing Member/Admin roles (not Owner)', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.admin, | |
otherMemberRoles: [ | |
OrganizationMembershipRole.member, | |
OrganizationMembershipRole.owner, | |
], | |
}); | |
const { organization, requestingUser, otherUsers } = data; | |
const memberUser = otherUsers.find( | |
u => u.email.includes('test-member-member'), | |
)!; | |
const ownerUser = otherUsers.find( | |
u => u.email.includes('test-member-owner'), | |
)!; | |
await page.goto(getMembersPagePath(organization.slug)); | |
// Verify Invite Cards are Visible | |
await expect( | |
page.getByRole('heading', { name: /Invite by Email/i }), | |
).toBeVisible(); | |
await expect( | |
page.getByRole('heading', { name: /Invite Link/i }), | |
).toBeVisible(); | |
// Role dropdown in invite card should NOT have Owner option | |
await page | |
.getByRole('combobox', { name: /Role/i }) | |
.first() // Assuming first role dropdown is email invite | |
.click(); | |
await expect( | |
page.getByRole('option', { name: /Owner/i }), | |
).toBeHidden(); | |
await page.keyboard.press('Escape'); | |
// Verify Member Table & Role Changes | |
const table = page.getByRole('table'); | |
// Check Admin's own row (cannot change self) | |
const adminRow = table.getByRole('row', { name: requestingUser.email }); | |
await expect( | |
adminRow.getByRole('cell', { name: /Admin/i }), | |
).toBeVisible(); | |
await expect( | |
adminRow.getByRole('button', { name: /Admin/i }), | |
).toBeHidden(); | |
// Check Member's row (Admin CAN change Member) | |
const memberRow = table.getByRole('row', { name: memberUser.email }); | |
const memberRoleButton = memberRow.getByRole('button', { | |
name: /Member/i, | |
}); | |
await expect(memberRoleButton).toBeVisible(); | |
// Try changing Member to Admin | |
await memberRoleButton.click(); | |
const changeToAdminButton = page.getByRole('button', { | |
name: /^Admin$/i, | |
}); // Use exact match to distinguish from description | |
await expect(changeToAdminButton).toBeVisible(); | |
await expect( | |
page.getByRole('button', { name: /^Owner$/i }), | |
).toBeHidden(); // Admin cannot promote to Owner | |
// Click change to Admin | |
const postPromiseAdmin = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await changeToAdminButton.click(); | |
const responseAdmin = await postPromiseAdmin; | |
expect(responseAdmin.ok()).toBeTruthy(); | |
// Check DB (or wait for UI update if optimistic) | |
let updatedMembership = | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: memberUser.id, organizationId: organization.id }, | |
); | |
expect(updatedMembership?.role).toEqual(OrganizationMembershipRole.admin); | |
// UI should ideally update after fetcher returns, check for the "Admin" button now | |
await expect( | |
memberRow.getByRole('button', { name: /Admin/i }), | |
).toBeVisible(); | |
// Check Owner's row (Admin CANNOT change Owner) | |
const ownerRow = table.getByRole('row', { name: ownerUser.email }); | |
await expect( | |
ownerRow.getByRole('cell', { name: /Owner/i }), | |
).toBeVisible(); // Just text | |
await expect( | |
ownerRow.getByRole('button', { name: /Owner/i }), | |
).toBeHidden(); // No button | |
await teardownMultipleMembers(data); | |
}); | |
test('should: allow deactivating Members/Admins (not Owners)', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.admin, | |
otherMemberRoles: [ | |
OrganizationMembershipRole.member, | |
OrganizationMembershipRole.owner, | |
], | |
}); | |
const { organization, otherUsers } = data; | |
const memberUser = otherUsers.find( | |
u => u.email.includes('test-member-member'), | |
)!; | |
const ownerUser = otherUsers.find( | |
u => u.email.includes('test-member-owner'), | |
)!; | |
await page.goto(getMembersPagePath(organization.slug)); | |
const table = page.getByRole('table'); | |
// Deactivate Member | |
const memberRow = table.getByRole('row', { name: memberUser.email }); | |
await memberRow.getByRole('button', { name: /Member/i }).click(); | |
const postPromiseDeactivate = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await page.getByRole('button', { name: /^Deactivated$/i }).click(); | |
const responseDeactivate = await postPromiseDeactivate; | |
expect(responseDeactivate.ok()).toBeTruthy(); | |
// Check DB | |
let updatedMembership = | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: memberUser.id, organizationId: organization.id }, | |
); | |
expect(updatedMembership?.deactivatedAt).not.toBeNull(); | |
expect(updatedMembership?.role).toEqual(OrganizationMembershipRole.member); // Role remains | |
// Check UI (shows deactivated text) | |
await expect( | |
memberRow.getByRole('cell', { name: /Deactivated/i }), | |
).toBeVisible(); | |
await expect( | |
memberRow.getByRole('button', { name: /Member/i }), | |
).toBeHidden(); // Button gone | |
// Cannot Deactivate Owner | |
const ownerRow = table.getByRole('row', { name: ownerUser.email }); | |
await expect( | |
ownerRow.getByRole('button', { name: /Owner/i }), | |
).toBeHidden(); // No button to open popover | |
await teardownMultipleMembers(data); | |
}); | |
}); | |
// ======================================================================== | |
// Role: Owner - UI & Functionality Tests | |
// ======================================================================== | |
test.describe('as Owner', () => { | |
test('should: show member list, invite cards, and allow changing ALL roles (except self)', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
otherMemberRoles: [ | |
OrganizationMembershipRole.member, | |
OrganizationMembershipRole.admin, | |
], | |
}); | |
const { organization, requestingUser, otherUsers } = data; | |
const memberUser = otherUsers.find( | |
u => u.email.includes('test-member-member'), | |
)!; | |
const adminUser = otherUsers.find( | |
u => u.email.includes('test-member-admin'), | |
)!; | |
await page.goto(getMembersPagePath(organization.slug)); | |
// Verify Invite Cards are Visible & Owner option available | |
await expect( | |
page.getByRole('heading', { name: /Invite by Email/i }), | |
).toBeVisible(); | |
await expect( | |
page.getByRole('heading', { name: /Invite Link/i }), | |
).toBeVisible(); | |
await page | |
.getByRole('combobox', { name: /Role/i }) | |
.first() | |
.click(); | |
await expect( | |
page.getByRole('option', { name: /Owner/i }), | |
).toBeVisible(); | |
await page.keyboard.press('Escape'); | |
// Verify Member Table & Role Changes | |
const table = page.getByRole('table'); | |
// Check Owner's own row (cannot change self) | |
const ownerRow = table.getByRole('row', { name: requestingUser.email }); | |
await expect( | |
ownerRow.getByRole('cell', { name: /Owner/i }), | |
).toBeVisible(); | |
await expect( | |
ownerRow.getByRole('button', { name: /Owner/i }), | |
).toBeHidden(); | |
// Check Member's row (Owner CAN change Member to Owner) | |
const memberRow = table.getByRole('row', { name: memberUser.email }); | |
await memberRow.getByRole('button', { name: /Member/i }).click(); | |
const postPromiseOwner = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await page.getByRole('button', { name: /^Owner$/i }).click(); | |
const responseOwner = await postPromiseOwner; | |
expect(responseOwner.ok()).toBeTruthy(); | |
let updatedMembership = | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: memberUser.id, organizationId: organization.id }, | |
); | |
expect(updatedMembership?.role).toEqual(OrganizationMembershipRole.owner); | |
await expect( | |
memberRow.getByRole('button', { name: /Owner/i }), | |
).toBeVisible(); // Button text updated | |
// Check Admin's row (Owner CAN change Admin to Member) | |
const adminRow = table.getByRole('row', { name: adminUser.email }); | |
await adminRow.getByRole('button', { name: /Admin/i }).click(); | |
const postPromiseMember = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await page.getByRole('button', { name: /^Member$/i }).click(); | |
const responseMember = await postPromiseMember; | |
expect(responseMember.ok()).toBeTruthy(); | |
updatedMembership = | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: adminUser.id, organizationId: organization.id }, | |
); | |
expect(updatedMembership?.role).toEqual(OrganizationMembershipRole.member); | |
await expect( | |
adminRow.getByRole('button', { name: /Member/i }), | |
).toBeVisible(); | |
await teardownMultipleMembers(data); | |
}); | |
test('should: allow deactivating any other member (Member, Admin, Owner)', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
otherMemberRoles: [ | |
OrganizationMembershipRole.member, | |
OrganizationMembershipRole.admin, | |
OrganizationMembershipRole.owner, // Add another owner | |
], | |
}); | |
const { organization, otherUsers } = data; | |
await page.goto(getMembersPagePath(organization.slug)); | |
const table = page.getByRole('table'); | |
// Deactivate each type of user | |
for (const userToDeactivate of otherUsers) { | |
const initialRole = ( | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: userToDeactivate.id, organizationId: organization.id }, | |
) | |
)?.role!; | |
const roleName = | |
initialRole.charAt(0).toUpperCase() + initialRole.slice(1); // Capitalize | |
const userRow = table.getByRole('row', { | |
name: userToDeactivate.email, | |
}); | |
await userRow.getByRole('button', { name: roleName }).click(); | |
const postPromise = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await page.getByRole('button', { name: /^Deactivated$/i }).click(); | |
const response = await postPromise; | |
expect(response.ok()).toBeTruthy(); | |
// Check DB | |
const updatedMembership = | |
await retrieveOrganizationMembershipFromDatabaseByUserIdAndOrganizationId( | |
{ userId: userToDeactivate.id, organizationId: organization.id }, | |
); | |
expect(updatedMembership?.deactivatedAt).not.toBeNull(); | |
expect(updatedMembership?.role).toEqual(initialRole); // Role preserved | |
// Check UI | |
await expect( | |
userRow.getByRole('cell', { name: /Deactivated/i }), | |
).toBeVisible(); | |
} | |
await teardownMultipleMembers(data); | |
}); | |
}); | |
// ======================================================================== | |
// Invite Link Card Tests (Owner/Admin) | |
// ======================================================================== | |
test.describe('Invite Link Card (as Owner)', () => { | |
test('should: show create button initially, then link UI after creation', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
activeInviteLink: false, // Start with no link | |
}); | |
const { organization } = data; | |
await page.goto(getMembersPagePath(organization.slug)); | |
const createButton = page.getByRole('button', { | |
name: /Create new invite link/i, | |
}); | |
const linkDisplay = page.getByRole('link', { name: /go to link/i }); | |
const regenerateButton = page.getByRole('button', { | |
name: /Regenerate Link/i, | |
}); | |
const deactivateButton = page.getByRole('button', { | |
name: /Deactivate Link/i, | |
}); | |
// Initial state: Create button visible, others hidden | |
await expect(createButton).toBeVisible(); | |
await expect(linkDisplay).toBeHidden(); | |
await expect(regenerateButton).toBeHidden(); | |
await expect(deactivateButton).toBeHidden(); | |
// Click Create | |
const postPromiseCreate = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await createButton.click(); | |
const responseCreate = await postPromiseCreate; | |
expect(responseCreate.ok()).toBeTruthy(); | |
// After creation: Link UI visible, create button hidden | |
await expect(createButton).toBeHidden(); | |
await expect(linkDisplay).toBeVisible(); | |
await expect(regenerateButton).toBeVisible(); | |
await expect(deactivateButton).toBeVisible(); | |
// Check link href structure | |
const href = await linkDisplay.getAttribute('href'); | |
expect(href).toContain('/organizations/invite?token='); | |
await teardownMultipleMembers(data); | |
}); | |
test('should: allow regenerating and deactivating the link', async ({ | |
page, | |
browserName, | |
}) => { | |
// Grant clipboard permissions for copy test | |
// eslint-disable-next-line playwright/no-conditional-in-test | |
if (browserName === 'chromium') { | |
await page.context().grantPermissions(['clipboard-read']); | |
} | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
activeInviteLink: true, // Start with a link | |
}); | |
const { organization } = data; | |
await page.goto(getMembersPagePath(organization.slug)); | |
const createButton = page.getByRole('button', { | |
name: /Create new invite link/i, | |
}); | |
const linkDisplay = page.getByRole('link', { name: /go to link/i }); | |
const copyButton = page.getByRole('button', { | |
name: /copy invite link/i, | |
}); | |
const regenerateButton = page.getByRole('button', { | |
name: /Regenerate Link/i, | |
}); | |
const deactivateButton = page.getByRole('button', { | |
name: /Deactivate Link/i, | |
}); | |
// Initial state (link exists) | |
await expect(createButton).toBeHidden(); | |
await expect(linkDisplay).toBeVisible(); | |
await expect(copyButton).toBeVisible(); | |
await expect(regenerateButton).toBeVisible(); | |
await expect(deactivateButton).toBeVisible(); | |
const initialHref = await linkDisplay.getAttribute('href'); | |
// Test Copy | |
await copyButton.click(); | |
await expect( | |
page.getByRole('button', { name: /invite link copied/i }), | |
).toBeVisible(); | |
// eslint-disable-next-line playwright/no-conditional-in-test | |
if (browserName === 'chromium') { | |
const clipboardText = await page.evaluate( | |
'navigator.clipboard.readText()', | |
); | |
expect(clipboardText).toEqual( | |
expect.stringContaining('/organizations/invite?token='), | |
); | |
} | |
// Test Regenerate | |
const postPromiseRegen = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await regenerateButton.click(); | |
const responseRegen = await postPromiseRegen; | |
expect(responseRegen.ok()).toBeTruthy(); | |
await expect(linkDisplay).toBeVisible(); // Still visible | |
const newHref = await linkDisplay.getAttribute('href'); | |
expect(newHref).not.toEqual(initialHref); // Href changed | |
expect(newHref).toContain('/organizations/invite?token='); | |
// Test Deactivate | |
const postPromiseDeactivate = page.waitForResponse( | |
res => res.url().includes(path) && res.request().method() === 'POST', | |
); | |
await deactivateButton.click(); | |
const responseDeactivate = await postPromiseDeactivate; | |
expect(responseDeactivate.ok()).toBeTruthy(); | |
// After deactivation: Create button visible, others hidden | |
await expect(createButton).toBeVisible(); | |
await expect(linkDisplay).toBeHidden(); | |
await expect(regenerateButton).toBeHidden(); | |
await expect(deactivateButton).toBeHidden(); | |
// Verify in DB | |
const latestLink = | |
await retrieveLatestInviteLinkFromDatabaseByOrganizationId( | |
organization.id, | |
); | |
expect(latestLink).toBeNull(); // No active link | |
await teardownMultipleMembers(data); | |
}); | |
}); | |
// ======================================================================== | |
// Pagination Tests | |
// ======================================================================== | |
test.describe('Pagination', () => { | |
test('should: be hidden when members are <= page size (10)', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
otherMemberRoles: Array.from( | |
{ length: 8 }, // 1 owner + 8 others = 9 total | |
() => OrganizationMembershipRole.member, | |
), | |
}); | |
await page.goto(getMembersPagePath(data.organization.slug)); | |
await expect( | |
page.getByRole('button', { name: /go to previous/i }), | |
).toBeHidden(); | |
await expect( | |
page.getByRole('button', { name: /go to next/i }), | |
).toBeHidden(); | |
await expect( | |
page.getByRole('combobox', { name: /rows per page/i }), | |
).toBeHidden(); // Assuming it hides if only one page | |
await teardownMultipleMembers(data); | |
}); | |
test('should: be visible and functional when members > page size (10)', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
otherMemberRoles: Array.from( | |
{ length: 15 }, // 1 owner + 15 others = 16 total | |
(_, i) => | |
i % 3 === 0 | |
? OrganizationMembershipRole.admin | |
: OrganizationMembershipRole.member, // Mix roles | |
), | |
}); | |
const { organization, allUsers } = data; | |
await page.goto(getMembersPagePath(organization.slug)); | |
const tableBody = page.getByRole('table').locator('tbody'); | |
const firstPageButton = page.getByRole('button', { | |
name: /go to first/i, | |
}); | |
const prevPageButton = page.getByRole('button', { | |
name: /go to previous/i, | |
}); | |
const nextPageButton = page.getByRole('button', { | |
name: /go to next/i, | |
}); | |
const lastPageButton = page.getByRole('button', { | |
name: /go to last/i, | |
}); | |
const pageInfo = page.getByText(/page \d+ of \d+/i); | |
const rowsPerPageSelect = page.getByRole('combobox', { | |
name: /rows per page/i, | |
}); | |
// Initial state (Page 1 of 2, 10 rows) | |
await expect(tableBody.getByRole('row')).toHaveCount(10); | |
await expect(pageInfo).toHaveText('Page 1 of 2'); | |
await expect(firstPageButton).toBeDisabled(); | |
await expect(prevPageButton).toBeDisabled(); | |
await expect(nextPageButton).toBeEnabled(); | |
await expect(lastPageButton).toBeEnabled(); | |
await expect(rowsPerPageSelect).toHaveValue('10'); // Default | |
// Go to next page | |
await nextPageButton.click(); | |
await expect(tableBody.getByRole('row')).toHaveCount(6); // Remaining rows | |
await expect(pageInfo).toHaveText('Page 2 of 2'); | |
await expect(firstPageButton).toBeEnabled(); | |
await expect(prevPageButton).toBeEnabled(); | |
await expect(nextPageButton).toBeDisabled(); | |
await expect(lastPageButton).toBeDisabled(); | |
// Go back to previous page | |
await prevPageButton.click(); | |
await expect(tableBody.getByRole('row')).toHaveCount(10); | |
await expect(pageInfo).toHaveText('Page 1 of 2'); | |
await expect(firstPageButton).toBeDisabled(); // Back to page 1 | |
// Change rows per page | |
await rowsPerPageSelect.selectOption('20'); | |
await expect(tableBody.getByRole('row')).toHaveCount(16); // All rows visible | |
await expect(pageInfo).toHaveText('Page 1 of 1'); | |
await expect(nextPageButton).toBeDisabled(); // Now only one page | |
await teardownMultipleMembers(data); | |
}); | |
}); | |
// ======================================================================== | |
// Accessibility Tests | |
// ======================================================================== | |
test('given: an owner user, should: lack automatically detectable accessibility issues', async ({ | |
page, | |
}) => { | |
const data = await setupMultipleMembers({ | |
page, | |
requestingUserRole: OrganizationMembershipRole.owner, | |
otherMemberRoles: [OrganizationMembershipRole.member], | |
activeInviteLink: true, | |
}); | |
await page.goto(getMembersPagePath(data.organization.slug)); | |
const accessibilityScanResults = await new AxeBuilder({ page }) | |
.disableRules(['color-contrast']) // Often requires manual checks | |
.analyze(); | |
expect(accessibilityScanResults.violations).toEqual([]); | |
await teardownMultipleMembers(data); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment