chore: improve invitation flow, update project dependencies and documentation for Next.js 16 (#408)

* chore: update project dependencies and documentation for Next.js 16

- Upgraded Next.js from version 15 to 16 across various documentation files and components.
- Updated references to Next.js 16 in AGENTS.md and CLAUDE.md for consistency.
- Incremented application version to 2.21.0 in package.json.
- Refactored identity setup components to improve user experience and added confirmation dialogs for authentication methods.
- Enhanced invitation flow with new logic for handling user redirection and token generation.

* refactor: streamline invitation flow in e2e tests

- Simplified the invitation flow test by using a predefined email instead of generating a random one.
- Removed unnecessary steps such as clearing cookies and reloading the page before user sign-up.
- Enhanced clarity by eliminating commented-out code related to identity verification and user membership checks.

* refactor: improve code readability in IdentitiesPage and UpdatePasswordForm components

- Enhanced formatting of JSX elements in IdentitiesPage and UpdatePasswordForm for better readability.
- Adjusted indentation and line breaks to maintain consistent coding style across components.

* refactor: enhance LinkAccountsList component with user redirection logic

- Updated the LinkAccountsList component to include a redirectToPath option in the useLinkIdentityWithProvider hook for improved user experience.
- Removed redundant user hook declaration to streamline the code structure.

* refactor: update account setup logic in JoinTeamAccountPage

- Introduced a check for email-only authentication support to streamline account setup requirements.
- Adjusted the conditions for determining if a new account should set up additional authentication methods, enhancing user experience for new users.
This commit is contained in:
Giancarlo Buomprisco
2025-11-05 11:39:08 +07:00
committed by GitHub
parent ae404d8366
commit fa2fa9a15c
23 changed files with 1005 additions and 154 deletions

View File

@@ -56,7 +56,7 @@ export class AccountPageObject {
password,
);
await this.page.click('[data-test="account-password-form"] button');
await this.page.click('[data-test="identity-form"] button');
}
async deleteAccount(email: string) {

View File

@@ -132,16 +132,32 @@ export class InvitationsPageObject {
await this.page.waitForTimeout(500);
// skip authentication setup
const skipIdentitiesButton = this.page.locator(
'[data-test="skip-identities-button"]',
const continueButton = this.page.locator(
'[data-test="continue-button"]',
);
if (
await skipIdentitiesButton.isVisible({
await continueButton.isVisible({
timeout: 1000,
})
) {
await skipIdentitiesButton.click();
await continueButton.click();
// Handle confirmation dialog that appears when skipping without adding auth
const confirmationDialog = this.page.locator(
'[data-test="no-auth-method-dialog"]',
);
if (
await confirmationDialog.isVisible({
timeout: 2000,
})
) {
console.log('Confirmation dialog appeared, clicking Continue...');
await this.page
.locator('[data-test="no-auth-dialog-continue"]')
.click();
}
}
// wait for redirect to account home

View File

@@ -128,4 +128,529 @@ test.describe('Full Invitation Flow', () => {
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
});
test('new users should be redirected to /identities to set up identity', async ({
page,
}) => {
const invitations = new InvitationsPageObject(page);
await invitations.setup();
await invitations.navigateToMembers();
const newUserEmail = invitations.auth.createRandomEmail();
const invites = [
{
email: newUserEmail,
role: 'member',
},
];
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
await expect(invitations.getInvitations()).toHaveCount(1);
// Sign out current user
await page.context().clearCookies();
await page.reload();
console.log(`Finding invitation email for new user: ${newUserEmail}`);
// Click invitation link from email
await invitations.auth.visitConfirmEmailLink(newUserEmail);
console.log(`New user authenticated, should land on /join page`);
// Verify user lands on /join page
await page.waitForURL('**/join?**');
// Click accept invitation button
const acceptButton = page.locator(
'[data-test="join-team-form"] button[type="submit"]',
);
await acceptButton.click();
console.log(`Checking if new user is redirected to /identities...`);
// NEW USERS should be redirected to /identities to set up auth method
await page.waitForURL('**/identities?**', { timeout: 5000 });
console.log(`✓ New user correctly redirected to /identities`);
// Verify continue button exists (user can skip and set up later)
const continueButton = page.locator('[data-test="continue-button"]');
await expect(continueButton).toBeVisible();
console.log(`Skipping identity setup...`);
// Skip identity setup for now
await continueButton.click();
// Handle confirmation dialog that appears when skipping without adding auth
const confirmationDialog = page.locator(
'[data-test="no-auth-method-dialog"]',
);
if (await confirmationDialog.isVisible({ timeout: 2000 })) {
console.log('Confirmation dialog appeared, clicking Continue...');
await page.locator('[data-test="no-auth-dialog-continue"]').click();
}
// Should redirect to team home after skipping
await page.waitForURL(new RegExp('/home/[a-z0-9-]+'));
console.log(`✓ New user successfully joined team after identity setup`);
// Verify user is now a member
await invitations.teamAccounts.openAccountsSelector();
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
});
test('existing users should skip /identities and go directly to team', async ({
page,
}) => {
const invitations = new InvitationsPageObject(page);
// First, create a user account by signing up
const existingUserEmail = 'test@makerkit.dev';
await invitations.setup();
await invitations.navigateToMembers();
const invites = [
{
email: existingUserEmail,
role: 'member',
},
];
console.log(`Sending invitation to existing user...`);
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
await expect(invitations.getInvitations()).toHaveCount(1);
// Sign out and click invitation as existing user
await page.context().clearCookies();
await page.reload();
console.log(`Existing user clicking invitation link...`);
// Click invitation link from email
await invitations.auth.visitConfirmEmailLink(existingUserEmail, {
deleteAfter: true,
});
// Verify user lands on /join page
await page.waitForURL('**/join?**');
// Click accept invitation button
const acceptButton = page.locator(
'[data-test="join-team-form"] button[type="submit"]',
);
await acceptButton.click();
console.log(`Verifying existing user skips /identities...`);
// EXISTING USERS should skip /identities and go directly to team home
await page.waitForURL(new RegExp('/home/[a-z0-9-]+'), { timeout: 5000 });
console.log(
`✓ Existing user correctly skipped /identities and went directly to team`,
);
});
test('invitation links should work for 7 days (on-the-fly generation)', async ({
page,
}) => {
const invitations = new InvitationsPageObject(page);
await invitations.setup();
await invitations.navigateToMembers();
const newUserEmail = invitations.auth.createRandomEmail();
const invites = [
{
email: newUserEmail,
role: 'member',
},
];
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
await expect(invitations.getInvitations()).toHaveCount(1);
// Get the invitation link from email
console.log(`Getting invitation link from email...`);
// Sign out to access mailbox
await page.context().clearCookies();
await page.reload();
// Visit the invitation link
await invitations.auth.visitConfirmEmailLink(newUserEmail, {
deleteAfter: false, // Keep email for multiple clicks
});
console.log(`✓ First click successful - user authenticated`);
// Verify we're on the join page
await page.waitForURL('**/join?**');
// Don't accept yet - just verify the link works
console.log(`Simulating clicking link again (second time)...`);
// Clear session and click link again
await page.context().clearCookies();
// Visit link again (simulating user clicking expired link)
await invitations.auth.visitConfirmEmailLink(newUserEmail, {
deleteAfter: false,
});
console.log(`✓ Second click successful - link still works!`);
// Should still work and land on join page
await page.waitForURL('**/join?**');
console.log(
`✓ Invitation link works multiple times (on-the-fly token generation)`,
);
// Now accept the invitation
await invitations.acceptInvitation();
// Verify successful
await invitations.teamAccounts.openAccountsSelector();
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
});
});
test.describe('Identity Setup Confirmation Dialog', () => {
test('should show confirmation dialog when skipping without adding auth method', async ({
page,
}) => {
const invitations = new InvitationsPageObject(page);
await invitations.setup();
await invitations.navigateToMembers();
const newUserEmail = invitations.auth.createRandomEmail();
const invites = [
{
email: newUserEmail,
role: 'member',
},
];
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
await expect(invitations.getInvitations()).toHaveCount(1);
// Sign out and accept invitation as new user
await page.context().clearCookies();
await page.reload();
console.log(`New user accepting invitation: ${newUserEmail}`);
await invitations.auth.visitConfirmEmailLink(newUserEmail);
await page.waitForURL('**/join?**');
// Click accept invitation
const acceptButton = page.locator(
'[data-test="join-team-form"] button[type="submit"]',
);
await acceptButton.click();
// Should redirect to /identities
await page.waitForURL('**/identities?**', { timeout: 5000 });
console.log(`✓ Redirected to /identities page`);
// Try to continue WITHOUT adding any auth method
const continueButton = page.locator('[data-test="continue-button"]');
await continueButton.click();
console.log(`Clicked continue without adding auth method...`);
// Confirmation dialog should appear
const confirmDialog = page.locator('[data-test="no-auth-method-dialog"]');
await expect(confirmDialog).toBeVisible({ timeout: 2000 });
console.log(`✓ Confirmation dialog appeared`);
// Verify dialog content
await expect(
page.locator('[data-test="no-auth-dialog-title"]'),
).toBeVisible();
await expect(
page.locator('[data-test="no-auth-dialog-description"]'),
).toBeVisible();
// Verify dialog has cancel and continue buttons
const cancelButton = page.locator('[data-test="no-auth-dialog-cancel"]');
const proceedButton = page.locator('[data-test="no-auth-dialog-continue"]');
await expect(cancelButton).toBeVisible();
await expect(proceedButton).toBeVisible();
console.log(`✓ Dialog has correct content and buttons`);
// Click proceed to continue without auth
await proceedButton.click();
// Should now redirect to team home
await page.waitForURL(new RegExp('/home/[a-z0-9-]+'), { timeout: 5000 });
console.log(`✓ User successfully continued without adding auth method`);
// Verify user joined team
await invitations.teamAccounts.openAccountsSelector();
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
});
test('should NOT show confirmation when user adds password', async ({
page,
}) => {
const invitations = new InvitationsPageObject(page);
await invitations.setup();
await invitations.navigateToMembers();
const newUserEmail = invitations.auth.createRandomEmail();
const invites = [
{
email: newUserEmail,
role: 'member',
},
];
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
// Sign out and accept invitation
await page.context().clearCookies();
await page.reload();
console.log(`New user accepting invitation: ${newUserEmail}`);
await invitations.auth.visitConfirmEmailLink(newUserEmail);
await page.waitForURL('**/join?**');
const acceptButton = page.locator(
'[data-test="join-team-form"] button[type="submit"]',
);
await acceptButton.click();
// Should redirect to /identities
await page.waitForURL('**/identities?**', { timeout: 5000 });
console.log(`Setting up password authentication...`);
// Click to open password dialog
const passwordDialogTrigger = page.locator(
'[data-test="open-password-dialog-trigger"]',
);
await passwordDialogTrigger.click();
// Wait for dialog to open
await page.waitForTimeout(500);
// Add password authentication
const passwordInput = page.locator(
'[data-test="account-password-form-password-input"]',
);
const confirmPasswordInput = page.locator(
'[data-test="account-password-form-repeat-password-input"]',
);
await passwordInput.fill('SecurePassword123!');
await confirmPasswordInput.fill('SecurePassword123!');
const submitPasswordButton = page.locator(
'[data-test="identity-form-submit"]',
);
await submitPasswordButton.click();
// Wait for password to be set
await page.waitForTimeout(1000);
console.log(`✓ Password added`);
// Now click continue
const continueButton = page.locator('[data-test="continue-button"]');
await continueButton.click();
console.log(`Clicked continue after adding password...`);
// Confirmation dialog should NOT appear - should go directly to team
await page.waitForURL(new RegExp('/home/[a-z0-9-]+'), { timeout: 5000 });
// Verify no dialog appeared
const confirmDialog = page.locator('[data-test="no-auth-method-dialog"]');
await expect(confirmDialog).not.toBeVisible();
console.log(
`✓ No confirmation dialog shown - user added authentication method`,
);
// Verify user joined team
await invitations.teamAccounts.openAccountsSelector();
await expect(invitations.teamAccounts.getTeams()).toHaveCount(1);
});
test('user can cancel confirmation dialog and return to add auth', async ({
page,
}) => {
const invitations = new InvitationsPageObject(page);
await invitations.setup();
await invitations.navigateToMembers();
const newUserEmail = invitations.auth.createRandomEmail();
const invites = [
{
email: newUserEmail,
role: 'member',
},
];
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
// Sign out and accept invitation
await page.context().clearCookies();
await page.reload();
await invitations.auth.visitConfirmEmailLink(newUserEmail);
await page.waitForURL('**/join?**');
const acceptButton = page.locator(
'[data-test="join-team-form"] button[type="submit"]',
);
await acceptButton.click();
await page.waitForURL('**/identities?**');
console.log(`Trying to continue without adding auth...`);
// Try to continue without adding auth
const continueButton = page.locator('[data-test="continue-button"]');
await continueButton.click();
// Dialog should appear
const confirmDialog = page.locator('[data-test="no-auth-method-dialog"]');
await expect(confirmDialog).toBeVisible();
console.log(`✓ Confirmation dialog appeared`);
// Click cancel
const cancelButton = page.locator('[data-test="no-auth-dialog-cancel"]');
await cancelButton.click();
console.log(`Clicked cancel button...`);
// Dialog should close and stay on /identities page
await expect(confirmDialog).not.toBeVisible();
await expect(page).toHaveURL(/\/identities/);
console.log(`✓ Dialog closed, still on /identities page`);
// User can now add password - verify the password dialog trigger is available
const passwordDialogTrigger = page.locator(
'[data-test="open-password-dialog-trigger"]',
);
await expect(passwordDialogTrigger).toBeVisible();
console.log(`✓ User can continue to set up authentication`);
});
test('should NOT show confirmation with email-only authentication', async ({
page,
}) => {
// This test assumes email-only auth is configured
// In that case, no confirmation dialog should appear even without adding methods
const invitations = new InvitationsPageObject(page);
await invitations.setup();
await invitations.navigateToMembers();
const newUserEmail = invitations.auth.createRandomEmail();
const invites = [
{
email: newUserEmail,
role: 'member',
},
];
await invitations.openInviteForm();
await invitations.inviteMembers(invites);
await page.context().clearCookies();
await page.reload();
await invitations.auth.visitConfirmEmailLink(newUserEmail);
await page.waitForURL('**/join?**');
const acceptButton = page.locator(
'[data-test="join-team-form"] button[type="submit"]',
);
await acceptButton.click();
// Check if redirected to /identities
const urlAfterAccept = page.url();
if (urlAfterAccept.includes('/identities')) {
console.log(
`Redirected to /identities - checking for password dialog trigger...`,
);
// If password dialog trigger is NOT available, this is email-only mode
const passwordDialogTrigger = page.locator(
'[data-test="open-password-dialog-trigger"]',
);
const isPasswordAvailable = await passwordDialogTrigger
.isVisible({
timeout: 1000,
})
.catch(() => false);
if (!isPasswordAvailable) {
console.log(`✓ Email-only mode detected`);
// Try to continue
const continueButton = page.locator('[data-test="continue-button"]');
if (await continueButton.isVisible({ timeout: 1000 })) {
await continueButton.click();
// No confirmation dialog should appear in email-only mode
const confirmDialog = page.locator(
'[data-test="no-auth-method-dialog"]',
);
await expect(confirmDialog).not.toBeVisible({ timeout: 2000 });
console.log(
`✓ No confirmation dialog in email-only mode - continuing directly`,
);
}
}
}
// Verify user can complete flow regardless
console.log(`✓ User successfully completed invitation flow`);
});
});