From 3fc1975a9d29a4ff30d5160c5af35d82856b31c0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 17 Nov 2025 12:12:56 +0800 Subject: [PATCH] chore: organize test --- cypress/e2e/account/avatar/avatar-api.cy.ts | 10 +- .../e2e/account/avatar/avatar-header.cy.ts | 14 +- .../account/avatar/avatar-notifications.cy.ts | 4 +- .../account/avatar/avatar-persistence.cy.ts | 6 +- .../e2e/account/avatar/avatar-priority.cy.ts | 4 +- .../e2e/account/avatar/avatar-test-utils.ts | 10 +- cypress/e2e/account/avatar/avatar-types.cy.ts | 6 +- cypress/e2e/account/update-user-profile.cy.ts | 41 ++- cypress/e2e/app/sidebar-components.cy.ts | 4 +- cypress/e2e/auth/login-logout.cy.ts | 14 +- cypress/e2e/auth/oauth-login.cy.ts | 10 +- cypress/e2e/auth/otp-login.cy.ts | 62 ++-- cypress/e2e/auth/password-login.cy.ts | 48 ++-- cypress/e2e/chat/chat-input.cy.ts | 58 ++-- cypress/e2e/chat/create-ai-chat.cy.ts | 21 +- .../chat/model-selection-persistence.cy.ts | 17 +- cypress/e2e/chat/selection-mode.cy.ts | 15 +- cypress/e2e/database/checkbox-column.cy.ts | 11 +- cypress/e2e/database/datetime-column.cy.ts | 11 +- .../e2e/database/grid-edit-operations.cy.ts | 8 +- cypress/e2e/database/row-deletion.cy.ts | 8 +- cypress/e2e/database/row-duplication.cy.ts | 8 +- cypress/e2e/database/row-insertion.cy.ts | 8 +- .../e2e/database/single-select-column.cy.ts | 10 +- cypress/e2e/editor/document-editing.cy.ts | 6 +- .../e2e/editor/slash-menu-formatting.cy.ts | 6 +- cypress/e2e/editor/slash-menu-lists.cy.ts | 6 +- cypress/e2e/editor/slash-menu-media.cy.ts | 6 +- cypress/e2e/editor/slash-menu.cy.ts | 6 +- cypress/e2e/editor/text-formatting.cy.ts | 6 +- cypress/e2e/page/breadcrumb-navigation.cy.ts | 34 +-- cypress/e2e/page/create-delete-page.cy.ts | 11 +- .../e2e/page/delete-page-verify-trash.cy.ts | 35 +-- cypress/e2e/page/edit-page.cy.ts | 8 +- cypress/e2e/page/more-page-action.cy.ts | 13 +- cypress/e2e/page/publish-page.cy.ts | 74 +++-- cypress/e2e/page/share-page.cy.ts | 83 +++--- cypress/e2e/space/create-space.cy.ts | 23 +- cypress/e2e/user/user.cy.ts | 14 +- cypress/support/api-mocks.ts | 213 ++++++++++++++ cypress/support/chat-mocks.ts | 268 ++++++++++++++++++ cypress/support/exception-handlers.ts | 80 ++++++ cypress/support/selectors.ts | 17 +- cypress/support/test-config.ts | 75 +++++ cypress/support/test-helpers.ts | 245 ++++++++++++++++ 45 files changed, 1226 insertions(+), 411 deletions(-) create mode 100644 cypress/support/api-mocks.ts create mode 100644 cypress/support/chat-mocks.ts create mode 100644 cypress/support/exception-handlers.ts create mode 100644 cypress/support/test-config.ts create mode 100644 cypress/support/test-helpers.ts diff --git a/cypress/e2e/account/avatar/avatar-api.cy.ts b/cypress/e2e/account/avatar/avatar-api.cy.ts index dd5f27f9..ab8702c6 100644 --- a/cypress/e2e/account/avatar/avatar-api.cy.ts +++ b/cypress/e2e/account/avatar/avatar-api.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { byTestId } from '../../../support/selectors'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateUserMetadata, AuthTestUtils, AvatarSelectors, WorkspaceSelectors } = imports; @@ -37,7 +38,7 @@ describe('Avatar API', () => { cy.task('log', 'Step 5: Open Account Settings to verify avatar'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); cy.task('log', 'Step 6: Verify avatar image is displayed in Account Settings'); @@ -48,7 +49,7 @@ describe('Avatar API', () => { // Wait for any avatar image to be present and loaded // The AvatarImage component loads asynchronously and sets opacity to 0 while loading - cy.get('[data-testid="avatar-image"]', { timeout: 10000 }) + cy.get(byTestId('avatar-image'), { timeout: 10000 }) .should('exist') .should(($imgs) => { // Find the first visible avatar image (opacity not 0) @@ -66,7 +67,7 @@ describe('Avatar API', () => { }); // Verify that the avatar image has loaded (check for non-empty src and visible state) - cy.get('[data-testid="avatar-image"]').then(($imgs) => { + cy.get(byTestId('avatar-image')).then(($imgs) => { let foundLoaded = false; $imgs.each((index, img) => { const $img = Cypress.$(img); @@ -158,7 +159,7 @@ describe('Avatar API', () => { cy.task('log', 'Step 5: Open Account Settings'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); cy.task('log', 'Step 6: Verify emoji is displayed in fallback'); @@ -190,4 +191,3 @@ describe('Avatar API', () => { }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-header.cy.ts b/cypress/e2e/account/avatar/avatar-header.cy.ts index 922ff82c..23e0b0d5 100644 --- a/cypress/e2e/account/avatar/avatar-header.cy.ts +++ b/cypress/e2e/account/avatar/avatar-header.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { byTestIdContains } from '../../../support/selectors'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { APP_EVENTS, updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils } = imports; @@ -39,8 +40,8 @@ describe('Avatar Header Display', () => { // Click on a page to open editor cy.get('body').then(($body) => { // Try to find and click on a page in the sidebar - if ($body.find('[data-testid*="page"]').length > 0) { - cy.get('[data-testid*="page"]').first().click(); + if ($body.find(byTestIdContains('page')).length > 0) { + cy.get(byTestIdContains('page')).first().click(); } else if ($body.text().includes('Getting started')) { cy.contains('Getting started').click(); } @@ -117,8 +118,8 @@ describe('Avatar Header Display', () => { cy.task('log', 'Step 4: Interact with editor to trigger collaborative user awareness'); // Click on a page to open editor cy.get('body').then(($body) => { - if ($body.find('[data-testid*="page"]').length > 0) { - cy.get('[data-testid*="page"]').first().click(); + if ($body.find(byTestIdContains('page')).length > 0) { + cy.get(byTestIdContains('page')).first().click(); } else if ($body.text().includes('Getting started')) { cy.contains('Getting started').click(); } @@ -213,8 +214,8 @@ describe('Avatar Header Display', () => { cy.task('log', 'Step 6: Interact with editor to trigger collaborative user awareness'); // Click on a page to open editor cy.get('body').then(($body) => { - if ($body.find('[data-testid*="page"]').length > 0) { - cy.get('[data-testid*="page"]').first().click(); + if ($body.find(byTestIdContains('page')).length > 0) { + cy.get(byTestIdContains('page')).first().click(); } else if ($body.text().includes('Getting started')) { cy.contains('Getting started').click(); } @@ -263,4 +264,3 @@ describe('Avatar Header Display', () => { }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-notifications.cy.ts b/cypress/e2e/account/avatar/avatar-notifications.cy.ts index 6b6494a5..d83d558d 100644 --- a/cypress/e2e/account/avatar/avatar-notifications.cy.ts +++ b/cypress/e2e/account/avatar/avatar-notifications.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { byTestId } from '../../../support/selectors'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { APP_EVENTS, updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -61,7 +62,7 @@ describe('Avatar Notifications', () => { cy.task('log', 'Step 7: Open Account Settings to verify avatar'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); cy.task('log', 'Step 8: Verify avatar image uses updated URL'); @@ -183,4 +184,3 @@ describe('Avatar Notifications', () => { }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-persistence.cy.ts b/cypress/e2e/account/avatar/avatar-persistence.cy.ts index b883322c..6ac83da2 100644 --- a/cypress/e2e/account/avatar/avatar-persistence.cy.ts +++ b/cypress/e2e/account/avatar/avatar-persistence.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { byTestId } from '../../../support/selectors'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -39,7 +40,7 @@ describe('Avatar Persistence', () => { cy.task('log', 'Step 5: Verify avatar persisted'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', testAvatarUrl); @@ -50,7 +51,7 @@ describe('Avatar Persistence', () => { WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', testAvatarUrl); @@ -58,4 +59,3 @@ describe('Avatar Persistence', () => { }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-priority.cy.ts b/cypress/e2e/account/avatar/avatar-priority.cy.ts index f92758d5..59ea4924 100644 --- a/cypress/e2e/account/avatar/avatar-priority.cy.ts +++ b/cypress/e2e/account/avatar/avatar-priority.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { byTestId } from '../../../support/selectors'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateUserMetadata, updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -45,7 +46,7 @@ describe('Avatar Priority', () => { cy.task('log', 'Step 5: Verify workspace avatar is displayed (priority)'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); // Workspace avatar should be displayed, not user metadata avatar @@ -54,4 +55,3 @@ describe('Avatar Priority', () => { }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-test-utils.ts b/cypress/e2e/account/avatar/avatar-test-utils.ts index 1b837c4e..78b23b0b 100644 --- a/cypress/e2e/account/avatar/avatar-test-utils.ts +++ b/cypress/e2e/account/avatar/avatar-test-utils.ts @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; - import { APP_EVENTS } from '../../../../src/application/constants'; import { updateUserMetadata, updateWorkspaceMemberAvatar } from '../../../support/api-utils'; @@ -7,13 +5,16 @@ import { AuthTestUtils } from '../../../support/auth-utils'; import { AvatarSelectors } from '../../../support/avatar-selectors'; import { dbUtils } from '../../../support/db-utils'; import { WorkspaceSelectors } from '../../../support/selectors'; +import { generateRandomEmail, getTestEnvironment } from '../../../support/test-config'; + +const appflowyEnv = getTestEnvironment(); /** * Shared utilities and setup for avatar tests */ export const avatarTestUtils = { - generateRandomEmail: () => `${uuidv4()}@appflowy.io`, - APPFLOWY_BASE_URL: Cypress.env('APPFLOWY_BASE_URL'), + generateRandomEmail, + APPFLOWY_BASE_URL: appflowyEnv.appflowyBaseUrl, /** * Common beforeEach setup for avatar tests @@ -47,4 +48,3 @@ export const avatarTestUtils = { WorkspaceSelectors, }, }; - diff --git a/cypress/e2e/account/avatar/avatar-types.cy.ts b/cypress/e2e/account/avatar/avatar-types.cy.ts index beea62ac..9c74272b 100644 --- a/cypress/e2e/account/avatar/avatar-types.cy.ts +++ b/cypress/e2e/account/avatar/avatar-types.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { byTestId } from '../../../support/selectors'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -36,7 +37,7 @@ describe('Avatar Types', () => { WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', httpsAvatar); @@ -73,7 +74,7 @@ describe('Avatar Types', () => { WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + cy.get(byTestId('account-settings-button')).click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); // Emoji should be displayed in fallback, not as image @@ -83,4 +84,3 @@ describe('Avatar Types', () => { }); }); }); - diff --git a/cypress/e2e/account/update-user-profile.cy.ts b/cypress/e2e/account/update-user-profile.cy.ts index 3f395a50..c96cb49c 100644 --- a/cypress/e2e/account/update-user-profile.cy.ts +++ b/cypress/e2e/account/update-user-profile.cy.ts @@ -1,9 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; +import { WorkspaceSelectors, byTestId } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Update User Profile', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -34,70 +33,70 @@ describe('Update User Profile', () => { // Open workspace dropdown cy.log('Step 3: Opening workspace dropdown'); - cy.get('[data-testid="workspace-dropdown-trigger"]', { timeout: 10000 }).should('be.visible').click(); + WorkspaceSelectors.dropdownTrigger().should('be.visible').click(); // Wait for dropdown to open - cy.get('[data-testid="workspace-dropdown-content"]', { timeout: 5000 }).should('be.visible'); + WorkspaceSelectors.dropdownContent().should('be.visible'); // Click on Account Settings cy.log('Step 4: Opening Account Settings'); - cy.get('[data-testid="account-settings-button"]').should('be.visible').click(); + cy.get(byTestId('account-settings-button')).should('be.visible').click(); // Add a wait to ensure the dialog has time to open cy.wait(1000); // Wait for Account Settings dialog to open cy.log('Step 5: Verifying Account Settings dialog opened'); - cy.get('[data-testid="account-settings-dialog"]', { timeout: 10000 }).should('be.visible'); + cy.get(byTestId('account-settings-dialog'), { timeout: 10000 }).should('be.visible'); // Check initial date format (should be Month/Day/Year) cy.log('Step 6: Checking initial date format'); - cy.get('[data-testid="date-format-dropdown"]').should('be.visible'); + cy.get(byTestId('date-format-dropdown')).should('be.visible'); // Test Date Format change - select Year/Month/Day cy.log('Step 7: Testing Date Format change to Year/Month/Day'); - cy.get('[data-testid="date-format-dropdown"]').click(); + cy.get(byTestId('date-format-dropdown')).click(); cy.wait(500); // Select US format (value 1) which is Year/Month/Day - cy.get('[data-testid="date-format-1"]').should('be.visible').click(); + cy.get(byTestId('date-format-1')).should('be.visible').click(); cy.wait(3000); // Wait for API call to complete // Verify the dropdown now shows Year/Month/Day - cy.get('[data-testid="date-format-dropdown"]').should('contain.text', 'Year/Month/Day'); + cy.get(byTestId('date-format-dropdown')).should('contain.text', 'Year/Month/Day'); // Test Time Format change cy.log('Step 8: Testing Time Format change'); - cy.get('[data-testid="time-format-dropdown"]').should('be.visible').click(); + cy.get(byTestId('time-format-dropdown')).should('be.visible').click(); cy.wait(500); // Select 24-hour format (value 1) - cy.get('[data-testid="time-format-1"]').should('be.visible').click(); + cy.get(byTestId('time-format-1')).should('be.visible').click(); cy.wait(3000); // Wait for API call to complete // Verify the dropdown now shows 24-hour format - cy.get('[data-testid="time-format-dropdown"]').should('contain.text', '24'); + cy.get(byTestId('time-format-dropdown')).should('contain.text', '24'); // Test Start Week On change cy.log('Step 9: Testing Start Week On change'); - cy.get('[data-testid="start-week-on-dropdown"]').should('be.visible').click(); + cy.get(byTestId('start-week-on-dropdown')).should('be.visible').click(); cy.wait(500); // Select Monday (value 1) - cy.get('[data-testid="start-week-1"]').should('be.visible').click(); + cy.get(byTestId('start-week-1')).should('be.visible').click(); cy.wait(3000); // Wait for API call to complete - cy.get('[data-testid="start-week-on-dropdown"]').should('contain.text', 'Monday'); + cy.get(byTestId('start-week-on-dropdown')).should('contain.text', 'Monday'); // The settings should remain selected in the current session cy.log('Step 10: Verifying all settings are showing correctly'); // Verify all dropdowns still show the selected values - cy.get('[data-testid="date-format-dropdown"]').should('contain.text', 'Year/Month/Day'); - cy.get('[data-testid="time-format-dropdown"]').should('contain.text', '24'); - cy.get('[data-testid="start-week-on-dropdown"]').should('contain.text', 'Monday'); + cy.get(byTestId('date-format-dropdown')).should('contain.text', 'Year/Month/Day'); + cy.get(byTestId('time-format-dropdown')).should('contain.text', '24'); + cy.get(byTestId('start-week-on-dropdown')).should('contain.text', 'Monday'); cy.log('Test completed: User profile settings updated successfully'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/app/sidebar-components.cy.ts b/cypress/e2e/app/sidebar-components.cy.ts index ec5aab52..1137567b 100644 --- a/cypress/e2e/app/sidebar-components.cy.ts +++ b/cypress/e2e/app/sidebar-components.cy.ts @@ -1,9 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { PageSelectors, SidebarSelectors } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Sidebar Components Resilience Tests', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; beforeEach(() => { @@ -206,4 +205,3 @@ describe('Sidebar Components Resilience Tests', () => { }); }); }); - diff --git a/cypress/e2e/auth/login-logout.cy.ts b/cypress/e2e/auth/login-logout.cy.ts index 5629e072..390b09de 100644 --- a/cypress/e2e/auth/login-logout.cy.ts +++ b/cypress/e2e/auth/login-logout.cy.ts @@ -1,4 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; import { @@ -6,11 +5,10 @@ import { AuthSelectors, waitForReactUpdate } from '../../support/selectors'; +import { TestConfig, generateRandomEmail } from '../../support/test-config'; describe('Login and Logout Flow', () => { - const baseUrl = Cypress.config('baseUrl') || 'http://localhost:3000'; - const gotrueUrl = Cypress.env('APPFLOWY_GOTRUE_BASE_URL') || 'http://localhost/gotrue'; - const apiUrl = Cypress.env('APPFLOWY_BASE_URL') || 'http://localhost'; + const { baseUrl, gotrueUrl, apiUrl } = TestConfig; beforeEach(() => { // Handle uncaught exceptions @@ -27,7 +25,7 @@ describe('Login and Logout Flow', () => { describe('Test Case 1: Complete Login and Logout Flow', () => { it('should login and successfully logout with detailed verification', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); cy.log(`[TEST START] Complete Login and Logout Flow - Email: ${testEmail}`); @@ -103,7 +101,7 @@ describe('Login and Logout Flow', () => { describe('Test Case 2: Quick Login and Logout using Test URL', () => { it('should login with test URL and successfully logout', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); cy.log(`[TEST START] Quick Login and Logout using Test URL - Email: ${testEmail}`); @@ -167,7 +165,7 @@ describe('Login and Logout Flow', () => { describe('Test Case 3: Cancel Logout Confirmation', () => { it('should cancel logout when clicking cancel button', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); cy.log(`[TEST START] Cancel Logout Confirmation - Email: ${testEmail}`); @@ -238,4 +236,4 @@ describe('Login and Logout Flow', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/auth/oauth-login.cy.ts b/cypress/e2e/auth/oauth-login.cy.ts index 3f5b3002..7d19c2bf 100644 --- a/cypress/e2e/auth/oauth-login.cy.ts +++ b/cypress/e2e/auth/oauth-login.cy.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; +import { TestConfig, generateRandomEmail } from '../../support/test-config'; /** * OAuth Login Flow Tests @@ -19,9 +20,7 @@ import { v4 as uuidv4 } from 'uuid'; * - Context initialization timing */ describe('OAuth Login Flow', () => { - const baseUrl = Cypress.config('baseUrl') || 'http://localhost:3000'; - const gotrueUrl = Cypress.env('APPFLOWY_GOTRUE_BASE_URL') || 'http://localhost/gotrue'; - const apiUrl = Cypress.env('APPFLOWY_BASE_URL') || 'http://localhost'; + const { baseUrl, gotrueUrl, apiUrl } = TestConfig; beforeEach(() => { // Handle uncaught exceptions @@ -46,7 +45,7 @@ describe('OAuth Login Flow', () => { describe('Google OAuth Login - New User', () => { it('should complete OAuth login for new user without redirect loop', () => { - const testEmail = `oauth-test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const mockAccessToken = 'mock-oauth-access-token-' + uuidv4(); const mockRefreshToken = 'mock-oauth-refresh-token-' + uuidv4(); const mockUserId = uuidv4(); @@ -204,7 +203,7 @@ describe('OAuth Login Flow', () => { describe('Google OAuth Login - Existing User', () => { it('should complete OAuth login for existing user without redirect loop', () => { - const testEmail = `oauth-existing-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const mockAccessToken = 'mock-oauth-access-token-existing-' + uuidv4(); const mockRefreshToken = 'mock-oauth-refresh-token-existing-' + uuidv4(); const mockUserId = uuidv4(); @@ -673,4 +672,3 @@ describe('OAuth Login Flow', () => { }); }); }); - diff --git a/cypress/e2e/auth/otp-login.cy.ts b/cypress/e2e/auth/otp-login.cy.ts index 445f9ddc..9063b790 100644 --- a/cypress/e2e/auth/otp-login.cy.ts +++ b/cypress/e2e/auth/otp-login.cy.ts @@ -1,4 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; +import { TestConfig, generateRandomEmail } from '../../support/test-config'; +import { AuthSelectors } from '../../support/selectors'; /** * OTP Login Flow Tests @@ -20,9 +22,7 @@ import { v4 as uuidv4 } from 'uuid'; * - localStorage cleanup for new users */ describe('OTP Login Flow', () => { - const baseUrl = Cypress.config('baseUrl') || 'http://localhost:3000'; - const gotrueUrl = Cypress.env('APPFLOWY_GOTRUE_BASE_URL') || 'http://localhost/gotrue'; - const apiUrl = Cypress.env('APPFLOWY_BASE_URL') || 'http://localhost'; + const { baseUrl, gotrueUrl, apiUrl } = TestConfig; beforeEach(() => { // Handle uncaught exceptions @@ -32,7 +32,7 @@ describe('OTP Login Flow', () => { describe('OTP Code Login with Redirect URL Conversion', () => { it('should successfully login with OTP code for new user and redirect to /app', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const testOtpCode = '123456'; const mockAccessToken = 'mock-access-token-' + uuidv4(); const mockRefreshToken = 'mock-refresh-token-' + uuidv4(); @@ -97,12 +97,12 @@ describe('OTP Login Flow', () => { // Enter email cy.log('[STEP 2] Entering email address'); - cy.get('[data-testid="login-email-input"]').should('be.visible').type(testEmail); + AuthSelectors.emailInput().should('be.visible').type(testEmail); cy.wait(500); // Click on "Sign in with email" button (magic link) cy.log('[STEP 3] Clicking sign in with email button (magic link)'); - cy.get('[data-testid="login-magic-link-button"]').should('be.visible').click(); + AuthSelectors.magicLinkButton().should('be.visible').click(); // Wait for magic link request cy.log('[STEP 4] Waiting for magic link request'); @@ -127,17 +127,17 @@ describe('OTP Login Flow', () => { // Click "Enter code manually" button cy.log('[STEP 7] Clicking enter code manually button'); - cy.get('[data-testid="enter-code-manually-button"]').should('be.visible').click(); + AuthSelectors.enterCodeManuallyButton().should('be.visible').click(); cy.wait(1000); // Enter OTP code cy.log('[STEP 8] Entering OTP code'); - cy.get('[data-testid="otp-code-input"]').should('be.visible').type(testOtpCode); + AuthSelectors.otpCodeInput().should('be.visible').type(testOtpCode); cy.wait(500); // Submit OTP code cy.log('[STEP 9] Submitting OTP code for verification'); - cy.get('[data-testid="otp-submit-button"]').should('be.visible').click(); + AuthSelectors.otpSubmitButton().should('be.visible').click(); // Wait for OTP verification API call cy.log('[STEP 10] Waiting for OTP verification API call'); @@ -173,7 +173,7 @@ describe('OTP Login Flow', () => { }); it('should login existing user and use afterAuth redirect logic', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const testOtpCode = '123456'; const mockAccessToken = 'mock-access-token-' + uuidv4(); const mockRefreshToken = 'mock-refresh-token-' + uuidv4(); @@ -228,24 +228,24 @@ describe('OTP Login Flow', () => { // Enter email and request magic link cy.log('[STEP 2] Entering email and requesting magic link'); - cy.get('[data-testid="login-email-input"]').type(testEmail); - cy.get('[data-testid="login-magic-link-button"]').click(); + AuthSelectors.emailInput().type(testEmail); + AuthSelectors.magicLinkButton().click(); cy.wait('@magicLinkRequest'); cy.wait(1000); // Click "Enter code manually" button cy.log('[STEP 3] Clicking enter code manually button'); - cy.get('[data-testid="enter-code-manually-button"]').click(); + AuthSelectors.enterCodeManuallyButton().click(); cy.wait(1000); // Enter OTP code cy.log('[STEP 4] Entering OTP code'); - cy.get('[data-testid="otp-code-input"]').type(testOtpCode); + AuthSelectors.otpCodeInput().type(testOtpCode); cy.wait(500); // Submit OTP code cy.log('[STEP 5] Submitting OTP code'); - cy.get('[data-testid="otp-submit-button"]').click(); + AuthSelectors.otpSubmitButton().click(); // Wait for verification cy.log('[STEP 6] Waiting for OTP verification'); @@ -263,7 +263,7 @@ describe('OTP Login Flow', () => { }); it('should handle invalid OTP code error', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const invalidOtpCode = '000000'; const redirectToUrl = '/app'; const encodedRedirectTo = encodeURIComponent(`${baseUrl}${redirectToUrl}`); @@ -292,24 +292,24 @@ describe('OTP Login Flow', () => { // Enter email and request magic link cy.log('[STEP 2] Entering email and requesting magic link'); - cy.get('[data-testid="login-email-input"]').type(testEmail); - cy.get('[data-testid="login-magic-link-button"]').click(); + AuthSelectors.emailInput().type(testEmail); + AuthSelectors.magicLinkButton().click(); cy.wait('@magicLinkRequest'); cy.wait(1000); // Click "Enter code manually" button cy.log('[STEP 3] Clicking enter code manually button'); - cy.get('[data-testid="enter-code-manually-button"]').click(); + AuthSelectors.enterCodeManuallyButton().click(); cy.wait(1000); // Enter invalid OTP code cy.log('[STEP 4] Entering invalid OTP code'); - cy.get('[data-testid="otp-code-input"]').type(invalidOtpCode); + AuthSelectors.otpCodeInput().type(invalidOtpCode); cy.wait(500); // Submit OTP code cy.log('[STEP 5] Submitting invalid OTP code'); - cy.get('[data-testid="otp-submit-button"]').click(); + AuthSelectors.otpSubmitButton().click(); // Wait for failed verification cy.log('[STEP 6] Waiting for OTP verification to fail'); @@ -327,7 +327,7 @@ describe('OTP Login Flow', () => { }); it('should navigate back to login from check email page', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const redirectToUrl = '/app'; const encodedRedirectTo = encodeURIComponent(`${baseUrl}${redirectToUrl}`); @@ -346,8 +346,8 @@ describe('OTP Login Flow', () => { // Enter email and request magic link cy.log('[STEP 2] Entering email and requesting magic link'); - cy.get('[data-testid="login-email-input"]').type(testEmail); - cy.get('[data-testid="login-magic-link-button"]').click(); + AuthSelectors.emailInput().type(testEmail); + AuthSelectors.magicLinkButton().click(); cy.wait('@magicLinkRequest'); cy.wait(1000); @@ -364,13 +364,13 @@ describe('OTP Login Flow', () => { cy.log('[STEP 5] Verifying back on login page'); cy.url().should('not.include', 'action='); cy.url().should('include', 'redirectTo='); - cy.get('[data-testid="login-email-input"]').should('be.visible'); + AuthSelectors.emailInput().should('be.visible'); cy.log('[STEP 6] Navigation test completed successfully'); }); it('should sanitize workspace-specific UUIDs from redirectTo before login', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const testOtpCode = '123456'; const mockAccessToken = 'mock-access-token-' + uuidv4(); const mockRefreshToken = 'mock-refresh-token-' + uuidv4(); @@ -449,12 +449,12 @@ describe('OTP Login Flow', () => { // Enter email (User B) cy.log('[STEP 2] User B entering email address'); - cy.get('[data-testid="login-email-input"]').should('be.visible').type(testEmail); + AuthSelectors.emailInput().should('be.visible').type(testEmail); cy.wait(500); // Click on "Sign in with email" button (magic link) cy.log('[STEP 3] User B clicking sign in with email button'); - cy.get('[data-testid="login-magic-link-button"]').should('be.visible').click(); + AuthSelectors.magicLinkButton().should('be.visible').click(); // Wait for magic link request cy.log('[STEP 4] Waiting for magic link request'); @@ -479,17 +479,17 @@ describe('OTP Login Flow', () => { // Click "Enter code manually" button cy.log('[STEP 6] Clicking enter code manually button'); - cy.get('[data-testid="enter-code-manually-button"]').should('be.visible').click(); + AuthSelectors.enterCodeManuallyButton().should('be.visible').click(); cy.wait(1000); // Enter OTP code cy.log('[STEP 7] User B entering OTP code'); - cy.get('[data-testid="otp-code-input"]').should('be.visible').type(testOtpCode); + AuthSelectors.otpCodeInput().should('be.visible').type(testOtpCode); cy.wait(500); // Submit OTP code cy.log('[STEP 8] User B submitting OTP code for verification'); - cy.get('[data-testid="otp-submit-button"]').should('be.visible').click(); + AuthSelectors.otpSubmitButton().should('be.visible').click(); // Wait for OTP verification cy.log('[STEP 9] Waiting for OTP verification'); diff --git a/cypress/e2e/auth/password-login.cy.ts b/cypress/e2e/auth/password-login.cy.ts index 0acd843b..b462a56b 100644 --- a/cypress/e2e/auth/password-login.cy.ts +++ b/cypress/e2e/auth/password-login.cy.ts @@ -1,9 +1,9 @@ import { v4 as uuidv4 } from 'uuid'; +import { TestConfig, generateRandomEmail } from '../../support/test-config'; +import { AuthSelectors } from '../../support/selectors'; describe('Password Login Flow', () => { - const baseUrl = Cypress.config('baseUrl') || 'http://localhost:3000'; - const gotrueUrl = Cypress.env('APPFLOWY_GOTRUE_BASE_URL') || 'http://localhost/gotrue'; - const apiUrl = Cypress.env('APPFLOWY_BASE_URL') || 'http://localhost'; + const { baseUrl, gotrueUrl, apiUrl } = TestConfig; beforeEach(() => { // Handle uncaught exceptions @@ -33,7 +33,7 @@ describe('Password Login Flow', () => { }); it('should allow entering email and navigating to password page', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); cy.log(`[TEST START] Testing email entry with: ${testEmail}`); @@ -65,7 +65,7 @@ describe('Password Login Flow', () => { describe('Successful Authentication', () => { it('should successfully login with email and password', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const testPassword = 'SecurePassword123!'; const mockAccessToken = 'mock-access-token-' + uuidv4(); const mockRefreshToken = 'mock-refresh-token-' + uuidv4(); @@ -105,12 +105,12 @@ describe('Password Login Flow', () => { // Enter email cy.log('[STEP 2] Entering email address'); - cy.get('[data-testid="login-email-input"]').should('be.visible').type(testEmail); + AuthSelectors.emailInput().should('be.visible').type(testEmail); cy.wait(500); // Click on "Sign in with password" button cy.log('[STEP 3] Clicking sign in with password button'); - cy.get('[data-testid="login-password-button"]').should('be.visible').click(); + AuthSelectors.passwordSignInButton().should('be.visible').click(); cy.wait(1000); // Verify we're on the password page @@ -120,12 +120,12 @@ describe('Password Login Flow', () => { // Enter password cy.log('[STEP 5] Entering password'); - cy.get('[data-testid="password-input"]').should('be.visible').type(testPassword); + AuthSelectors.passwordInput().should('be.visible').type(testPassword); cy.wait(500); // Submit password cy.log('[STEP 6] Submitting password for authentication'); - cy.get('[data-testid="password-submit-button"]').should('be.visible').click(); + AuthSelectors.passwordSubmitButton().should('be.visible').click(); // Wait for API calls cy.log('[STEP 7] Waiting for authentication API calls'); @@ -142,7 +142,7 @@ describe('Password Login Flow', () => { }); it('should handle login with mock API using flexible selectors', () => { - const testEmail = `test-${uuidv4()}@appflowy.io`; + const testEmail = generateRandomEmail(); const testPassword = 'TestPassword123!'; const mockAccessToken = 'mock-token-' + uuidv4(); @@ -236,14 +236,14 @@ describe('Password Login Flow', () => { // Enter email and go to password page cy.log('[STEP 2] Entering email and navigating to password page'); - cy.get('[data-testid="login-email-input"]').type(testEmail); - cy.get('[data-testid="login-password-button"]').click(); + AuthSelectors.emailInput().type(testEmail); + AuthSelectors.passwordSignInButton().click(); cy.wait(1000); // Enter wrong password cy.log('[STEP 3] Entering incorrect password'); - cy.get('[data-testid="password-input"]').type(wrongPassword); - cy.get('[data-testid="password-submit-button"]').click(); + AuthSelectors.passwordInput().type(wrongPassword); + AuthSelectors.passwordSubmitButton().click(); // Wait for failed API call cy.log('[STEP 4] Waiting for authentication to fail'); @@ -282,14 +282,14 @@ describe('Password Login Flow', () => { // Enter credentials cy.log('[STEP 2] Entering email and navigating to password page'); - cy.get('[data-testid="login-email-input"]').type(testEmail); - cy.get('[data-testid="login-password-button"]').click(); + AuthSelectors.emailInput().type(testEmail); + AuthSelectors.passwordSignInButton().click(); cy.wait(1000); // Enter password and submit cy.log('[STEP 3] Entering password and submitting'); - cy.get('[data-testid="password-input"]').type(testPassword); - cy.get('[data-testid="password-submit-button"]').click(); + AuthSelectors.passwordInput().type(testPassword); + AuthSelectors.passwordSubmitButton().click(); // Wait for network error cy.log('[STEP 4] Waiting for network error'); @@ -301,8 +301,8 @@ describe('Password Login Flow', () => { // Verify user can retry cy.log('[STEP 6] Verifying retry is possible'); - cy.get('[data-testid="password-input"]').should('be.visible'); - cy.get('[data-testid="password-submit-button"]').should('be.visible'); + AuthSelectors.passwordInput().should('be.visible'); + AuthSelectors.passwordSubmitButton().should('be.visible'); cy.log('[STEP 7] Network error test completed successfully'); }); @@ -321,11 +321,11 @@ describe('Password Login Flow', () => { // Enter email cy.log('[STEP 2] Entering email'); - cy.get('[data-testid="login-email-input"]').type(testEmail); + AuthSelectors.emailInput().type(testEmail); // Navigate to password page cy.log('[STEP 3] Navigating to password page'); - cy.get('[data-testid="login-password-button"]').click(); + AuthSelectors.passwordSignInButton().click(); cy.wait(1000); // Verify on password page @@ -341,9 +341,9 @@ describe('Password Login Flow', () => { // Verify back on main login page cy.log('[STEP 6] Verifying back on main login page'); cy.url().should('not.include', 'action='); - cy.get('[data-testid="login-email-input"]').should('be.visible'); + AuthSelectors.emailInput().should('be.visible'); cy.log('[STEP 7] Navigation test completed successfully'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/chat/chat-input.cy.ts b/cypress/e2e/chat/chat-input.cy.ts index 5d090eb7..3fa5a89b 100644 --- a/cypress/e2e/chat/chat-input.cy.ts +++ b/cypress/e2e/chat/chat-input.cy.ts @@ -1,19 +1,13 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, SidebarSelectors } from '../../support/selectors'; +import { AddPageSelectors, ModelSelectorSelectors, PageSelectors, SidebarSelectors, byTestId } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Chat Input Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; before(() => { - cy.task( - 'log', - `Test Environment Configuration:\n - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}` - ); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -54,35 +48,35 @@ describe('Chat Input Tests', () => { cy.wait(1000); - cy.get('[data-testid="inline-add-page"]').first().click({ force: true }); - cy.get('[data-testid="add-ai-chat-button"]').should('be.visible').click(); + AddPageSelectors.inlineAddButton().first().click({ force: true }); + AddPageSelectors.addAIChatButton().should('be.visible').click(); cy.wait(2000); // Test 1: Format toggle cy.log('Testing format toggle'); cy.get('body').then($body => { - if ($body.find('[data-testid="chat-format-group"]').length > 0) { - cy.get('[data-testid="chat-input-format-toggle"]').click(); - cy.get('[data-testid="chat-format-group"]').should('not.exist'); + if ($body.find(byTestId('chat-format-group')).length > 0) { + cy.get(byTestId('chat-input-format-toggle')).click(); + cy.get(byTestId('chat-format-group')).should('not.exist'); } }); - cy.get('[data-testid="chat-input-format-toggle"]').should('be.visible').click(); - cy.get('[data-testid="chat-format-group"]').should('exist'); - cy.get('[data-testid="chat-format-group"] button').should('have.length.at.least', 4); - cy.get('[data-testid="chat-input-format-toggle"]').click(); - cy.get('[data-testid="chat-format-group"]').should('not.exist'); + cy.get(byTestId('chat-input-format-toggle')).should('be.visible').click(); + cy.get(byTestId('chat-format-group')).should('exist'); + cy.get(byTestId('chat-format-group')).find('button').should('have.length.at.least', 4); + cy.get(byTestId('chat-input-format-toggle')).click(); + cy.get(byTestId('chat-format-group')).should('not.exist'); // Test 2: Model selector cy.log('Testing model selector'); - cy.get('[data-testid="model-selector-button"]').should('be.visible').click(); - cy.get('[data-testid^="model-option-"]').should('exist'); + ModelSelectorSelectors.button().should('be.visible').click(); + ModelSelectorSelectors.options().should('exist'); cy.get('body').click(0, 0); // Test 3: Browse prompts cy.log('Testing browse prompts'); - cy.get('[data-testid="chat-input-browse-prompts"]').click(); + cy.get(byTestId('chat-input-browse-prompts')).click(); cy.get('[role="dialog"]').should('exist'); cy.get('[role="dialog"]').contains('Browse prompts').should('be.visible'); cy.get('body').type('{esc}'); @@ -90,10 +84,10 @@ describe('Chat Input Tests', () => { // Test 4: Related views cy.log('Testing related views'); - cy.get('[data-testid="chat-input-related-views"]').click(); - cy.get('[data-testid="chat-related-views-popover"]').should('be.visible'); + cy.get(byTestId('chat-input-related-views')).click(); + cy.get(byTestId('chat-related-views-popover')).should('be.visible'); cy.get('body').type('{esc}'); - cy.get('[data-testid="chat-related-views-popover"]').should('not.exist'); + cy.get(byTestId('chat-related-views-popover')).should('not.exist'); }); }); @@ -131,8 +125,8 @@ describe('Chat Input Tests', () => { cy.wait(1000); - cy.get('[data-testid="inline-add-page"]').first().click({ force: true }); - cy.get('[data-testid="add-ai-chat-button"]').should('be.visible').click(); + AddPageSelectors.inlineAddButton().first().click({ force: true }); + AddPageSelectors.addAIChatButton().should('be.visible').click(); cy.wait(3000); // Wait for chat to fully load @@ -210,8 +204,8 @@ describe('Chat Input Tests', () => { cy.wait(500); // Check send button is disabled when empty - cy.get('[data-testid="chat-input-send"]').should('exist'); - cy.get('[data-testid="chat-input-send"]').then($button => { + cy.get(byTestId('chat-input-send')).should('exist'); + cy.get(byTestId('chat-input-send')).then($button => { // Button might be disabled via attribute or opacity const isDisabled = $button.prop('disabled') || $button.css('opacity') === '0.5'; expect(isDisabled).to.be.true; @@ -221,7 +215,7 @@ describe('Chat Input Tests', () => { getTextarea().type('Test message'); cy.wait(500); - cy.get('[data-testid="chat-input-send"]').then($button => { + cy.get(byTestId('chat-input-send')).then($button => { const isDisabled = $button.prop('disabled') || $button.css('opacity') === '0.5'; expect(isDisabled).to.be.false; }); @@ -231,7 +225,7 @@ describe('Chat Input Tests', () => { getTextarea().clear().type('Hello world'); cy.wait(500); - cy.get('[data-testid="chat-input-send"]').click(); + cy.get(byTestId('chat-input-send')).click(); cy.wait('@submitQuestion', { timeout: 10000 }); // Wait for textarea to be ready again @@ -272,4 +266,4 @@ describe('Chat Input Tests', () => { .and('have.value', ''); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/chat/create-ai-chat.cy.ts b/cypress/e2e/chat/create-ai-chat.cy.ts index 9db0e433..f19df301 100644 --- a/cypress/e2e/chat/create-ai-chat.cy.ts +++ b/cypress/e2e/chat/create-ai-chat.cy.ts @@ -1,20 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, ModalSelectors, SidebarSelectors, waitForReactUpdate } from '../../support/selectors'; +import { AddPageSelectors, PageSelectors, ModalSelectors, SidebarSelectors, byTestId, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('AI Chat Creation and Navigation Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; let chatName: string; before(() => { // Log environment configuration for debugging - cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -96,7 +91,7 @@ describe('AI Chat Creation and Navigation Tests', () => { // Click the inline add button (plus icon) - use first() since there might be multiple cy.wrap($page).within(() => { - cy.get('[data-testid="inline-add-page"]') + AddPageSelectors.inlineAddButton() .first() .should('be.visible') .click({ force: true }); @@ -112,7 +107,7 @@ describe('AI Chat Creation and Navigation Tests', () => { cy.task('log', '=== Step 3: Creating AI Chat ==='); // Click on the AI Chat option in the dropdown - cy.get('[data-testid="add-ai-chat-button"]') + AddPageSelectors.addAIChatButton() .should('be.visible') .click(); @@ -130,7 +125,7 @@ describe('AI Chat Creation and Navigation Tests', () => { // Check if the AI Chat container exists (but don't fail if it doesn't load immediately) cy.get('body').then($body => { - if ($body.find('[data-testid="ai-chat-container"]').length > 0) { + if ($body.find(byTestId('ai-chat-container')).length > 0) { cy.task('log', '✓ AI Chat container exists'); } else { cy.task('log', 'AI Chat container not immediately visible, checking for navigation success...'); @@ -145,7 +140,7 @@ describe('AI Chat Creation and Navigation Tests', () => { cy.get('body').then($body => { // Check if chat interface elements exist const hasChatElements = $body.find('.ai-chat').length > 0 || - $body.find('[data-testid="ai-chat-container"]').length > 0; + $body.find(byTestId('ai-chat-container')).length > 0; if (hasChatElements) { cy.task('log', '✓ AI Chat interface loaded'); @@ -192,4 +187,4 @@ describe('AI Chat Creation and Navigation Tests', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/chat/model-selection-persistence.cy.ts b/cypress/e2e/chat/model-selection-persistence.cy.ts index afaf9e6b..8fce812b 100644 --- a/cypress/e2e/chat/model-selection-persistence.cy.ts +++ b/cypress/e2e/chat/model-selection-persistence.cy.ts @@ -1,19 +1,14 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, SidebarSelectors, ModelSelectorSelectors } from '../../support/selectors'; +import { AddPageSelectors, PageSelectors, SidebarSelectors, ModelSelectorSelectors } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Chat Model Selection Persistence Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; before(() => { // Log environment configuration for debugging - cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -81,7 +76,7 @@ describe('Chat Model Selection Persistence Tests', () => { // Click the inline add button (plus icon) cy.wrap($page).within(() => { - cy.get('[data-testid="inline-add-page"]') + AddPageSelectors.inlineAddButton() .first() .should('be.visible') .click({ force: true }); @@ -92,7 +87,7 @@ describe('Chat Model Selection Persistence Tests', () => { cy.wait(1000); // Click on the AI Chat option from the dropdown - cy.get('[data-testid="add-ai-chat-button"]') + AddPageSelectors.addAIChatButton() .should('be.visible') .click(); @@ -202,4 +197,4 @@ describe('Chat Model Selection Persistence Tests', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/chat/selection-mode.cy.ts b/cypress/e2e/chat/selection-mode.cy.ts index 09a6241f..670e368b 100644 --- a/cypress/e2e/chat/selection-mode.cy.ts +++ b/cypress/e2e/chat/selection-mode.cy.ts @@ -1,7 +1,7 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, SidebarSelectors } from '../../support/selectors'; +import { AddPageSelectors, PageSelectors, SidebarSelectors } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; const STUBBED_MESSAGE_ID = 101; const STUBBED_MESSAGE_CONTENT = 'Stubbed AI answer ready for export'; @@ -88,13 +88,10 @@ function setupChatApiStubs() { } describe('Chat Selection Mode Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; before(() => { - cy.task('log', `Test Environment Configuration:\n - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -140,9 +137,9 @@ describe('Chat Selection Mode Tests', () => { cy.wait(1000); - cy.get('[data-testid="inline-add-page"]').first().click({ force: true }); + AddPageSelectors.inlineAddButton().first().click({ force: true }); - cy.get('[data-testid="add-ai-chat-button"]').should('be.visible').click(); + AddPageSelectors.addAIChatButton().should('be.visible').click(); cy.wait('@getChatSettings'); cy.wait('@getModelList'); @@ -150,7 +147,7 @@ describe('Chat Selection Mode Tests', () => { cy.contains(STUBBED_MESSAGE_CONTENT).should('be.visible'); - cy.get('[data-testid="page-more-actions"]').first().click({ force: true }); + PageSelectors.moreActionsButton().first().click({ force: true }); cy.get('[role="menu"]').should('exist'); diff --git a/cypress/e2e/database/checkbox-column.cy.ts b/cypress/e2e/database/checkbox-column.cy.ts index 395547a3..41a8bbc6 100644 --- a/cypress/e2e/database/checkbox-column.cy.ts +++ b/cypress/e2e/database/checkbox-column.cy.ts @@ -1,16 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, CheckboxSelectors, byTestId, + byTestIdPrefix, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('Checkbox Column Type', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -60,7 +59,7 @@ describe('Checkbox Column Type', () => { cy.log('[STEP 9] Looking for checkbox elements'); cy.get('body').then($body => { // Check for checkbox cells with our data-testid - const checkboxCells = $body.find('[data-testid^="checkbox-cell-"]'); + const checkboxCells = $body.find(byTestIdPrefix('checkbox-cell-')); if (checkboxCells.length > 0) { cy.log(`[STEP 10] Found ${checkboxCells.length} checkbox cells`); @@ -76,4 +75,4 @@ describe('Checkbox Column Type', () => { cy.log('[STEP 12] Test completed successfully'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/database/datetime-column.cy.ts b/cypress/e2e/database/datetime-column.cy.ts index 6ac6aa6a..cae2967b 100644 --- a/cypress/e2e/database/datetime-column.cy.ts +++ b/cypress/e2e/database/datetime-column.cy.ts @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, @@ -8,12 +6,13 @@ import { GridFieldSelectors, FieldType, byTestId, + byTestIdPrefix, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('DateTime Column Type', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -103,7 +102,7 @@ describe('DateTime Column Type', () => { // Verify datetime cells exist cy.log('[STEP 12] Checking for datetime cells'); cy.get('body').then($body => { - const datetimeCells = $body.find('[data-testid^="datetime-cell-"]'); + const datetimeCells = $body.find(byTestIdPrefix('datetime-cell-')); if (datetimeCells.length > 0) { cy.log(`[STEP 13] Found ${datetimeCells.length} datetime cells`); @@ -138,4 +137,4 @@ describe('DateTime Column Type', () => { cy.log('[STEP 17] DateTime column test completed'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/database/grid-edit-operations.cy.ts b/cypress/e2e/database/grid-edit-operations.cy.ts index a5f41c2a..2875c23f 100644 --- a/cypress/e2e/database/grid-edit-operations.cy.ts +++ b/cypress/e2e/database/grid-edit-operations.cy.ts @@ -1,14 +1,12 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('Database Grid Edit Operations', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -106,4 +104,4 @@ describe('Database Grid Edit Operations', () => { }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/database/row-deletion.cy.ts b/cypress/e2e/database/row-deletion.cy.ts index 0cc099d0..7d0633d2 100644 --- a/cypress/e2e/database/row-deletion.cy.ts +++ b/cypress/e2e/database/row-deletion.cy.ts @@ -1,15 +1,13 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, RowControlsSelectors, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('Database Row Deletion', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -168,4 +166,4 @@ describe('Database Row Deletion', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/database/row-duplication.cy.ts b/cypress/e2e/database/row-duplication.cy.ts index 56129e5d..a2a75cf3 100644 --- a/cypress/e2e/database/row-duplication.cy.ts +++ b/cypress/e2e/database/row-duplication.cy.ts @@ -1,15 +1,13 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, RowControlsSelectors, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('Database Row Duplication', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -161,4 +159,4 @@ describe('Database Row Duplication', () => { cy.log('[STEP 15] Row duplication test completed successfully'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/database/row-insertion.cy.ts b/cypress/e2e/database/row-insertion.cy.ts index a682acc7..c3613870 100644 --- a/cypress/e2e/database/row-insertion.cy.ts +++ b/cypress/e2e/database/row-insertion.cy.ts @@ -1,15 +1,13 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, RowControlsSelectors, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('Database Row Insertion', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -235,4 +233,4 @@ describe('Database Row Insertion', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/database/single-select-column.cy.ts b/cypress/e2e/database/single-select-column.cy.ts index 6324cfb7..abde8a51 100644 --- a/cypress/e2e/database/single-select-column.cy.ts +++ b/cypress/e2e/database/single-select-column.cy.ts @@ -1,5 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; -import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, DatabaseGridSelectors, @@ -9,11 +7,13 @@ import { PageSelectors, FieldType, byTestId, + byTestIdPrefix, waitForReactUpdate } from '../../support/selectors'; +import { AuthTestUtils } from '../../support/auth-utils'; +import { generateRandomEmail } from '../../support/test-config'; describe('Single Select Column Type', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; const SINGLE_SELECT_FIELD_TYPE = 3; // From FieldType enum beforeEach(() => { @@ -192,7 +192,7 @@ describe('Single Select Column Type', () => { // First try to find select cells cy.get('body').then($body => { - const selectCells = $body.find('[data-testid^="select-option-cell-"]'); + const selectCells = $body.find(byTestIdPrefix('select-option-cell-')); if (selectCells.length > 0) { cy.log(`[STEP 9] Found ${selectCells.length} select cells`); @@ -306,7 +306,7 @@ describe('Single Select Column Type', () => { // Verify select options are displayed again cy.log('[STEP 16] Verifying select options are displayed again'); cy.get('body').then($body => { - const selectCells = $body.find('[data-testid^="select-option-cell-"]'); + const selectCells = $body.find(byTestIdPrefix('select-option-cell-')); if (selectCells.length > 0) { cy.log(`[STEP 17] Success! Found ${selectCells.length} select option cells after conversion`); diff --git a/cypress/e2e/editor/document-editing.cy.ts b/cypress/e2e/editor/document-editing.cy.ts index 7d683472..dfda8df4 100644 --- a/cypress/e2e/editor/document-editing.cy.ts +++ b/cypress/e2e/editor/document-editing.cy.ts @@ -1,10 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { EditorSelectors } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Document Editing with Formatting', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', () => false); cy.viewport(1280, 720); @@ -241,4 +239,4 @@ describe('Document Editing with Formatting', () => { EditorSelectors.slateEditor().should('contain.text', 'Review code'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/editor/slash-menu-formatting.cy.ts b/cypress/e2e/editor/slash-menu-formatting.cy.ts index 66b3933d..5b23ad9f 100644 --- a/cypress/e2e/editor/slash-menu-formatting.cy.ts +++ b/cypress/e2e/editor/slash-menu-formatting.cy.ts @@ -1,10 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Slash Menu - Text Formatting', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -107,4 +105,4 @@ describe('Slash Menu - Text Formatting', () => { cy.log('Heading 1 added successfully'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/editor/slash-menu-lists.cy.ts b/cypress/e2e/editor/slash-menu-lists.cy.ts index c556f12e..c6249f08 100644 --- a/cypress/e2e/editor/slash-menu-lists.cy.ts +++ b/cypress/e2e/editor/slash-menu-lists.cy.ts @@ -1,10 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Slash Menu - List Actions', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -102,4 +100,4 @@ describe('Slash Menu - List Actions', () => { }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/editor/slash-menu-media.cy.ts b/cypress/e2e/editor/slash-menu-media.cy.ts index 6b3d82db..752e844c 100644 --- a/cypress/e2e/editor/slash-menu-media.cy.ts +++ b/cypress/e2e/editor/slash-menu-media.cy.ts @@ -1,10 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Slash Menu - Media Actions', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -94,4 +92,4 @@ describe('Slash Menu - Media Actions', () => { cy.log('Image option clicked successfully'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/editor/slash-menu.cy.ts b/cypress/e2e/editor/slash-menu.cy.ts index a7d73b69..1c101ad2 100644 --- a/cypress/e2e/editor/slash-menu.cy.ts +++ b/cypress/e2e/editor/slash-menu.cy.ts @@ -1,10 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { EditorSelectors, PageSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Editor Slash Menu', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', (err) => { if (err.message.includes('Minified React error') || @@ -59,4 +57,4 @@ describe('Editor Slash Menu', () => { }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/editor/text-formatting.cy.ts b/cypress/e2e/editor/text-formatting.cy.ts index 3db2ee1d..b393f8e0 100644 --- a/cypress/e2e/editor/text-formatting.cy.ts +++ b/cypress/e2e/editor/text-formatting.cy.ts @@ -1,10 +1,8 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { EditorSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Text Formatting - Selection and Formatting', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - beforeEach(() => { cy.on('uncaught:exception', () => false); cy.viewport(1280, 720); @@ -152,4 +150,4 @@ describe('Text Formatting - Selection and Formatting', () => { cy.log('All text formatting styles tested successfully - each line shows a different style'); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/page/breadcrumb-navigation.cy.ts b/cypress/e2e/page/breadcrumb-navigation.cy.ts index f37004ab..267eeb3a 100644 --- a/cypress/e2e/page/breadcrumb-navigation.cy.ts +++ b/cypress/e2e/page/breadcrumb-navigation.cy.ts @@ -1,19 +1,21 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, SpaceSelectors, SidebarSelectors, waitForReactUpdate } from '../../support/selectors'; +import { + PageSelectors, + SpaceSelectors, + SidebarSelectors, + byTestId, + byTestIdContains, + waitForReactUpdate +} from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Breadcrumb Navigation Complete Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; before(() => { // Log environment configuration for debugging - cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -71,11 +73,11 @@ describe('Breadcrumb Navigation Complete Tests', () => { // Step 4: Check for breadcrumb navigation cy.task('log', '=== Step 4: Checking for breadcrumb navigation ==='); cy.get('body').then($body => { - if ($body.find('[data-testid="breadcrumb-navigation"]').length > 0) { + if ($body.find(byTestId('breadcrumb-navigation')).length > 0) { cy.task('log', '✓ Breadcrumb navigation found on this page'); // Count breadcrumb items - cy.get('[data-testid*="breadcrumb-item-"]').then($items => { + cy.get(byTestIdContains('breadcrumb-item-')).then($items => { cy.task('log', `✓ Found ${$items.length} breadcrumb items`); }); } else { @@ -133,12 +135,12 @@ describe('Breadcrumb Navigation Complete Tests', () => { // Check for breadcrumb navigation cy.task('log', '=== Step 4: Testing breadcrumb navigation ==='); cy.get('body', { timeout: 5000 }).then($body => { - if ($body.find('[data-testid="breadcrumb-navigation"]').length > 0) { + if ($body.find(byTestId('breadcrumb-navigation')).length > 0) { cy.task('log', '✓ Breadcrumb navigation is visible'); // Try to click breadcrumb to navigate back - if ($body.find('[data-testid*="breadcrumb-item-"]').length > 1) { - cy.get('[data-testid*="breadcrumb-item-"]').first().click({ force: true }); + if ($body.find(byTestIdContains('breadcrumb-item-')).length > 1) { + cy.get(byTestIdContains('breadcrumb-item-')).first().click({ force: true }); cy.task('log', '✓ Clicked breadcrumb item to navigate back'); cy.wait(2000); cy.task('log', '✓ Successfully used breadcrumb navigation'); @@ -251,11 +253,11 @@ describe('Breadcrumb Navigation Complete Tests', () => { // Step 4: Test breadcrumb navigation cy.task('log', '=== Step 4: Testing breadcrumb navigation ==='); cy.get('body').then($body => { - if ($body.find('[data-testid="breadcrumb-navigation"]').length > 0) { + if ($body.find(byTestId('breadcrumb-navigation')).length > 0) { cy.task('log', '✓ Breadcrumb navigation is visible'); // Check breadcrumb items with timeout - cy.get('[data-testid*="breadcrumb-item-"]', { timeout: 10000 }).then($items => { + cy.get(byTestIdContains('breadcrumb-item-'), { timeout: 10000 }).then($items => { cy.task('log', `Found ${$items.length} breadcrumb items`); if ($items.length > 1) { @@ -293,4 +295,4 @@ describe('Breadcrumb Navigation Complete Tests', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/page/create-delete-page.cy.ts b/cypress/e2e/page/create-delete-page.cy.ts index 63ac199d..b289d2de 100644 --- a/cypress/e2e/page/create-delete-page.cy.ts +++ b/cypress/e2e/page/create-delete-page.cy.ts @@ -1,20 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; import { PageSelectors, ModalSelectors, SidebarSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Page Create and Delete Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; let testPageName: string; before(() => { // Log environment configuration for debugging - cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -193,4 +188,4 @@ describe('Page Create and Delete Tests', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/page/delete-page-verify-trash.cy.ts b/cypress/e2e/page/delete-page-verify-trash.cy.ts index 2a71bdde..d803bc09 100644 --- a/cypress/e2e/page/delete-page-verify-trash.cy.ts +++ b/cypress/e2e/page/delete-page-verify-trash.cy.ts @@ -1,20 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, ModalSelectors, SidebarSelectors, waitForReactUpdate } from '../../support/selectors'; +import { PageSelectors, ModalSelectors, SidebarSelectors, byTestId, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Delete Page, Verify in Trash, and Restore Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; let testPageName: string; before(() => { // Log environment configuration for debugging - cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -168,7 +163,7 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { cy.task('log', '=== Step 5: Navigating to trash page ==='); // Click on the trash button in the sidebar - cy.get('[data-testid="sidebar-trash-button"]').click(); + cy.get(byTestId('sidebar-trash-button')).click(); // Wait for navigation cy.wait(2000); @@ -181,10 +176,10 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { cy.task('log', '=== Step 6: Verifying deleted page exists in trash ==='); // Wait for trash table to load - cy.get('[data-testid="trash-table"]', { timeout: 10000 }).should('be.visible'); + cy.get(byTestId('trash-table'), { timeout: 10000 }).should('be.visible'); // Look for our deleted page in the trash table - cy.get('[data-testid="trash-table-row"]').then($rows => { + cy.get(byTestId('trash-table-row')).then($rows => { let foundPage = false; // Check each row for our page name @@ -210,13 +205,13 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { // Step 7: Verify restore and permanent delete buttons are present cy.task('log', '=== Step 7: Verifying trash actions are available ==='); - cy.get('[data-testid="trash-table-row"]').first().within(() => { + cy.get(byTestId('trash-table-row')).first().within(() => { // Check for restore button - cy.get('[data-testid="trash-restore-button"]').should('exist'); + cy.get(byTestId('trash-restore-button')).should('exist'); cy.task('log', '✓ Restore button found'); // Check for permanent delete button - cy.get('[data-testid="trash-delete-button"]').should('exist'); + cy.get(byTestId('trash-delete-button')).should('exist'); cy.task('log', '✓ Permanent delete button found'); }); @@ -227,15 +222,15 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { let restoredPageName = 'Untitled'; // Default to Untitled since that's what usually gets created // Click the restore button on the first row (our deleted page) - cy.get('[data-testid="trash-table-row"]').first().within(() => { + cy.get(byTestId('trash-table-row')).first().within(() => { // Get the page name before restoring cy.get('td').first().invoke('text').then((text) => { restoredPageName = text.trim() || 'Untitled'; cy.task('log', `Restoring page: ${restoredPageName}`); }); - + // Click restore button - cy.get('[data-testid="trash-restore-button"]').click(); + cy.get(byTestId('trash-restore-button')).click(); }); // Wait for restore to complete @@ -248,13 +243,13 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { // Check if trash is now empty or doesn't contain our page cy.get('body').then(($body) => { // Check if there are any rows left in trash - const rowsExist = $body.find('[data-testid="trash-table-row"]').length > 0; + const rowsExist = $body.find(byTestId('trash-table-row')).length > 0; if (!rowsExist) { cy.task('log', '✓ Trash is now empty - page successfully removed from trash'); } else { // If there are still rows, verify our page is not among them - cy.get('[data-testid="trash-table-row"]').then($rows => { + cy.get(byTestId('trash-table-row')).then($rows => { let pageStillInTrash = false; $rows.each((index, row) => { @@ -312,4 +307,4 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/page/edit-page.cy.ts b/cypress/e2e/page/edit-page.cy.ts index 55b96640..1a6617f5 100644 --- a/cypress/e2e/page/edit-page.cy.ts +++ b/cypress/e2e/page/edit-page.cy.ts @@ -1,12 +1,9 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; import { PageSelectors, ModalSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('Page Edit Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; let testPageName: string; let testContent: string[]; @@ -16,7 +13,6 @@ describe('Page Edit Tests', () => { testPageName = 'e2e test-edit page'; // Generate random content for testing - const randomId = uuidv4().slice(0, 8); testContent = [ `AppFlowy Web`, `AppFlowy Web is a modern open-source project management tool that helps you manage your projects and tasks efficiently.`, @@ -120,4 +116,4 @@ describe('Page Edit Tests', () => { cy.task('log', '=== Test completed successfully ==='); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/page/more-page-action.cy.ts b/cypress/e2e/page/more-page-action.cy.ts index da75218c..eb3943f8 100644 --- a/cypress/e2e/page/more-page-action.cy.ts +++ b/cypress/e2e/page/more-page-action.cy.ts @@ -1,12 +1,9 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, waitForReactUpdate } from '../../support/selectors'; +import { PageSelectors, byTestId, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; describe('More Page Actions', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; const newPageName = 'Renamed Test Page'; let testEmail: string; @@ -191,7 +188,7 @@ describe('More Page Actions', () => { cy.task('log', 'Clicked Rename option'); // Wait for the rename modal to appear - cy.get('[data-testid="rename-modal-input"]', { timeout: 5000 }) + cy.get(byTestId('rename-modal-input'), { timeout: 5000 }) .should('be.visible') .clear() .type(renamedPageName); @@ -199,7 +196,7 @@ describe('More Page Actions', () => { cy.task('log', `Entered new page name: ${renamedPageName}`); // Click the save button - cy.get('[data-testid="rename-modal-save"]').click(); + cy.get(byTestId('rename-modal-save')).click(); cy.task('log', 'Clicked save button'); @@ -238,4 +235,4 @@ describe('More Page Actions', () => { cy.task('log', 'Rename test completed successfully - name persisted after refresh'); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/page/publish-page.cy.ts b/cypress/e2e/page/publish-page.cy.ts index 52c7a32c..4df54356 100644 --- a/cypress/e2e/page/publish-page.cy.ts +++ b/cypress/e2e/page/publish-page.cy.ts @@ -1,19 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, ShareSelectors, SidebarSelectors } from '../../support/selectors'; +import { PageSelectors, ShareSelectors, SidebarSelectors, byTestId } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Publish Page Test', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - let testEmail: string; const pageName = 'publish page'; const pageContent = 'This is a publish page content'; before(() => { - cy.task('log', `Env:\n- APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n- APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -74,7 +70,7 @@ describe('Publish Page Test', () => { cy.wait(5000); // Verify that the page is now published by checking for published UI elements - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); cy.task('log', 'Page published successfully, URL elements visible'); // 6. Get the published URL by constructing it from UI elements @@ -82,8 +78,8 @@ describe('Publish Page Test', () => { const origin = win.location.origin; // Get namespace and publish name from the UI - cy.get('[data-testid="publish-namespace"]').should('be.visible').invoke('text').then((namespace) => { - cy.get('[data-testid="publish-name-input"]').should('be.visible').invoke('val').then((publishName) => { + cy.get(byTestId('publish-namespace')).should('be.visible').invoke('text').then((namespace) => { + cy.get(byTestId('publish-name-input')).should('be.visible').invoke('val').then((publishName) => { const namespaceText = namespace.trim(); const publishNameText = String(publishName).trim(); const publishedUrl = `${origin}/${namespaceText}/${publishNameText}`; @@ -92,9 +88,9 @@ describe('Publish Page Test', () => { // 7. Find and click the copy link button // The copy button is an IconButton with LinkIcon SVG, inside a Tooltip // Located in a div with class "p-1 text-text-primary" next to the URL container - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { // Find the parent container that holds both URL inputs and copy button - cy.get('[data-testid="publish-name-input"]') + cy.get(byTestId('publish-name-input')) .closest('div.flex.w-full.items-center.overflow-hidden') .find('div.p-1.text-text-primary') .should('be.visible') @@ -266,13 +262,13 @@ describe('Publish Page Test', () => { cy.wait(5000); // Verify published - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); // Get the published URL cy.window().then((win) => { const origin = win.location.origin; - cy.get('[data-testid="publish-namespace"]').should('be.visible').invoke('text').then((namespace) => { - cy.get('[data-testid="publish-name-input"]').should('be.visible').invoke('val').then((publishName) => { + cy.get(byTestId('publish-namespace')).should('be.visible').invoke('text').then((namespace) => { + cy.get(byTestId('publish-name-input')).should('be.visible').invoke('val').then((publishName) => { const publishedUrl = `${origin}/${namespace.trim()}/${String(publishName).trim()}`; cy.task('log', `Published URL: ${publishedUrl}`); @@ -318,20 +314,20 @@ describe('Publish Page Test', () => { ShareSelectors.publishConfirmButton().should('be.visible').click({ force: true }); cy.wait(5000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); // Get original URL cy.window().then((win) => { const origin = win.location.origin; - cy.get('[data-testid="publish-namespace"]').invoke('text').then((namespace) => { - cy.get('[data-testid="publish-name-input"]').invoke('val').then((originalName) => { + cy.get(byTestId('publish-namespace')).invoke('text').then((namespace) => { + cy.get(byTestId('publish-name-input')).invoke('val').then((originalName) => { const namespaceText = namespace.trim(); const originalNameText = String(originalName).trim(); cy.task('log', `Original publish name: ${originalNameText}`); // Edit the publish name directly in the input const newPublishName = `custom-name-${Date.now()}`; - cy.get('[data-testid="publish-name-input"]') + cy.get(byTestId('publish-name-input')) .clear() .type(newPublishName) .blur(); @@ -400,14 +396,14 @@ describe('Publish Page Test', () => { ShareSelectors.publishConfirmButton().should('be.visible').click({ force: true }); cy.wait(5000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); cy.task('log', '✓ First publish successful'); // Get published URL cy.window().then((win) => { const origin = win.location.origin; - cy.get('[data-testid="publish-namespace"]').invoke('text').then((namespace) => { - cy.get('[data-testid="publish-name-input"]').invoke('val').then((publishName) => { + cy.get(byTestId('publish-namespace')).invoke('text').then((namespace) => { + cy.get(byTestId('publish-name-input')).invoke('val').then((publishName) => { const publishedUrl = `${origin}/${namespace.trim()}/${String(publishName).trim()}`; cy.task('log', `Published URL: ${publishedUrl}`); @@ -461,7 +457,7 @@ describe('Publish Page Test', () => { // Republish with updated content ShareSelectors.publishConfirmButton().should('be.visible').click({ force: true }); cy.wait(5000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); cy.task('log', '✓ Republished successfully'); // Verify updated content is published @@ -504,14 +500,14 @@ describe('Publish Page Test', () => { ShareSelectors.publishConfirmButton().should('be.visible').click({ force: true }); cy.wait(5000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); // Try to set invalid publish name with spaces - cy.get('[data-testid="publish-name-input"]').invoke('val').then((originalName) => { + cy.get(byTestId('publish-name-input')).invoke('val').then((originalName) => { cy.task('log', `Original name: ${originalName}`); // Try to set name with space (should be rejected) - cy.get('[data-testid="publish-name-input"]') + cy.get(byTestId('publish-name-input')) .clear() .type('invalid name with spaces') .blur(); @@ -522,7 +518,7 @@ describe('Publish Page Test', () => { cy.get('body').then(($body) => { const bodyText = $body.text(); // The name should either revert or show an error - cy.get('[data-testid="publish-name-input"]').invoke('val').then((currentName) => { + cy.get(byTestId('publish-name-input')).invoke('val').then((currentName) => { // Name should not contain spaces (validation should prevent it) if (String(currentName).includes(' ')) { cy.task('log', '⚠ Warning: Invalid characters were not rejected'); @@ -561,10 +557,10 @@ describe('Publish Page Test', () => { ShareSelectors.publishConfirmButton().should('be.visible').click({ force: true }); cy.wait(5000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); // Test comments switch - find by looking for Switch components in the published panel - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { // Find switches by looking for Switch components (they use MUI Switch which renders as input[type="checkbox"]) // Look for the container divs that have the text labels cy.get('div.flex.items-center.justify-between').contains(/comments|comment/i).parent().within(() => { @@ -635,13 +631,13 @@ describe('Publish Page Test', () => { ShareSelectors.publishConfirmButton().should('be.visible').click({ force: true }); cy.wait(5000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); // Get first URL cy.window().then((win) => { const origin = win.location.origin; - cy.get('[data-testid="publish-namespace"]').invoke('text').then((namespace) => { - cy.get('[data-testid="publish-name-input"]').invoke('val').then((publishName) => { + cy.get(byTestId('publish-namespace')).invoke('text').then((namespace) => { + cy.get(byTestId('publish-name-input')).invoke('val').then((publishName) => { firstPublishedUrl = `${origin}/${namespace.trim()}/${String(publishName).trim()}`; cy.task('log', `First published URL: ${firstPublishedUrl}`); @@ -654,9 +650,9 @@ describe('Publish Page Test', () => { cy.contains('Publish').should('exist').click({ force: true }); cy.wait(1000); - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); - cy.get('[data-testid="publish-namespace"]').invoke('text').then((namespace2) => { - cy.get('[data-testid="publish-name-input"]').invoke('val').then((publishName2) => { + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).invoke('text').then((namespace2) => { + cy.get(byTestId('publish-name-input')).invoke('val').then((publishName2) => { const secondPublishedUrl = `${origin}/${namespace2.trim()}/${String(publishName2).trim()}`; cy.task('log', `Second check URL: ${secondPublishedUrl}`); @@ -753,7 +749,7 @@ describe('Publish Page Test', () => { cy.wait(5000); // Verify that the database is now published by checking for published UI elements - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + cy.get(byTestId('publish-namespace')).should('be.visible', { timeout: 10000 }); cy.task('log', 'Database published successfully, URL elements visible'); // Get the published URL @@ -761,8 +757,8 @@ describe('Publish Page Test', () => { const origin = win.location.origin; // Get namespace and publish name from the UI - cy.get('[data-testid="publish-namespace"]').should('be.visible').invoke('text').then((namespace) => { - cy.get('[data-testid="publish-name-input"]').should('be.visible').invoke('val').then((publishName) => { + cy.get(byTestId('publish-namespace')).should('be.visible').invoke('text').then((namespace) => { + cy.get(byTestId('publish-name-input')).should('be.visible').invoke('val').then((publishName) => { const namespaceText = namespace.trim(); const publishNameText = String(publishName).trim(); const publishedUrl = `${origin}/${namespaceText}/${publishNameText}`; @@ -803,5 +799,3 @@ describe('Publish Page Test', () => { }); }); }); - - diff --git a/cypress/e2e/page/share-page.cy.ts b/cypress/e2e/page/share-page.cy.ts index 1f3aaa23..253654d9 100644 --- a/cypress/e2e/page/share-page.cy.ts +++ b/cypress/e2e/page/share-page.cy.ts @@ -1,18 +1,14 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, SidebarSelectors, waitForReactUpdate } from '../../support/selectors'; +import { PageSelectors, SidebarSelectors, ShareSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Share Page Test', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; - let userAEmail: string; let userBEmail: string; before(() => { - cy.task('log', `Env:\n- APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n- APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -53,7 +49,7 @@ describe('Share Page Test', () => { cy.task('log', 'Share and Publish tabs verified'); // 3. Make sure we're on the Share tab (click it if needed) - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { @@ -67,7 +63,7 @@ describe('Share Page Test', () => { // 4. Find the email input field and type user B's email cy.task('log', `Inviting user B: ${userBEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { // Find the input field inside the email-tag-input container cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') @@ -98,7 +94,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B appears in the "People with access" section - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains('People with access', { timeout: 10000 }).should('be.visible'); cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.task('log', 'User B successfully added to the page'); @@ -106,7 +102,7 @@ describe('Share Page Test', () => { // 6. Find user B's access level dropdown and click it cy.task('log', 'Finding user B\'s access dropdown...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { // Find the person item containing user B's email // The PersonItem component renders the email in a div with text-xs class cy.contains(userBEmail) @@ -148,7 +144,7 @@ describe('Share Page Test', () => { // 8. Verify user B is removed from the list cy.task('log', 'Verifying user B is removed...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { // User B should no longer appear in the people list cy.contains(userBEmail).should('not.exist'); cy.task('log', '✓ User B successfully removed from access list'); @@ -191,7 +187,7 @@ describe('Share Page Test', () => { // Invite user B first TestTool.openSharePopover(); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -199,7 +195,7 @@ describe('Share Page Test', () => { } }); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') .should('be.visible') @@ -219,7 +215,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B is added with default "Can view" access - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.contains(userBEmail) .closest('div.group') @@ -232,7 +228,7 @@ describe('Share Page Test', () => { // Change access level to "Can edit" cy.task('log', 'Changing user B access level to "Can edit"...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .closest('div.group') .within(() => { @@ -259,7 +255,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify access level changed - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .closest('div.group') .within(() => { @@ -297,7 +293,7 @@ describe('Share Page Test', () => { cy.wait(2000); TestTool.openSharePopover(); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -307,7 +303,7 @@ describe('Share Page Test', () => { // Invite multiple users cy.task('log', `Inviting multiple users: ${userBEmail}, ${userCEmail}, ${userDEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { const emails = [userBEmail, userCEmail, userDEmail]; emails.forEach((email, index) => { @@ -333,7 +329,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify all users appear in the list - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains('People with access', { timeout: 10000 }).should('be.visible'); cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.contains(userCEmail, { timeout: 10000 }).should('be.visible'); @@ -366,7 +362,7 @@ describe('Share Page Test', () => { cy.wait(2000); TestTool.openSharePopover(); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -376,7 +372,7 @@ describe('Share Page Test', () => { // Set access level to "Can edit" before inviting cy.task('log', `Inviting user B with "Can edit" access level`); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { // First, find and click the access level selector (if it exists) // The access level selector might be a button or dropdown near the invite input // Look for access level selector button within the popover @@ -415,7 +411,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B is added - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.task('log', 'User B successfully invited'); @@ -448,7 +444,7 @@ describe('Share Page Test', () => { cy.wait(2000); TestTool.openSharePopover(); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -457,7 +453,7 @@ describe('Share Page Test', () => { }); // Invite user B - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') .should('be.visible') @@ -477,7 +473,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Check for pending status - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); // Look for "Pending" badge or text near user B's email @@ -524,7 +520,7 @@ describe('Share Page Test', () => { cy.wait(2000); TestTool.openSharePopover(); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -534,7 +530,7 @@ describe('Share Page Test', () => { // Invite two users cy.task('log', `Inviting users: ${userBEmail}, ${userCEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { [userBEmail, userCEmail].forEach((email) => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') @@ -557,7 +553,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify both users are added - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.contains(userCEmail, { timeout: 10000 }).should('be.visible'); cy.task('log', 'Both users added successfully'); @@ -565,7 +561,7 @@ describe('Share Page Test', () => { // Remove user B's access cy.task('log', 'Removing user B access...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .closest('div.group') .within(() => { @@ -591,7 +587,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B is removed but user C still exists - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail).should('not.exist'); cy.contains(userCEmail).should('be.visible'); cy.task('log', '✓ User B removed, User C still has access'); @@ -599,7 +595,7 @@ describe('Share Page Test', () => { // Remove user C's access cy.task('log', 'Removing user C access...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userCEmail) .closest('div.group') .within(() => { @@ -625,7 +621,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify both users are removed - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail).should('not.exist'); cy.contains(userCEmail).should('not.exist'); cy.task('log', '✓ Both users successfully removed'); @@ -667,7 +663,7 @@ describe('Share Page Test', () => { TestTool.openSharePopover(); cy.task('log', 'Share popover opened'); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -677,7 +673,7 @@ describe('Share Page Test', () => { // Invite user B cy.task('log', `Inviting user B: ${userBEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') .should('be.visible') @@ -697,7 +693,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B is added - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains('People with access', { timeout: 10000 }).should('be.visible'); cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.task('log', 'User B successfully added'); @@ -705,7 +701,7 @@ describe('Share Page Test', () => { // Remove user B's access (NOT user A's own access) cy.task('log', 'Removing user B\'s access (NOT user A\'s own access)...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .should('be.visible') .closest('div.group') @@ -733,7 +729,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B is removed - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail).should('not.exist'); cy.task('log', '✓ User B removed'); }); @@ -776,7 +772,7 @@ describe('Share Page Test', () => { TestTool.openSharePopover(); cy.task('log', 'Share popover opened'); - cy.get('[data-testid="share-popover"]').then(($popover) => { + ShareSelectors.sharePopover().then(($popover) => { const hasInviteInput = $popover.find('[data-slot="email-tag-input"]').length > 0; if (!hasInviteInput) { cy.contains('Share').should('exist').click({ force: true }); @@ -786,7 +782,7 @@ describe('Share Page Test', () => { // Invite user B cy.task('log', `Inviting user B: ${userBEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') .should('be.visible') @@ -806,7 +802,7 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // Verify user B is added - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains('People with access', { timeout: 10000 }).should('be.visible'); cy.contains(userBEmail, { timeout: 10000 }).should('be.visible'); cy.task('log', 'User B successfully added'); @@ -818,7 +814,7 @@ describe('Share Page Test', () => { // Remove user B's access (NOT user A's own access) cy.task('log', 'Removing user B\'s access (verifying outline refresh mechanism)...'); - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .should('be.visible') .closest('div.group') @@ -852,7 +848,7 @@ describe('Share Page Test', () => { cy.task('log', `End time: ${endTime}, Elapsed: ${elapsed}ms`); // Verify user B is removed - cy.get('[data-testid="share-popover"]').within(() => { + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail).should('not.exist'); cy.task('log', '✓ User B removed'); }); @@ -867,4 +863,3 @@ describe('Share Page Test', () => { }); }); }); - diff --git a/cypress/e2e/space/create-space.cy.ts b/cypress/e2e/space/create-space.cy.ts index 6de2dd7d..9a3f2258 100644 --- a/cypress/e2e/space/create-space.cy.ts +++ b/cypress/e2e/space/create-space.cy.ts @@ -1,20 +1,15 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; -import { PageSelectors, SpaceSelectors, SidebarSelectors, waitForReactUpdate } from '../../support/selectors'; +import { PageSelectors, SpaceSelectors, SidebarSelectors, byTestId, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; describe('Space Creation Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; let spaceName: string; before(() => { // Log environment configuration for debugging - cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); + logAppFlowyEnvironment(); }); beforeEach(() => { @@ -73,7 +68,7 @@ describe('Space Creation Tests', () => { // Click the more actions button for spaces // It's always visible in test environment - cy.get('[data-testid="inline-more-actions"]') + SpaceSelectors.moreActionsButton() .first() .should('be.visible') .click(); @@ -87,7 +82,7 @@ describe('Space Creation Tests', () => { // Step 3: Click on "Create New Space" option cy.task('log', '=== Step 3: Clicking Create New Space option ==='); - cy.get('[data-testid="create-new-space-button"]') + cy.get(byTestId('create-new-space-button')) .should('be.visible') .click(); @@ -100,13 +95,13 @@ describe('Space Creation Tests', () => { cy.task('log', '=== Step 4: Filling space creation form ==='); // Verify the modal is visible - cy.get('[data-testid="create-space-modal"]') + cy.get(byTestId('create-space-modal')) .should('be.visible'); cy.task('log', 'Create Space modal is visible'); // Enter space name - cy.get('[data-testid="space-name-input"]') + cy.get(byTestId('space-name-input')) .should('be.visible') .clear() .type(spaceName); @@ -120,7 +115,7 @@ describe('Space Creation Tests', () => { cy.task('log', '=== Step 5: Saving new space ==='); // Click the Save button - cy.get('[data-testid="modal-ok-button"]') + cy.get(byTestId('modal-ok-button')) .should('be.visible') .click(); @@ -197,4 +192,4 @@ describe('Space Creation Tests', () => { }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/user/user.cy.ts b/cypress/e2e/user/user.cy.ts index 0965fcd3..3f557aed 100644 --- a/cypress/e2e/user/user.cy.ts +++ b/cypress/e2e/user/user.cy.ts @@ -1,19 +1,17 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; import { WorkspaceSelectors, SidebarSelectors, PageSelectors } from '../../support/selectors'; +import { generateRandomEmail, getTestEnvironment } from '../../support/test-config'; describe('User Feature Tests', () => { - const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); - const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); + const env = getTestEnvironment(); const APPFLOWY_WS_BASE_URL = Cypress.env('APPFLOWY_WS_BASE_URL'); - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; before(() => { cy.task('log', `Test Environment Configuration: - - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} - - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL} - - APPFLOWY_WS_BASE_URL: ${APPFLOWY_WS_BASE_URL} + - APPFLOWY_BASE_URL: ${env.appflowyBaseUrl} + - APPFLOWY_GOTRUE_BASE_URL: ${env.appflowyGotrueBaseUrl} + - APPFLOWY_WS_BASE_URL: ${APPFLOWY_WS_BASE_URL ?? ''} `); }); @@ -104,4 +102,4 @@ describe('User Feature Tests', () => { }); -}); \ No newline at end of file +}); diff --git a/cypress/support/api-mocks.ts b/cypress/support/api-mocks.ts new file mode 100644 index 00000000..96723d52 --- /dev/null +++ b/cypress/support/api-mocks.ts @@ -0,0 +1,213 @@ +/** + * Centralized API mocking utilities for E2E tests + * Consolidates common API intercept patterns to reduce duplication + * + * Usage: + * ```typescript + * import { mockAuthEndpoints, mockWorkspaceEndpoints, createAuthResponse } from '@/cypress/support/api-mocks'; + * + * // Mock all standard auth endpoints + * const { userId, accessToken, refreshToken } = mockAuthEndpoints(testEmail); + * + * // Mock workspace endpoints + * const { workspaceId } = mockWorkspaceEndpoints(); + * ``` + */ + +import { v4 as uuidv4 } from 'uuid'; +import { TestConfig } from './test-config'; + +/** + * Creates a standard GoTrue auth response body + * Used for password login, OTP, refresh token, etc. + */ +export const createAuthResponse = ( + email: string, + accessToken: string, + refreshToken: string, + userId = uuidv4() +) => ({ + access_token: accessToken, + refresh_token: refreshToken, + expires_at: Math.floor(Date.now() / 1000) + 3600, + expires_in: 3600, + token_type: 'bearer', + user: { + id: userId, + email, + email_confirmed_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }, +}); + +/** + * Mocks standard authentication endpoints (password login, verify, refresh) + * Returns the generated IDs and tokens for use in tests + */ +export const mockAuthEndpoints = ( + email: string, + accessToken = `mock-token-${uuidv4()}`, + refreshToken = `mock-refresh-${uuidv4()}`, + userId = uuidv4() +) => { + const { gotrueUrl, apiUrl } = TestConfig; + + // Password login + cy.intercept('POST', `${gotrueUrl}/token?grant_type=password`, { + statusCode: 200, + body: createAuthResponse(email, accessToken, refreshToken, userId), + }).as('passwordLogin'); + + // Verify token + cy.intercept('GET', `${apiUrl}/api/user/verify/${accessToken}`, { + statusCode: 200, + body: { + code: 0, + data: { + is_new: false, + }, + message: 'User verified successfully', + }, + }).as('verifyUser'); + + // Refresh token + cy.intercept('POST', `${gotrueUrl}/token?grant_type=refresh_token`, { + statusCode: 200, + body: createAuthResponse(email, accessToken, refreshToken, userId), + }).as('refreshToken'); + + return { userId, accessToken, refreshToken }; +}; + +/** + * Mocks OTP (One-Time Password) authentication endpoints + */ +export const mockOTPEndpoints = ( + email: string, + accessToken = `mock-otp-token-${uuidv4()}`, + refreshToken = `mock-otp-refresh-${uuidv4()}`, + userId = uuidv4() +) => { + const { gotrueUrl, apiUrl } = TestConfig; + + // OTP login + cy.intercept('POST', `${gotrueUrl}/otp`, { + statusCode: 200, + body: {}, + }).as('sendOTP'); + + // Verify OTP + cy.intercept('POST', `${gotrueUrl}/verify`, { + statusCode: 200, + body: createAuthResponse(email, accessToken, refreshToken, userId), + }).as('verifyOTP'); + + // Verify token + cy.intercept('GET', `${apiUrl}/api/user/verify/${accessToken}`, { + statusCode: 200, + body: { + code: 0, + data: { + is_new: false, + }, + message: 'User verified successfully', + }, + }).as('verifyUser'); + + // Refresh token + cy.intercept('POST', `${gotrueUrl}/token?grant_type=refresh_token`, { + statusCode: 200, + body: createAuthResponse(email, accessToken, refreshToken, userId), + }).as('refreshToken'); + + return { userId, accessToken, refreshToken }; +}; + +/** + * Mocks workspace-related API endpoints + * Returns workspace and user IDs for use in tests + */ +export const mockWorkspaceEndpoints = ( + workspaceId = uuidv4(), + userId = uuidv4(), + workspaceName = 'Test Workspace' +) => { + const { apiUrl } = TestConfig; + + cy.intercept('GET', `${apiUrl}/api/user/workspace`, { + statusCode: 200, + body: { + code: 0, + data: { + user_profile: { uuid: userId }, + visiting_workspace: { + workspace_id: workspaceId, + workspace_name: workspaceName, + icon: '', + created_at: Date.now().toString(), + database_storage_id: '', + owner_uid: 1, + owner_name: 'Test User', + member_count: 1, + }, + workspaces: [ + { + workspace_id: workspaceId, + workspace_name: workspaceName, + icon: '', + created_at: Date.now().toString(), + database_storage_id: '', + owner_uid: 1, + owner_name: 'Test User', + member_count: 1, + }, + ], + }, + }, + }).as('getUserWorkspaceInfo'); + + return { workspaceId, userId }; +}; + +/** + * Mocks user verification endpoint with custom response + * Useful for testing new vs existing user scenarios + */ +export const mockUserVerification = ( + accessToken: string, + isNewUser = false +) => { + const { apiUrl } = TestConfig; + + cy.intercept('GET', `${apiUrl}/api/user/verify/${accessToken}`, { + statusCode: 200, + body: { + code: 0, + data: { + is_new: isNewUser, + }, + message: 'User verified successfully', + }, + }).as('verifyUser'); +}; + +/** + * Mocks all common endpoints for a complete auth flow + * Convenience function that sets up auth + workspace mocks + */ +export const mockCompleteAuthFlow = ( + email: string, + accessToken = `mock-token-${uuidv4()}`, + refreshToken = `mock-refresh-${uuidv4()}`, + userId = uuidv4(), + workspaceId = uuidv4() +) => { + const authMocks = mockAuthEndpoints(email, accessToken, refreshToken, userId); + const workspaceMocks = mockWorkspaceEndpoints(workspaceId, userId); + + return { + ...authMocks, + ...workspaceMocks, + }; +}; diff --git a/cypress/support/chat-mocks.ts b/cypress/support/chat-mocks.ts new file mode 100644 index 00000000..b1bfbc6d --- /dev/null +++ b/cypress/support/chat-mocks.ts @@ -0,0 +1,268 @@ +/** + * Chat-specific API mocking utilities for E2E tests + * Consolidates common chat API intercept patterns + * + * Usage: + * ```typescript + * import { setupChatApiStubs, mockChatMessage } from '@/cypress/support/chat-mocks'; + * + * // Set up all chat-related API stubs + * setupChatApiStubs(); + * + * // Or mock individual endpoints + * mockChatMessage('Test message content', 123); + * mockChatSettings('Auto'); + * mockModelList(['Auto', 'GPT-4', 'Claude']); + * ``` + */ + +/** + * Default stubbed message ID for testing + */ +export const DEFAULT_MESSAGE_ID = 101; + +/** + * Default stubbed message content + */ +export const DEFAULT_MESSAGE_CONTENT = 'Stubbed AI answer ready for export'; + +/** + * Mock chat messages endpoint + * @param content - Message content (default: stubbed content) + * @param messageId - Message ID (default: 101) + * @param authorType - Author type (3 = assistant, 1 = user) + */ +export const mockChatMessage = ( + content = DEFAULT_MESSAGE_CONTENT, + messageId = DEFAULT_MESSAGE_ID, + authorType = 3 // 3 = assistant +) => { + cy.intercept('GET', '**/api/chat/**/message**', { + statusCode: 200, + body: { + code: 0, + data: { + messages: [ + { + message_id: messageId, + author: { + author_type: authorType, + author_uuid: authorType === 3 ? 'assistant' : 'user', + }, + content, + created_at: new Date().toISOString(), + meta_data: [], + }, + ], + has_more: false, + total: 1, + }, + message: 'success', + }, + }).as('getChatMessages'); +}; + +/** + * Mock empty chat messages (no messages) + */ +export const mockEmptyChatMessages = () => { + cy.intercept('GET', '**/api/chat/**/message**', { + statusCode: 200, + body: { + code: 0, + data: { + messages: [], + has_more: false, + total: 0, + }, + message: 'success', + }, + }).as('getChatMessages'); +}; + +/** + * Mock chat settings endpoint + * @param aiModel - AI model name (default: 'Auto') + * @param ragIds - RAG IDs (default: empty array) + */ +export const mockChatSettings = (aiModel = 'Auto', ragIds: string[] = []) => { + cy.intercept('GET', '**/api/chat/**/settings**', { + statusCode: 200, + body: { + code: 0, + data: { + rag_ids: ragIds, + metadata: { + ai_model: aiModel, + }, + }, + message: 'success', + }, + }).as('getChatSettings'); +}; + +/** + * Mock update chat settings endpoint + */ +export const mockUpdateChatSettings = () => { + cy.intercept('PATCH', '**/api/chat/**/settings**', { + statusCode: 200, + body: { + code: 0, + message: 'success', + }, + }).as('updateChatSettings'); +}; + +/** + * Mock AI model list endpoint + * @param modelNames - Array of model names to include + * @param defaultModel - Which model should be default (default: 'Auto') + */ +export const mockModelList = ( + modelNames: string[] = ['Auto', 'E2E Test Model'], + defaultModel = 'Auto' +) => { + const models = modelNames.map((name, index) => ({ + name, + provider: name === 'Auto' ? undefined : 'Test Provider', + metadata: { + is_default: name === defaultModel, + desc: + name === 'Auto' + ? 'Automatically select an AI model' + : `Stubbed model for testing: ${name}`, + }, + })); + + cy.intercept('GET', '**/api/ai/**/model/list**', { + statusCode: 200, + body: { + code: 0, + data: { + models, + }, + message: 'success', + }, + }).as('getModelList'); +}; + +/** + * Mock related questions endpoint + * @param messageId - Message ID (default: DEFAULT_MESSAGE_ID) + * @param questions - Array of related questions (default: empty) + */ +export const mockRelatedQuestions = ( + messageId = DEFAULT_MESSAGE_ID, + questions: string[] = [] +) => { + cy.intercept('GET', '**/api/chat/**/**/related_question**', { + statusCode: 200, + body: { + code: 0, + data: { + message_id: `${messageId}`, + items: questions.map((q, idx) => ({ + id: idx + 1, + question: q, + })), + }, + message: 'success', + }, + }).as('getRelatedQuestions'); +}; + +/** + * Mock send message endpoint + * @param responseContent - AI response content + */ +export const mockSendMessage = (responseContent = 'AI response to your message') => { + cy.intercept('POST', '**/api/chat/**/message**', { + statusCode: 200, + body: { + code: 0, + data: { + message_id: DEFAULT_MESSAGE_ID + 1, + content: responseContent, + created_at: new Date().toISOString(), + }, + message: 'success', + }, + }).as('sendMessage'); +}; + +/** + * Sets up all common chat-related API stubs + * Convenience function that mocks all standard chat endpoints + * + * @param options - Optional configuration + */ +export const setupChatApiStubs = (options?: { + messageContent?: string; + messageId?: number; + aiModel?: string; + modelNames?: string[]; + includeRelatedQuestions?: boolean; +}) => { + const { + messageContent = DEFAULT_MESSAGE_CONTENT, + messageId = DEFAULT_MESSAGE_ID, + aiModel = 'Auto', + modelNames = ['Auto', 'E2E Test Model'], + includeRelatedQuestions = true, + } = options || {}; + + // Mock chat messages + mockChatMessage(messageContent, messageId); + + // Mock chat settings + mockChatSettings(aiModel); + + // Mock update chat settings + mockUpdateChatSettings(); + + // Mock model list + mockModelList(modelNames); + + // Mock related questions + if (includeRelatedQuestions) { + mockRelatedQuestions(messageId); + } +}; + +/** + * Mock chat streaming response + * Useful for testing streaming message updates + */ +export const mockChatStreaming = (chunks: string[]) => { + let currentChunk = 0; + + cy.intercept('POST', '**/api/chat/**/stream**', (req) => { + req.reply((res) => { + const chunk = chunks[currentChunk] || ''; + currentChunk++; + + res.send({ + statusCode: 200, + body: { + chunk, + done: currentChunk >= chunks.length, + }, + }); + }); + }).as('streamMessage'); +}; + +/** + * Mock chat error response + * Useful for testing error handling + */ +export const mockChatError = (errorMessage = 'Failed to load chat') => { + cy.intercept('GET', '**/api/chat/**/message**', { + statusCode: 500, + body: { + code: 1, + message: errorMessage, + }, + }).as('getChatMessagesError'); +}; diff --git a/cypress/support/exception-handlers.ts b/cypress/support/exception-handlers.ts new file mode 100644 index 00000000..8294903a --- /dev/null +++ b/cypress/support/exception-handlers.ts @@ -0,0 +1,80 @@ +/** + * Centralized exception handlers for E2E tests + * Consolidates error handling across all test files + * + * Usage: + * ```typescript + * import { setupCommonExceptionHandlers } from '@/cypress/support/exception-handlers'; + * + * beforeEach(() => { + * setupCommonExceptionHandlers(); + * }); + * ``` + */ + +/** + * List of known non-critical errors that can be safely ignored during E2E tests + * These errors don't affect test validity but may appear in the console + */ +const IGNORED_ERROR_PATTERNS = [ + // React errors + 'Minified React error', + 'React does not recognize', + + // AppFlowy-specific errors + 'View not found', + 'No workspace or service found', + 'App outline not found', + 'Favorite views not found', + 'App trash not found', + + // Network and WebSocket errors + 'WebSocket', + 'connection', + 'Failed to fetch', + 'NetworkError', + + // AI/Model errors + 'Failed to load models', + + // Common JavaScript errors in test environment + 'Cannot read properties of undefined', + 'ResizeObserver loop', + 'Loading chunk', +]; + +/** + * Sets up common exception handlers for E2E tests + * Ignores known non-critical errors that don't affect test validity + * + * @param additionalPatterns - Optional array of additional error patterns to ignore + */ +export const setupCommonExceptionHandlers = (additionalPatterns: string[] = []) => { + const allPatterns = [...IGNORED_ERROR_PATTERNS, ...additionalPatterns]; + + cy.on('uncaught:exception', (err) => { + const shouldIgnore = allPatterns.some(pattern => + err.message.includes(pattern) + ); + + if (shouldIgnore) { + // Log warning for debugging but don't fail the test + console.warn('[Test] Ignoring known non-critical error:', err.message); + return false; // Prevent test failure + } + + // Unknown error - let it fail the test + return true; + }); +}; + +/** + * Sets up exception handlers that ignore ALL errors + * ⚠️ Use with caution - only for tests where errors are expected/irrelevant + */ +export const ignoreAllExceptions = () => { + cy.on('uncaught:exception', () => { + console.warn('[Test] Ignoring all exceptions (permissive mode)'); + return false; + }); +}; diff --git a/cypress/support/selectors.ts b/cypress/support/selectors.ts index d5798f38..2606492e 100644 --- a/cypress/support/selectors.ts +++ b/cypress/support/selectors.ts @@ -10,6 +10,17 @@ export function byTestId(id: string): string { return `[data-testid="${id}"]`; } +/** + * Helper for selectors that match data-testid prefixes or substrings + */ +export function byTestIdPrefix(prefix: string): string { + return `[data-testid^="${prefix}"]`; +} + +export function byTestIdContains(fragment: string): string { + return `[data-testid*="${fragment}"]`; +} + /** * Page-related selectors */ @@ -417,6 +428,10 @@ export const RowControlsSelectors = { export const AuthSelectors = { // Login page elements emailInput: () => cy.get(byTestId('login-email-input')), + magicLinkButton: () => cy.get(byTestId('login-magic-link-button')), + enterCodeManuallyButton: () => cy.get(byTestId('enter-code-manually-button')), + otpCodeInput: () => cy.get(byTestId('otp-code-input')), + otpSubmitButton: () => cy.get(byTestId('otp-submit-button')), // Password sign-in button passwordSignInButton: () => cy.get(byTestId('login-password-button')), @@ -432,4 +447,4 @@ export const AuthSelectors = { export function waitForReactUpdate(ms: number = 500) { return cy.wait(ms); -} \ No newline at end of file +} diff --git a/cypress/support/test-config.ts b/cypress/support/test-config.ts new file mode 100644 index 00000000..537abd0a --- /dev/null +++ b/cypress/support/test-config.ts @@ -0,0 +1,75 @@ +import { v4 as uuidv4 } from 'uuid'; + +/** + * Centralized test configuration + * Consolidates environment variable access across all E2E tests + * + * Usage: + * ```typescript + * import { TestConfig, logTestEnvironment } from '@/cypress/support/test-config'; + * + * const apiUrl = TestConfig.apiUrl; + * logTestEnvironment(); // Logs all config values + * ``` + */ + +export const TestConfig = { + /** + * Base URL for the web application + * Default: http://localhost:3000 + */ + baseUrl: Cypress.config('baseUrl') || 'http://localhost:3000', + + /** + * GoTrue authentication service URL + * Default: http://localhost/gotrue + */ + gotrueUrl: Cypress.env('APPFLOWY_GOTRUE_BASE_URL') || 'http://localhost/gotrue', + + /** + * AppFlowy Cloud API base URL + * Default: http://localhost + */ + apiUrl: Cypress.env('APPFLOWY_BASE_URL') || 'http://localhost', +} as const; + +/** + * Logs test environment configuration to Cypress task log + * Useful for debugging test failures in CI/CD + */ +export const logTestEnvironment = (env: Partial = TestConfig) => { + cy.task('log', ` +╔════════════════════════════════════════════════════════════════╗ +║ Test Environment Configuration ║ +╠════════════════════════════════════════════════════════════════╣ +║ Base URL: ${(env.baseUrl ?? TestConfig.baseUrl).padEnd(45)}║ +║ GoTrue URL: ${(env.gotrueUrl ?? TestConfig.gotrueUrl).padEnd(45)}║ +║ API URL: ${(env.apiUrl ?? TestConfig.apiUrl).padEnd(45)}║ +╚════════════════════════════════════════════════════════════════╝ + `); +}; + +/** + * Quickly fetches the AppFlowy URLs used across specs. + * Prefer this over reading Cypress.env directly to keep tests consistent. + */ +export const getTestEnvironment = () => ({ + appflowyBaseUrl: TestConfig.apiUrl, + appflowyGotrueBaseUrl: TestConfig.gotrueUrl, +}); + +/** + * Lightweight logger for the two most used URLs in tests. + */ +export const logAppFlowyEnvironment = () => { + const env = getTestEnvironment(); + cy.task( + 'log', + `Test Environment Configuration:\n - APPFLOWY_BASE_URL: ${env.appflowyBaseUrl}\n - APPFLOWY_GOTRUE_BASE_URL: ${env.appflowyGotrueBaseUrl}` + ); +}; + +/** + * Shared email generator for e2e specs. + */ +export const generateRandomEmail = (domain = 'appflowy.io') => `${uuidv4()}@${domain}`; diff --git a/cypress/support/test-helpers.ts b/cypress/support/test-helpers.ts new file mode 100644 index 00000000..cd307458 --- /dev/null +++ b/cypress/support/test-helpers.ts @@ -0,0 +1,245 @@ +/** + * General test helper utilities + * Common functions used across multiple E2E tests + * + * Usage: + * ```typescript + * import { closeModalsIfOpen, testLog, waitForReactUpdate } from '@/cypress/support/test-helpers'; + * + * closeModalsIfOpen(); + * testLog.step(1, 'Login user'); + * testLog.success('Login completed'); + * waitForReactUpdate(500); + * ``` + */ + +/** + * Closes any open modals or dialogs by pressing ESC + * Safe to call even if no modals are open + */ +export const closeModalsIfOpen = () => { + cy.get('body').then(($body) => { + const hasModal = + $body.find('[role="dialog"], .MuiDialog-container, [data-testid*="modal"]').length > 0; + + if (hasModal) { + cy.task('log', 'Closing open modal dialog'); + cy.get('body').type('{esc}'); + cy.wait(1000); + } + }); +}; + +/** + * Standardized logging utilities for test output + * Provides consistent formatting for test logs + */ +export const testLog = { + /** + * Log a test step with number + * @example testLog.step(1, 'Login user'); + */ + step: (num: number, msg: string) => cy.task('log', `=== Step ${num}: ${msg} ===`), + + /** + * Log general information + * @example testLog.info('Navigating to page'); + */ + info: (msg: string) => cy.task('log', msg), + + /** + * Log success message with checkmark + * @example testLog.success('User logged in'); + */ + success: (msg: string) => cy.task('log', `✓ ${msg}`), + + /** + * Log error message with X mark + * @example testLog.error('Login failed'); + */ + error: (msg: string) => cy.task('log', `✗ ${msg}`), + + /** + * Log warning message + * @example testLog.warn('Retrying operation'); + */ + warn: (msg: string) => cy.task('log', `⚠ ${msg}`), + + /** + * Log data in JSON format + * @example testLog.data('User info', { email, id }); + */ + data: (label: string, value: unknown) => + cy.task('log', `${label}: ${JSON.stringify(value, null, 2)}`), + + /** + * Log test start with separator + * @example testLog.testStart('OAuth Login Flow'); + */ + testStart: (testName: string) => + cy.task( + 'log', + ` +╔════════════════════════════════════════════════════════════════╗ +║ TEST: ${testName.padEnd(55)}║ +╚════════════════════════════════════════════════════════════════╝` + ), + + /** + * Log test end with separator + * @example testLog.testEnd('OAuth Login Flow'); + */ + testEnd: (testName: string) => + cy.task('log', `\n✅ TEST COMPLETED: ${testName}\n`), +}; + +/** + * Wait for React updates to complete + * Useful after DOM mutations or state changes + * + * @param ms - Milliseconds to wait (default: 500) + */ +export const waitForReactUpdate = (ms = 500) => { + cy.wait(ms); +}; + +/** + * Wait for an element to exist and be stable + * Retries if element is not found + * + * @param selector - CSS selector or test ID + * @param timeout - Max time to wait in ms (default: 10000) + */ +export const waitForElement = (selector: string, timeout = 10000) => { + cy.get(selector, { timeout }).should('exist'); + waitForReactUpdate(300); +}; + +/** + * Clear all form inputs within a container + * @param containerSelector - Optional container selector (defaults to body) + */ +export const clearAllInputs = (containerSelector = 'body') => { + cy.get(containerSelector) + .find('input, textarea') + .each(($el) => { + cy.wrap($el).clear(); + }); +}; + +/** + * Type text slowly to simulate real user input + * Useful for inputs with validation or autocomplete + * + * @param selector - Element selector + * @param text - Text to type + * @param delayMs - Delay between keystrokes in ms (default: 50) + */ +export const typeSlowly = (selector: string, text: string, delayMs = 50) => { + cy.get(selector).type(text, { delay: delayMs }); +}; + +/** + * Scroll element into view and click + * Useful for elements that might be off-screen + * + * @param selector - Element selector + */ +export const scrollAndClick = (selector: string) => { + cy.get(selector).scrollIntoView().should('be.visible').click(); +}; + +/** + * Assert that no error messages are visible on the page + * Checks for common error indicators + */ +export const assertNoErrors = () => { + cy.get('body').then(($body) => { + const hasError = + $body.text().includes('Error') || + $body.text().includes('Failed') || + $body.find('[role="alert"][data-severity="error"]').length > 0 || + $body.find('[class*="error"]').length > 0; + + if (hasError) { + testLog.warn('Error indicators detected on page'); + } + + expect(hasError).to.be.false; + }); +}; + +/** + * Wait for network requests to complete + * Useful after actions that trigger API calls + * + * @param aliasName - Cypress intercept alias (without @) + * @param timeout - Max time to wait in ms (default: 10000) + */ +export const waitForRequest = (aliasName: string, timeout = 10000) => { + cy.wait(`@${aliasName}`, { timeout }); +}; + +/** + * Retry an action until it succeeds or times out + * Useful for flaky operations + * + * @param action - Function containing the action to retry + * @param maxAttempts - Maximum number of retry attempts (default: 3) + * @param delayMs - Delay between attempts in ms (default: 1000) + */ +export const retryAction = ( + action: () => void, + maxAttempts = 3, + delayMs = 1000 +) => { + let attempts = 0; + + const tryAction = () => { + attempts++; + try { + action(); + } catch (error) { + if (attempts < maxAttempts) { + testLog.warn(`Action failed, retrying... (${attempts}/${maxAttempts})`); + cy.wait(delayMs); + tryAction(); + } else { + throw error; + } + } + }; + + tryAction(); +}; + +/** + * Check if element exists without failing test + * Returns a boolean via then() callback + * + * @param selector - Element selector + */ +export const elementExists = (selector: string) => { + return cy.get('body').then(($body) => { + return $body.find(selector).length > 0; + }); +}; + +/** + * Generate a random string for test data + * @param length - Length of string (default: 8) + */ +export const randomString = (length = 8) => { + return Math.random() + .toString(36) + .substring(2, 2 + length); +}; + +/** + * Take a screenshot with a descriptive name + * @param name - Screenshot name (test name will be prepended) + */ +export const takeScreenshot = (name: string) => { + const timestamp = Date.now(); + cy.screenshot(`${timestamp}-${name}`, { capture: 'viewport' }); +};