* fix: enhance DataTable pagination examples and improve display logic - Added a note in the DataTableStory component to clarify that examples show only the first page of data for demonstration purposes. - Adjusted pagination examples to reflect smaller datasets, changing the displayed data slices for better clarity and testing. - Updated the Pagination component to calculate and display the current record range more accurately based on the current page index and size. * chore(dependencies): update package versions for improved compatibility - Upgraded `@supabase/supabase-js` from `2.55.0` to `2.57.0` for enhanced functionality and performance. - Bumped `@tanstack/react-query` from `5.85.5` to `5.85.9` to incorporate the latest improvements. - Updated `ai` from `5.0.28` to `5.0.30` for better performance. - Incremented `nodemailer` from `7.0.5` to `7.0.6` for stability. - Updated `typescript-eslint` from `8.41.0` to `8.42.0` for improved type definitions and linting capabilities. - Adjusted various package dependencies across multiple components to ensure compatibility and stability. * chore(dependencies): update package versions for improved compatibility - Upgraded `@ai-sdk/openai` from `2.0.23` to `2.0.24` for enhanced functionality. - Bumped `@tanstack/react-query` from `5.85.9` to `5.86.0` to incorporate the latest improvements. - Updated `ai` from `5.0.30` to `5.0.33` for better performance. - Incremented `@types/node` from `24.3.0` to `24.3.1` for type safety enhancements. - Updated `dotenv` from `17.2.1` to `17.2.2` for stability. - Adjusted `tailwindcss` and related packages to `4.1.13` for improved styling capabilities. - Updated `react-i18next` from `15.7.3` to `15.7.3` to include the latest localization fixes. - Incremented `@sentry/nextjs` from `10.8.0` to `10.10.0` for enhanced monitoring features. - Updated various package dependencies across multiple components to ensure compatibility and stability. * fix(config): conditionally disable `devIndicators` in CI environment * feat(settings): encapsulate danger zone actions in a styled card component - Introduced a new `DangerZoneCard` component to enhance the visual presentation of danger zone actions in the team account settings. - Updated `TeamAccountDangerZone` to wrap deletion and leave actions within the `DangerZoneCard` for improved user experience. - Removed redundant card structure from `TeamAccountSettingsContainer` to streamline the component hierarchy. * fix(e2e): improve admin account tests for response handling and visibility checks - Enhanced the admin test suite by adding a check for the POST request method when waiting for the response from the `/admin/accounts` endpoint. - Reduced wait times in the `filterAccounts` function for improved test performance. - Updated the `selectAccount` function to ensure the account link is visible before clicking, enhancing reliability in the test flow. * chore(dependencies): update package versions for improved compatibility - Upgraded `@supabase/supabase-js` from `2.57.0` to `2.57.2` for enhanced functionality and performance. - Bumped `@tanstack/react-query` from `5.86.0` to `5.87.1` to incorporate the latest improvements. - Updated `i18next` from `25.5.1` to `25.5.2` for better localization support. - Incremented `eslint` from `9.34.0` to `9.35.0` for improved linting capabilities. - Adjusted various package dependencies across multiple components to ensure compatibility and stability. * feat(admin): enhance user ban and reactivation actions with success handling - Updated `AdminBanUserDialog` and `AdminReactivateUserDialog` components to handle success states based on the results of the respective actions. - Modified `banUserAction` and `reactivateUserAction` to return success status and log errors if the actions fail. - Introduced `revalidatePage` function to refresh the user account page after banning or reactivating a user. - Improved error handling in the dialogs to provide better feedback to the admin user. * feat(admin): refactor user ban and reactivation dialogs for improved structure and error handling - Introduced `BanUserForm` and `ReactivateUserForm` components to encapsulate form logic within the respective dialogs, enhancing readability and maintainability. - Updated the `AdminBanUserDialog` and `AdminReactivateUserDialog` components to utilize the new form components, streamlining the user interface. - Enhanced error handling to provide clearer feedback to the admin user during ban and reactivation actions. - Removed unnecessary revalidation calls in the server actions to optimize performance and maintain clarity in the action flow. - Added `@types/react-dom` dependency for improved type definitions. * refactor(admin): streamline user dialogs and server actions for improved clarity - Removed unnecessary `useRouter` imports from `AdminBanUserDialog` and `AdminReactivateUserDialog` components to simplify the code. - Updated `revalidateAdmin` function calls to use `revalidatePath` with specific paths, enhancing clarity in the server actions. - Ensured that the user dialogs maintain a clean structure while focusing on form logic and error handling.
408 lines
11 KiB
TypeScript
408 lines
11 KiB
TypeScript
import { Page, expect, selectors, test } from '@playwright/test';
|
|
|
|
import { AuthPageObject } from '../authentication/auth.po';
|
|
import { TeamAccountsPageObject } from '../team-accounts/team-accounts.po';
|
|
|
|
const MFA_KEY = 'NHOHJVGPO3R3LKVPRMNIYLCDMBHUM2SE';
|
|
|
|
test.describe('Admin Auth flow without MFA', () => {
|
|
test('will return a 404 for non-admin users', async ({ page }) => {
|
|
const auth = new AuthPageObject(page);
|
|
|
|
await page.goto('/auth/sign-in');
|
|
|
|
await auth.signIn({
|
|
email: 'owner@makerkit.dev',
|
|
password: 'testingpassword',
|
|
});
|
|
|
|
await page.waitForURL('/home');
|
|
|
|
await page.goto('/admin');
|
|
|
|
expect(page.url()).toContain('/404');
|
|
});
|
|
|
|
test('will redirect to 404 for admin users without MFA', async ({ page }) => {
|
|
const auth = new AuthPageObject(page);
|
|
|
|
await page.goto('/auth/sign-in');
|
|
|
|
await auth.signIn({
|
|
email: 'test@makerkit.dev',
|
|
password: 'testingpassword',
|
|
});
|
|
|
|
await page.waitForURL('/home');
|
|
|
|
await page.goto('/admin');
|
|
|
|
expect(page.url()).toContain('/404');
|
|
});
|
|
});
|
|
|
|
test.describe('Admin', () => {
|
|
// must be serial because OTP verification is not working in parallel
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.describe('Admin Dashboard', () => {
|
|
test('displays all stat cards', async ({ page }) => {
|
|
await goToAdmin(page);
|
|
|
|
// Check all stat cards are present
|
|
await expect(page.getByRole('heading', { name: 'Users' })).toBeVisible();
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Team Accounts' }),
|
|
).toBeVisible();
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Paying Customers' }),
|
|
).toBeVisible();
|
|
|
|
await expect(page.getByRole('heading', { name: 'Trials' })).toBeVisible();
|
|
|
|
// Verify stat values are numbers
|
|
const stats = await page.$$('.text-3xl.font-bold');
|
|
|
|
for (const stat of stats) {
|
|
const value = await stat.textContent();
|
|
expect(Number.isInteger(Number(value))).toBeTruthy();
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Personal Account Management', () => {
|
|
let testUserEmail: string;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
selectors.setTestIdAttribute('data-test');
|
|
|
|
// Create a new test user before each test
|
|
testUserEmail = await createUser(page);
|
|
|
|
await goToAdmin(page);
|
|
|
|
// Navigate to the newly created user's account page
|
|
// Note: We need to get the user's ID from the email - this might need adjustment
|
|
// based on your URL structure
|
|
await page.goto(`/admin/accounts`);
|
|
|
|
// use the email as the filter text
|
|
const filterText = testUserEmail;
|
|
|
|
await filterAccounts(page, filterText);
|
|
await selectAccount(page, filterText);
|
|
});
|
|
|
|
test('displays personal account details', async ({ page }) => {
|
|
await expect(page.getByText('Personal Account')).toBeVisible();
|
|
await expect(page.getByTestId('admin-ban-account-button')).toBeVisible();
|
|
await expect(page.getByTestId('admin-impersonate-button')).toBeVisible();
|
|
await expect(
|
|
page.getByTestId('admin-delete-account-button'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('ban user flow', async ({ page }) => {
|
|
await page.getByTestId('admin-ban-account-button').click();
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Ban User' }),
|
|
).toBeVisible();
|
|
|
|
// Try with invalid confirmation
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
|
await page.getByRole('button', { name: 'Ban User' }).click();
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Ban User' }),
|
|
).toBeVisible(); // Dialog should still be open
|
|
|
|
// Confirm with correct text
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
|
|
|
await Promise.all([
|
|
page.getByRole('button', { name: 'Ban User' }).click(),
|
|
page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes('/admin/accounts') &&
|
|
response.request().method() === 'POST',
|
|
),
|
|
]);
|
|
|
|
await expect(page.getByText('Banned')).toBeVisible();
|
|
|
|
await page.context().clearCookies();
|
|
|
|
// Verify user can't log in
|
|
await page.goto('/auth/sign-in');
|
|
|
|
const auth = new AuthPageObject(page);
|
|
|
|
await auth.signIn({
|
|
email: testUserEmail,
|
|
password: 'testingpassword',
|
|
});
|
|
|
|
// Should show an error message
|
|
await expect(
|
|
page.locator('[data-test="auth-error-message"]'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('reactivate user flow', async ({ page }) => {
|
|
// First ban the user
|
|
await page.getByTestId('admin-ban-account-button').click();
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
|
await page.getByRole('button', { name: 'Ban User' }).click();
|
|
|
|
await expect(page.getByText('Banned')).toBeVisible();
|
|
|
|
// Now reactivate
|
|
await page.getByTestId('admin-reactivate-account-button').click();
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Reactivate User' }),
|
|
).toBeVisible();
|
|
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
|
|
|
await Promise.all([
|
|
page.getByRole('button', { name: 'Reactivate User' }).click(),
|
|
page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes('/admin/accounts') &&
|
|
response.request().method() === 'POST',
|
|
),
|
|
]);
|
|
|
|
await page.waitForTimeout(250);
|
|
|
|
// Verify ban badge is removed
|
|
await expect(page.getByText('Banned')).not.toBeVisible();
|
|
|
|
// Log out
|
|
await page.context().clearCookies();
|
|
|
|
// Verify user can log in again
|
|
await page.goto('/auth/sign-in');
|
|
|
|
const auth = new AuthPageObject(page);
|
|
|
|
await auth.signIn({
|
|
email: testUserEmail,
|
|
password: 'testingpassword',
|
|
});
|
|
|
|
await page.waitForURL('/home');
|
|
});
|
|
|
|
test('impersonate user flow', async ({ page }) => {
|
|
await page.getByTestId('admin-impersonate-button').click();
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Impersonate User' }),
|
|
).toBeVisible();
|
|
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
|
await page.getByRole('button', { name: 'Impersonate User' }).click();
|
|
|
|
// Should redirect to home and be logged in as the user
|
|
await page.waitForURL('/home');
|
|
});
|
|
|
|
test('delete user flow', async ({ page }) => {
|
|
await page.getByTestId('admin-delete-account-button').click();
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Delete User' }),
|
|
).toBeVisible();
|
|
|
|
// Try with invalid confirmation
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
|
|
|
await page.getByRole('button', { name: 'Delete' }).click();
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Delete User' }),
|
|
).toBeVisible(); // Dialog should still be open
|
|
|
|
// Confirm with correct text
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
|
|
|
await page.getByRole('button', { name: 'Delete' }).click();
|
|
|
|
// Should redirect to admin dashboard
|
|
await page.waitForURL('/admin/accounts');
|
|
|
|
// Log out
|
|
await page.context().clearCookies();
|
|
await page.waitForURL('/');
|
|
|
|
// Verify user can't log in
|
|
await page.goto('/auth/sign-in');
|
|
|
|
const auth = new AuthPageObject(page);
|
|
|
|
await auth.signIn({
|
|
email: testUserEmail,
|
|
password: 'testingpassword',
|
|
});
|
|
|
|
// Should show an error message
|
|
await expect(
|
|
page.locator('[data-test="auth-error-message"]'),
|
|
).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test.describe('Team Account Management', () => {
|
|
test.skip(
|
|
process.env.ENABLE_TEAM_ACCOUNT_TESTS !== 'true',
|
|
'Team account tests are disabled',
|
|
);
|
|
|
|
let testUserEmail: string;
|
|
let teamName: string;
|
|
let slug: string;
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
selectors.setTestIdAttribute('data-test');
|
|
|
|
// Create a new test user and team account
|
|
testUserEmail = await createUser(page, {
|
|
afterSignIn: async () => {
|
|
teamName = `test-${Math.random().toString(36).substring(2, 15)}`;
|
|
|
|
const teamAccountPo = new TeamAccountsPageObject(page);
|
|
const teamSlug = teamName.toLowerCase().replace(/ /g, '-');
|
|
|
|
slug = teamSlug;
|
|
|
|
await teamAccountPo.createTeam({
|
|
teamName,
|
|
slug,
|
|
});
|
|
},
|
|
});
|
|
|
|
await goToAdmin(page);
|
|
|
|
await page.goto(`/admin/accounts`);
|
|
|
|
await filterAccounts(page, teamName);
|
|
await selectAccount(page, teamName);
|
|
});
|
|
|
|
test('displays team account details', async ({ page }) => {
|
|
await expect(page.getByText('Team Account')).toBeVisible();
|
|
await expect(
|
|
page.getByTestId('admin-delete-account-button'),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test('delete team account flow', async ({ page }) => {
|
|
await page.getByTestId('admin-delete-account-button').click();
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Delete Account' }),
|
|
).toBeVisible();
|
|
|
|
// Try with invalid confirmation
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'WRONG');
|
|
await page.getByRole('button', { name: 'Delete' }).click();
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Delete Account' }),
|
|
).toBeVisible(); // Dialog should still be open
|
|
|
|
// Confirm with correct text
|
|
await page.fill('[placeholder="Type CONFIRM to confirm"]', 'CONFIRM');
|
|
await page.getByRole('button', { name: 'Delete' }).click();
|
|
|
|
// Should redirect to admin dashboard after deletion
|
|
await expect(page).toHaveURL('/admin/accounts');
|
|
});
|
|
});
|
|
});
|
|
|
|
async function goToAdmin(page: Page) {
|
|
const auth = new AuthPageObject(page);
|
|
|
|
await page.goto('/auth/sign-in');
|
|
|
|
await auth.signIn({
|
|
email: 'super-admin@makerkit.dev',
|
|
password: 'testingpassword',
|
|
});
|
|
|
|
await page.waitForURL('/auth/verify');
|
|
await page.waitForTimeout(250);
|
|
|
|
await expect(async () => {
|
|
await auth.submitMFAVerification(MFA_KEY);
|
|
await page.waitForURL('/home');
|
|
}).toPass({
|
|
intervals: [
|
|
500, 2500, 5000, 7500, 10_000, 15_000, 20_000, 25_000, 30_000, 35_000,
|
|
40_000, 45_000, 50_000,
|
|
],
|
|
});
|
|
|
|
await page.goto('/admin');
|
|
}
|
|
|
|
async function createUser(
|
|
page: Page,
|
|
params: {
|
|
afterSignIn?: () => Promise<void>;
|
|
} = {},
|
|
) {
|
|
const auth = new AuthPageObject(page);
|
|
const password = 'testingpassword';
|
|
const email = auth.createRandomEmail();
|
|
|
|
// sign up
|
|
await page.goto('/auth/sign-up');
|
|
|
|
await auth.signUp({
|
|
email,
|
|
password,
|
|
repeatPassword: password,
|
|
});
|
|
|
|
// confirm email
|
|
await auth.visitConfirmEmailLink(email);
|
|
|
|
if (params.afterSignIn) {
|
|
await params.afterSignIn();
|
|
}
|
|
|
|
// sign out
|
|
await auth.signOut();
|
|
await page.waitForURL('/');
|
|
|
|
// return the email
|
|
return email;
|
|
}
|
|
|
|
async function filterAccounts(page: Page, email: string) {
|
|
await page
|
|
.locator('[data-test="admin-accounts-table-filter-input"]')
|
|
.first()
|
|
.fill(email);
|
|
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForTimeout(250);
|
|
}
|
|
|
|
async function selectAccount(page: Page, email: string) {
|
|
const link = page
|
|
.locator('tr', { hasText: email.split('@')[0] })
|
|
.locator('a');
|
|
|
|
await expect(link).toBeVisible();
|
|
|
|
await link.click();
|
|
|
|
await page.waitForURL(new RegExp(`/admin/accounts/[a-z0-9-]+`));
|
|
await page.waitForTimeout(500);
|
|
}
|