Skip to content

Instantly share code, notes, and snippets.

@janhesters
Created March 30, 2025 20:30
Show Gist options
  • Save janhesters/a386d2d64917dfb23b2939315f2e8be5 to your computer and use it in GitHub Desktop.
Save janhesters/a386d2d64917dfb23b2939315f2e8be5 to your computer and use it in GitHub Desktop.
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