diff --git a/.gitignore b/.gitignore index eb328075..c577f474 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ cypress/snapshots/**/__diff_output__/ .claude cypress/screenshots cypress/videos +cypress/downloads .serena *storybook.log diff --git a/cypress.config.ts b/cypress.config.ts index db20dff7..bfb59125 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -49,8 +49,6 @@ export default defineConfig({ // Force disable fullscreen launchOptions.args.push('--force-device-scale-factor=1'); - - console.log('Chrome launch args:', launchOptions.args); } return launchOptions; diff --git a/cypress/e2e/account/avatar/avatar-api.cy.ts b/cypress/e2e/account/avatar/avatar-api.cy.ts index dd5f27f9..87f5c528 100644 --- a/cypress/e2e/account/avatar/avatar-api.cy.ts +++ b/cypress/e2e/account/avatar/avatar-api.cy.ts @@ -1,4 +1,6 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { AccountSelectors, AvatarUiSelectors } from '../../../support/selectors'; +import { testLog } from '../../../support/test-helpers'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateUserMetadata, AuthTestUtils, AvatarSelectors, WorkspaceSelectors } = imports; @@ -14,33 +16,33 @@ describe('Avatar API', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=test'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail); cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Update avatar via API'); + testLog.info( 'Step 3: Update avatar via API'); updateUserMetadata(testAvatarUrl).then((response) => { - cy.task('log', `API Response: ${JSON.stringify(response)}`); + testLog.info( `API Response: ${JSON.stringify(response)}`); expect(response.status).to.equal(200); }); - cy.task('log', 'Step 4: Reload page to see updated avatar'); + testLog.info( 'Step 4: Reload page to see updated avatar'); cy.reload(); cy.wait(3000); - cy.task('log', 'Step 5: Open Account Settings to verify avatar'); + testLog.info( 'Step 5: Open Account Settings to verify avatar'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); - cy.task('log', 'Step 6: Verify avatar image is displayed in Account Settings'); + testLog.info( 'Step 6: Verify avatar image is displayed in Account Settings'); // Note: Account Settings dialog may not display avatar directly // The avatar is displayed via getUserIconUrl which prioritizes workspace member avatar // Since we updated user metadata (icon_url), it should be available @@ -48,7 +50,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 }) + AvatarUiSelectors.image() .should('exist') .should(($imgs) => { // Find the first visible avatar image (opacity not 0) @@ -66,7 +68,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) => { + AvatarUiSelectors.image().then(($imgs) => { let foundLoaded = false; $imgs.each((index, img) => { const $img = Cypress.$(img); @@ -75,7 +77,7 @@ describe('Avatar API', () => { if (opacity > 0 && src.length > 0) { foundLoaded = true; - cy.task('log', `Found loaded avatar image with src: ${src.substring(0, 50)}...`); + testLog.info( `Found loaded avatar image with src: ${src.substring(0, 50)}...`); return false; // break } }); @@ -88,44 +90,44 @@ describe('Avatar API', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=test'; - cy.task('log', '========== Step 1: Visit login page =========='); + testLog.info( '========== Step 1: Visit login page =========='); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', '========== Step 2: Sign in with test account =========='); + testLog.info( '========== Step 2: Sign in with test account =========='); authUtils.signInWithTestUrl(testEmail); cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', '========== Step 3: Get token from localStorage =========='); + testLog.info( '========== Step 3: Get token from localStorage =========='); cy.window() .its('localStorage') .invoke('getItem', 'token') .then((tokenStr) => { - cy.task('log', `Token string: ${tokenStr ? 'Found' : 'Not found'}`); + testLog.info( `Token string: ${tokenStr ? 'Found' : 'Not found'}`); const token = JSON.parse(tokenStr); const accessToken = token.access_token; - cy.task('log', `Access token: ${accessToken ? 'Present (length: ' + accessToken.length + ')' : 'Missing'}`); + testLog.info( `Access token: ${accessToken ? 'Present (length: ' + accessToken.length + ')' : 'Missing'}`); }); - cy.task('log', '========== Step 4: Making API request =========='); - cy.task('log', `URL: ${avatarTestUtils.APPFLOWY_BASE_URL}/api/user/update`); - cy.task('log', `Avatar URL: ${testAvatarUrl}`); + testLog.info( '========== Step 4: Making API request =========='); + testLog.info( `URL: ${avatarTestUtils.APPFLOWY_BASE_URL}/api/user/update`); + testLog.info( `Avatar URL: ${testAvatarUrl}`); updateUserMetadata(testAvatarUrl).then((response) => { - cy.task('log', '========== Step 5: Checking response =========='); - cy.task('log', `Response is null: ${response === null}`); - cy.task('log', `Response type: ${typeof response}`); - cy.task('log', `Response status: ${response?.status}`); - cy.task('log', `Response body: ${JSON.stringify(response?.body)}`); - cy.task('log', `Response headers: ${JSON.stringify(response?.headers)}`); + testLog.info( '========== Step 5: Checking response =========='); + testLog.info( `Response is null: ${response === null}`); + testLog.info( `Response type: ${typeof response}`); + testLog.info( `Response status: ${response?.status}`); + testLog.info( `Response body: ${JSON.stringify(response?.body)}`); + testLog.info( `Response headers: ${JSON.stringify(response?.headers)}`); expect(response).to.not.be.null; expect(response.status).to.equal(200); if (response.body) { - cy.task('log', `Response body code: ${response.body.code}`); - cy.task('log', `Response body message: ${response.body.message}`); + testLog.info( `Response body code: ${response.body.code}`); + testLog.info( `Response body message: ${response.body.message}`); } }); }); @@ -135,33 +137,33 @@ describe('Avatar API', () => { const authUtils = new AuthTestUtils(); const testEmoji = 'šŸŽØ'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail); cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Update avatar to emoji via API'); + testLog.info( 'Step 3: Update avatar to emoji via API'); updateUserMetadata(testEmoji).then((response) => { expect(response).to.not.be.null; expect(response.status).to.equal(200); }); - cy.task('log', 'Step 4: Reload page'); + testLog.info( 'Step 4: Reload page'); cy.reload(); cy.wait(3000); - cy.task('log', 'Step 5: Open Account Settings'); + testLog.info( 'Step 5: Open Account Settings'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); - cy.task('log', 'Step 6: Verify emoji is displayed in fallback'); + testLog.info( 'Step 6: Verify emoji is displayed in fallback'); AvatarSelectors.avatarFallback().should('contain.text', testEmoji); }); @@ -169,20 +171,20 @@ describe('Avatar API', () => { const testEmail = generateRandomEmail(); const authUtils = new AuthTestUtils(); - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account (no avatar set)'); + testLog.info( 'Step 2: Sign in with test account (no avatar set)'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Open workspace dropdown to see avatar'); + testLog.info( 'Step 3: Open workspace dropdown to see avatar'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(500); - cy.task('log', 'Step 4: Verify fallback is displayed in workspace dropdown avatar'); + testLog.info( 'Step 4: Verify fallback is displayed in workspace dropdown avatar'); AvatarSelectors.workspaceDropdownAvatar().within(() => { AvatarSelectors.avatarFallback().should('be.visible'); }); @@ -190,4 +192,3 @@ describe('Avatar API', () => { }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-database.cy.ts b/cypress/e2e/account/avatar/avatar-database.cy.ts index 8fdd9122..3d69fdd1 100644 --- a/cypress/e2e/account/avatar/avatar-database.cy.ts +++ b/cypress/e2e/account/avatar/avatar-database.cy.ts @@ -1,4 +1,5 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { testLog } from '../../../support/test-helpers'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateWorkspaceMemberAvatar, AuthTestUtils, dbUtils } = imports; @@ -14,16 +15,16 @@ describe('Avatar Database', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=db-test'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set avatar via API'); + testLog.info( 'Step 3: Set avatar via API'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -33,7 +34,7 @@ describe('Avatar Database', () => { cy.wait(3000); - cy.task('log', 'Step 4: Verify avatar is stored in database'); + testLog.info( 'Step 4: Verify avatar is stored in database'); dbUtils.getCurrentUserUuid().then((userUuid) => { expect(userUuid).to.not.be.null; diff --git a/cypress/e2e/account/avatar/avatar-header.cy.ts b/cypress/e2e/account/avatar/avatar-header.cy.ts index 922ff82c..d205877b 100644 --- a/cypress/e2e/account/avatar/avatar-header.cy.ts +++ b/cypress/e2e/account/avatar/avatar-header.cy.ts @@ -1,3 +1,6 @@ +import { TestTool } from '../../../support/page-utils'; +import { PageSelectors } from '../../../support/selectors'; +import { testLog } from '../../../support/test-helpers'; import { avatarTestUtils } from './avatar-test-utils'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; @@ -14,20 +17,23 @@ describe('Avatar Header Display', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=header-test'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info('Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info('Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set avatar via workspace member profile API'); + testLog.info('Step 3: Set avatar via workspace member profile API'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; - updateWorkspaceMemberAvatar(workspaceId!, testAvatarUrl).then((response) => { + // Update avatar and wait for it to complete + cy.wrap(null).then(() => { + return updateWorkspaceMemberAvatar(workspaceId!, testAvatarUrl); + }).then((response) => { expect(response.status).to.equal(200); }); @@ -35,15 +41,17 @@ describe('Avatar Header Display', () => { cy.reload(); cy.wait(3000); - cy.task('log', 'Step 4: Interact with editor to trigger collaborative user awareness'); + testLog.info('Step 4: Interact with editor to trigger collaborative user awareness'); + // Expand space first to make pages visible + TestTool.expandSpace(0); + cy.wait(1000); + + // Wait for pages to be visible + PageSelectors.names().should('be.visible', { timeout: 10000 }); + // 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(); - } else if ($body.text().includes('Getting started')) { - cy.contains('Getting started').click(); - } + PageSelectors.names().first().then($page => { + cy.wrap($page).click({ force: true }); }); cy.wait(2000); @@ -73,13 +81,13 @@ describe('Avatar Header Display', () => { cy.wait(2000); - cy.task('log', 'Step 5: Verify avatar appears in header top right corner'); + testLog.info('Step 5: Verify avatar appears in header top right corner'); // Wait for header to be visible cy.get('.appflowy-top-bar').should('be.visible'); // Check if avatar container exists in header (collaborative users area) // The current user's avatar will appear there when they're actively editing - cy.task('log', 'Header avatar area should be visible'); + testLog.info('Header avatar area should be visible'); AvatarSelectors.headerAvatarContainer().should('exist'); // Verify avatar image or fallback is present @@ -93,20 +101,23 @@ describe('Avatar Header Display', () => { const authUtils = new AuthTestUtils(); const testEmoji = 'šŸŽØ'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info('Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info('Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set emoji avatar via API'); + testLog.info('Step 3: Set emoji avatar via API'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; - updateWorkspaceMemberAvatar(workspaceId!, testEmoji).then((response) => { + // Update avatar and wait for it to complete + cy.wrap(null).then(() => { + return updateWorkspaceMemberAvatar(workspaceId!, testEmoji); + }).then((response) => { expect(response.status).to.equal(200); }); @@ -114,14 +125,17 @@ describe('Avatar Header Display', () => { cy.reload(); cy.wait(3000); - cy.task('log', 'Step 4: Interact with editor to trigger collaborative user awareness'); + testLog.info('Step 4: Interact with editor to trigger collaborative user awareness'); + // Expand space first to make pages visible + TestTool.expandSpace(0); + cy.wait(1000); + + // Wait for pages to be visible + PageSelectors.names().should('be.visible', { timeout: 10000 }); + // 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(); - } else if ($body.text().includes('Getting started')) { - cy.contains('Getting started').click(); - } + PageSelectors.names().first().then($page => { + cy.wrap($page).click({ force: true }); }); cy.wait(2000); @@ -150,12 +164,12 @@ describe('Avatar Header Display', () => { cy.wait(2000); - cy.task('log', 'Step 5: Verify emoji appears in header avatar fallback'); + testLog.info('Step 5: Verify emoji appears in header avatar fallback'); cy.get('.appflowy-top-bar').should('be.visible'); // When user is actively editing, their avatar should appear in header // Emoji avatars show in fallback - cy.task('log', 'Header should be visible with avatar area'); + testLog.info('Header should be visible with avatar area'); AvatarSelectors.headerAvatarContainer().should('exist'); // Verify emoji appears in fallback @@ -170,23 +184,23 @@ describe('Avatar Header Display', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=header-notification'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info('Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info('Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Get user UUID and workspace ID'); + testLog.info('Step 3: Get user UUID and workspace ID'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; dbUtils.getCurrentUserUuid().then((userUuid) => { expect(userUuid).to.not.be.null; - cy.task('log', 'Step 4: Simulate workspace member profile changed notification'); + testLog.info('Step 4: Simulate workspace member profile changed notification'); cy.window().then((win) => { const emitter = (win as typeof window & { __APPFLOWY_EVENT_EMITTER__?: { emit: (...args: unknown[]) => void }; @@ -204,20 +218,23 @@ describe('Avatar Header Display', () => { cy.wait(2000); - cy.task('log', 'Step 5: Verify avatar is updated in database'); + testLog.info('Step 5: Verify avatar is updated in database'); dbUtils.getWorkspaceMemberProfile(workspaceId!, userUuid!).then((profile) => { expect(profile).to.not.be.null; expect(profile?.avatar_url).to.equal(testAvatarUrl); }); - cy.task('log', 'Step 6: Interact with editor to trigger collaborative user awareness'); + testLog.info('Step 6: Interact with editor to trigger collaborative user awareness'); + // Expand space first to make pages visible + TestTool.expandSpace(0); + cy.wait(1000); + + // Wait for pages to be visible + PageSelectors.names().should('be.visible', { timeout: 10000 }); + // 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(); - } else if ($body.text().includes('Getting started')) { - cy.contains('Getting started').click(); - } + PageSelectors.names().first().then($page => { + cy.wrap($page).click({ force: true }); }); cy.wait(2000); @@ -246,7 +263,7 @@ describe('Avatar Header Display', () => { cy.wait(2000); - cy.task('log', 'Step 7: Verify header avatar area is visible and updated'); + testLog.info('Step 7: Verify header avatar area is visible and updated'); cy.get('.appflowy-top-bar').should('be.visible'); AvatarSelectors.headerAvatarContainer().should('exist'); @@ -256,11 +273,10 @@ describe('Avatar Header Display', () => { // Verify the avatar image uses the updated URL (if image is loaded) // The avatar might show as image or fallback depending on loading state // We already verified the database update in Step 5, so just verify avatar container exists - cy.task('log', 'Avatar container verified in header - database update confirmed in Step 5'); + testLog.info('Avatar container verified in header - database update confirmed in Step 5'); }); }); }); }); }); }); - diff --git a/cypress/e2e/account/avatar/avatar-notifications.cy.ts b/cypress/e2e/account/avatar/avatar-notifications.cy.ts index 6b6494a5..c98f3006 100644 --- a/cypress/e2e/account/avatar/avatar-notifications.cy.ts +++ b/cypress/e2e/account/avatar/avatar-notifications.cy.ts @@ -1,4 +1,6 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { AccountSelectors } from '../../../support/selectors'; +import { testLog } from '../../../support/test-helpers'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { APP_EVENTS, updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -14,23 +16,23 @@ describe('Avatar Notifications', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=notification-test'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Get user UUID and workspace ID'); + testLog.info( 'Step 3: Get user UUID and workspace ID'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; dbUtils.getCurrentUserUuid().then((userUuid) => { expect(userUuid).to.not.be.null; - cy.task('log', 'Step 4: Simulate workspace member profile changed notification'); + testLog.info( 'Step 4: Simulate workspace member profile changed notification'); cy.window().then((win) => { const emitter = (win as typeof window & { __APPFLOWY_EVENT_EMITTER__?: { emit: (...args: unknown[]) => void }; @@ -48,23 +50,23 @@ describe('Avatar Notifications', () => { cy.wait(2000); - cy.task('log', 'Step 5: Verify avatar is updated in database'); + testLog.info( 'Step 5: Verify avatar is updated in database'); dbUtils.getWorkspaceMemberProfile(workspaceId!, userUuid!).then((profile) => { expect(profile).to.not.be.null; expect(profile?.avatar_url).to.equal(testAvatarUrl); }); - cy.task('log', 'Step 6: Reload page and verify avatar persists'); + testLog.info( 'Step 6: Reload page and verify avatar persists'); cy.reload(); cy.wait(3000); - cy.task('log', 'Step 7: Open Account Settings to verify avatar'); + testLog.info( 'Step 7: Open Account Settings to verify avatar'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); - cy.task('log', 'Step 8: Verify avatar image uses updated URL'); + testLog.info( 'Step 8: Verify avatar image uses updated URL'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', testAvatarUrl); }); }); @@ -76,16 +78,16 @@ describe('Avatar Notifications', () => { const authUtils = new AuthTestUtils(); const existingAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=existing'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set initial avatar via API'); + testLog.info( 'Step 3: Set initial avatar via API'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -95,16 +97,16 @@ describe('Avatar Notifications', () => { cy.wait(2000); - cy.task('log', 'Step 4: Get user UUID and workspace ID'); + testLog.info( 'Step 4: Get user UUID and workspace ID'); dbUtils.getCurrentUserUuid().then((userUuid) => { expect(userUuid).to.not.be.null; - cy.task('log', 'Step 5: Verify initial avatar is set'); + testLog.info( 'Step 5: Verify initial avatar is set'); dbUtils.getWorkspaceMemberProfile(workspaceId!, userUuid!).then((profile) => { expect(profile?.avatar_url).to.equal(existingAvatarUrl); }); - cy.task('log', 'Step 6: Simulate notification without avatar field'); + testLog.info( 'Step 6: Simulate notification without avatar field'); cy.window().then((win) => { const emitter = (win as typeof window & { __APPFLOWY_EVENT_EMITTER__?: { emit: (...args: unknown[]) => void }; @@ -120,7 +122,7 @@ describe('Avatar Notifications', () => { cy.wait(2000); - cy.task('log', 'Step 7: Verify avatar is preserved'); + testLog.info( 'Step 7: Verify avatar is preserved'); dbUtils.getWorkspaceMemberProfile(workspaceId!, userUuid!).then((profile) => { expect(profile?.avatar_url).to.equal(existingAvatarUrl); expect(profile?.name).to.equal('Updated Name'); @@ -135,16 +137,16 @@ describe('Avatar Notifications', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=to-clear'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set initial avatar'); + testLog.info( 'Step 3: Set initial avatar'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -157,7 +159,7 @@ describe('Avatar Notifications', () => { dbUtils.getCurrentUserUuid().then((userUuid) => { expect(userUuid).to.not.be.null; - cy.task('log', 'Step 4: Simulate notification with empty avatar'); + testLog.info( 'Step 4: Simulate notification with empty avatar'); cy.window().then((win) => { const emitter = (win as typeof window & { __APPFLOWY_EVENT_EMITTER__?: { emit: (...args: unknown[]) => void }; @@ -173,7 +175,7 @@ describe('Avatar Notifications', () => { cy.wait(2000); - cy.task('log', 'Step 5: Verify avatar is cleared'); + testLog.info( 'Step 5: Verify avatar is cleared'); dbUtils.getWorkspaceMemberProfile(workspaceId!, userUuid!).then((profile) => { expect(profile?.avatar_url).to.be.null; }); @@ -183,4 +185,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..d446d1b9 100644 --- a/cypress/e2e/account/avatar/avatar-persistence.cy.ts +++ b/cypress/e2e/account/avatar/avatar-persistence.cy.ts @@ -1,4 +1,6 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { AccountSelectors } from '../../../support/selectors'; +import { testLog } from '../../../support/test-helpers'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -13,16 +15,16 @@ describe('Avatar Persistence', () => { const authUtils = new AuthTestUtils(); const testAvatarUrl = 'https://api.dicebear.com/7.x/avataaars/svg?seed=persist'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set avatar via workspace member profile API'); + testLog.info( 'Step 3: Set avatar via workspace member profile API'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -32,25 +34,25 @@ describe('Avatar Persistence', () => { cy.wait(2000); - cy.task('log', 'Step 4: Reload page'); + testLog.info( 'Step 4: Reload page'); cy.reload(); cy.wait(3000); - cy.task('log', 'Step 5: Verify avatar persisted'); + testLog.info( 'Step 5: Verify avatar persisted'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', testAvatarUrl); - cy.task('log', 'Step 6: Reload again to verify persistence'); + testLog.info( 'Step 6: Reload again to verify persistence'); cy.reload(); cy.wait(3000); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', testAvatarUrl); @@ -58,4 +60,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..8da93f03 100644 --- a/cypress/e2e/account/avatar/avatar-priority.cy.ts +++ b/cypress/e2e/account/avatar/avatar-priority.cy.ts @@ -1,4 +1,6 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { AccountSelectors } from '../../../support/selectors'; +import { testLog } from '../../../support/test-helpers'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateUserMetadata, updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -14,23 +16,23 @@ describe('Avatar Priority', () => { const userMetadataAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=user-metadata'; const workspaceAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=workspace'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Set user metadata avatar'); + testLog.info( 'Step 3: Set user metadata avatar'); updateUserMetadata(userMetadataAvatar).then((response) => { expect(response.status).to.equal(200); }); cy.wait(2000); - cy.task('log', 'Step 4: Set workspace member avatar'); + testLog.info( 'Step 4: Set workspace member avatar'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -42,10 +44,10 @@ describe('Avatar Priority', () => { cy.reload(); cy.wait(3000); - cy.task('log', 'Step 5: Verify workspace avatar is displayed (priority)'); + testLog.info( 'Step 5: Verify workspace avatar is displayed (priority)'); WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); // Workspace avatar should be displayed, not user metadata avatar @@ -54,4 +56,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..47eace8b 100644 --- a/cypress/e2e/account/avatar/avatar-types.cy.ts +++ b/cypress/e2e/account/avatar/avatar-types.cy.ts @@ -1,4 +1,6 @@ import { avatarTestUtils } from './avatar-test-utils'; +import { AccountSelectors } from '../../../support/selectors'; +import { testLog } from '../../../support/test-helpers'; const { generateRandomEmail, setupBeforeEach, imports } = avatarTestUtils; const { updateWorkspaceMemberAvatar, AuthTestUtils, AvatarSelectors, dbUtils, WorkspaceSelectors } = imports; @@ -13,16 +15,16 @@ describe('Avatar Types', () => { const authUtils = new AuthTestUtils(); const httpsAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=https'; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Test HTTPS avatar URL'); + testLog.info( 'Step 3: Test HTTPS avatar URL'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -36,7 +38,7 @@ describe('Avatar Types', () => { WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); AvatarSelectors.avatarImage().should('exist').and('have.attr', 'src', httpsAvatar); @@ -49,16 +51,16 @@ describe('Avatar Types', () => { const authUtils = new AuthTestUtils(); const emojiAvatars = ['šŸŽØ', 'šŸš€', '⭐', 'šŸŽÆ']; - cy.task('log', 'Step 1: Visit login page'); + testLog.info( 'Step 1: Visit login page'); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); - cy.task('log', 'Step 2: Sign in with test account'); + testLog.info( 'Step 2: Sign in with test account'); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url({ timeout: 30000 }).should('include', '/app'); cy.wait(3000); - cy.task('log', 'Step 3: Test each emoji avatar'); + testLog.info( 'Step 3: Test each emoji avatar'); dbUtils.getCurrentWorkspaceId().then((workspaceId) => { expect(workspaceId).to.not.be.null; @@ -73,7 +75,7 @@ describe('Avatar Types', () => { WorkspaceSelectors.dropdownTrigger().click(); cy.wait(1000); - cy.get('[data-testid="account-settings-button"]').click(); + AccountSelectors.settingsButton().click(); AvatarSelectors.accountSettingsDialog().should('be.visible'); // Emoji should be displayed in fallback, not as image @@ -83,4 +85,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..5d1953b1 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, AccountSelectors } 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(); + AccountSelectors.settingsButton().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'); + AccountSelectors.settingsDialog().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'); + AccountSelectors.dateFormatDropdown().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(); + AccountSelectors.dateFormatDropdown().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(); + AccountSelectors.dateFormatOptionYearMonthDay().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'); + AccountSelectors.dateFormatDropdown().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(); + AccountSelectors.timeFormatDropdown().should('be.visible').click(); cy.wait(500); // Select 24-hour format (value 1) - cy.get('[data-testid="time-format-1"]').should('be.visible').click(); + AccountSelectors.timeFormatOption24().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'); + AccountSelectors.timeFormatDropdown().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(); + AccountSelectors.startWeekDropdown().should('be.visible').click(); cy.wait(500); // Select Monday (value 1) - cy.get('[data-testid="start-week-1"]').should('be.visible').click(); + AccountSelectors.startWeekMonday().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'); + AccountSelectors.startWeekDropdown().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'); + AccountSelectors.dateFormatDropdown().should('contain.text', 'Year/Month/Day'); + AccountSelectors.timeFormatDropdown().should('contain.text', '24'); + AccountSelectors.startWeekDropdown().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..e979516d 100644 --- a/cypress/e2e/app/sidebar-components.cy.ts +++ b/cypress/e2e/app/sidebar-components.cy.ts @@ -1,9 +1,9 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { PageSelectors, SidebarSelectors } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; describe('Sidebar Components Resilience Tests', () => { - const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; let testEmail: string; beforeEach(() => { @@ -35,10 +35,10 @@ describe('Sidebar Components Resilience Tests', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in successfully'); + testLog.info( 'Signed in successfully'); // Wait for app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); cy.wait(3000); @@ -56,9 +56,9 @@ describe('Sidebar Components Resilience Tests', () => { }); if (errorBoundaryLogs.length > 0) { - cy.task('log', `Found ${errorBoundaryLogs.length} React error boundary logs`); + testLog.info( `Found ${errorBoundaryLogs.length} React error boundary logs`); errorBoundaryLogs.forEach((log: any) => { - cy.task('log', `Error boundary log: ${JSON.stringify(log.args)}`); + testLog.info( `Error boundary log: ${JSON.stringify(log.args)}`); }); } @@ -67,13 +67,13 @@ describe('Sidebar Components Resilience Tests', () => { }); // Verify sidebar is visible and functional - cy.task('log', 'Verifying sidebar is visible and functional'); + testLog.info( 'Verifying sidebar is visible and functional'); SidebarSelectors.pageHeader().should('be.visible'); // Verify we can interact with the sidebar without errors PageSelectors.items().should('exist'); - cy.task('log', 'Sidebar components loaded successfully without errors'); + testLog.info( 'Sidebar components loaded successfully without errors'); }); }); @@ -102,7 +102,7 @@ describe('Sidebar Components Resilience Tests', () => { expect(favoriteErrors.length).to.equal(0, 'Favorite component should handle empty state gracefully'); }); - cy.task('log', 'App handles empty favorites state correctly'); + testLog.info( 'App handles empty favorites state correctly'); }); }); @@ -134,7 +134,7 @@ describe('Sidebar Components Resilience Tests', () => { expect(shareWithMeErrors.length).to.equal(0, 'ShareWithMe component should handle empty state gracefully'); }); - cy.task('log', 'App handles ShareWithMe with no shared content correctly'); + testLog.info( 'App handles ShareWithMe with no shared content correctly'); }); }); @@ -168,7 +168,7 @@ describe('Sidebar Components Resilience Tests', () => { expect(outlineErrors.length).to.equal(0, 'Components should handle invalid outline data gracefully'); }); - cy.task('log', 'App handles invalid outline data correctly'); + testLog.info( 'App handles invalid outline data correctly'); }); }); @@ -202,8 +202,7 @@ describe('Sidebar Components Resilience Tests', () => { expect(dateErrors.length).to.equal(0, 'Favorite component should handle invalid dates gracefully'); }); - cy.task('log', 'App handles invalid favorite dates correctly'); + testLog.info( 'App handles invalid favorite dates correctly'); }); }); }); - 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..d3d660fc 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, ChatSelectors } 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'); + ChatSelectors.formatGroup().then($group => { + if ($group.length > 0) { + ChatSelectors.formatToggle().click(); + ChatSelectors.formatGroup().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'); + ChatSelectors.formatToggle().should('be.visible').click(); + ChatSelectors.formatGroup().should('exist'); + ChatSelectors.formatGroup().find('button').should('have.length.at.least', 4); + ChatSelectors.formatToggle().click(); + ChatSelectors.formatGroup().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(); + ChatSelectors.browsePromptsButton().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'); + ChatSelectors.relatedViewsButton().click(); + ChatSelectors.relatedViewsPopover().should('be.visible'); cy.get('body').type('{esc}'); - cy.get('[data-testid="chat-related-views-popover"]').should('not.exist'); + ChatSelectors.relatedViewsPopover().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 => { + ChatSelectors.sendButton().should('exist'); + ChatSelectors.sendButton().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 => { + ChatSelectors.sendButton().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(); + ChatSelectors.sendButton().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..a366f85d 100644 --- a/cypress/e2e/chat/create-ai-chat.cy.ts +++ b/cypress/e2e/chat/create-ai-chat.cy.ts @@ -1,20 +1,16 @@ -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, ChatSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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(() => { @@ -42,7 +38,7 @@ describe('AI Chat Creation and Navigation Tests', () => { }); // Step 1: Login - cy.task('log', '=== Step 1: Login ==='); + testLog.info( '=== Step 1: Login ==='); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); @@ -51,7 +47,7 @@ describe('AI Chat Creation and Navigation Tests', () => { cy.url().should('include', '/app'); // Wait for the app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); // Wait for the loading screen to disappear and main app to appear cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); @@ -66,26 +62,26 @@ describe('AI Chat Creation and Navigation Tests', () => { cy.wait(2000); // Now wait for the new page button to be available - cy.task('log', 'Looking for new page button...'); + testLog.info( 'Looking for new page button...'); PageSelectors.newPageButton() .should('exist', { timeout: 20000 }) .then(() => { - cy.task('log', 'New page button found!'); + testLog.info( 'New page button found!'); }); // Step 2: Find a space/document that has the add button - cy.task('log', '=== Step 2: Finding a space/document with add button ==='); + testLog.info( '=== Step 2: Finding a space/document with add button ==='); // Expand the first space to see its pages TestTool.expandSpace(); cy.wait(1000); // Find the first page item and hover over it to show actions - cy.task('log', 'Finding first page item to access add actions...'); + testLog.info( 'Finding first page item to access add actions...'); // Get the first page and hover to show the inline actions PageSelectors.items().first().then($page => { - cy.task('log', 'Hovering over first page to show action buttons...'); + testLog.info( 'Hovering over first page to show action buttons...'); // Hover over the page to reveal the action buttons cy.wrap($page) @@ -96,44 +92,44 @@ 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 }); }); - cy.task('log', 'Clicked inline add page button'); + testLog.info( 'Clicked inline add page button'); }); // Wait for the dropdown menu to appear cy.wait(1000); // Step 3: Click on AI Chat option from the dropdown - cy.task('log', '=== Step 3: Creating AI Chat ==='); + testLog.info( '=== 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(); - cy.task('log', 'Clicked AI Chat option from dropdown'); + testLog.info( 'Clicked AI Chat option from dropdown'); // Wait for navigation to the AI chat page cy.wait(3000); // Step 4: Verify AI Chat page loaded successfully - cy.task('log', '=== Step 4: Verifying AI Chat page loaded ==='); + testLog.info( '=== Step 4: Verifying AI Chat page loaded ==='); // Check that the URL contains a view ID (indicating navigation to chat) cy.url().should('match', /\/app\/[a-f0-9-]+\/[a-f0-9-]+/, { timeout: 10000 }); - cy.task('log', 'āœ“ Navigated to AI Chat page'); + testLog.info( 'āœ“ Navigated to AI Chat page'); // 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) { - cy.task('log', 'āœ“ AI Chat container exists'); + ChatSelectors.aiChatContainer().then($container => { + if ($container.length > 0) { + testLog.info( 'āœ“ AI Chat container exists'); } else { - cy.task('log', 'AI Chat container not immediately visible, checking for navigation success...'); + testLog.info( 'AI Chat container not immediately visible, checking for navigation success...'); } }); @@ -143,15 +139,14 @@ describe('AI Chat Creation and Navigation Tests', () => { // Check for AI Chat specific elements (the chat interface) // The AI chat library loads its own components 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; - - if (hasChatElements) { - cy.task('log', 'āœ“ AI Chat interface loaded'); - } else { - cy.task('log', 'Warning: AI Chat elements not immediately visible, but container exists'); - } + ChatSelectors.aiChatContainer().then($container => { + const hasChatElements = $body.find('.ai-chat').length > 0 || $container.length > 0; + if (hasChatElements) { + testLog.info( 'āœ“ AI Chat interface loaded'); + } else { + testLog.info( 'Warning: AI Chat elements not immediately visible, but container exists'); + } + }); }); // Verify no error messages are displayed @@ -166,11 +161,11 @@ describe('AI Chat Creation and Navigation Tests', () => { throw new Error('Error detected on AI Chat page'); } - cy.task('log', 'āœ“ No errors detected on page'); + testLog.info( 'āœ“ No errors detected on page'); }); // Step 5: Basic verification that we're on a chat page - cy.task('log', '=== Step 5: Final verification ==='); + testLog.info( '=== Step 5: Final verification ==='); // Simply verify that: // 1. We navigated to a new page (URL changed) @@ -178,18 +173,18 @@ describe('AI Chat Creation and Navigation Tests', () => { // 3. The page appears to have loaded cy.url().then(url => { - cy.task('log', `Current URL: ${url}`); + testLog.info( `Current URL: ${url}`); // Verify we're on a view page if (url.includes('/app/') && url.split('/').length >= 5) { - cy.task('log', 'āœ“ Successfully navigated to a view page'); + testLog.info( 'āœ“ Successfully navigated to a view page'); } }); // Final verification - cy.task('log', '=== Test completed successfully! ==='); - cy.task('log', 'āœ“āœ“āœ“ AI Chat created and opened without errors'); + testLog.info( '=== Test completed successfully! ==='); + testLog.info( 'āœ“āœ“āœ“ AI Chat created and opened without errors'); }); }); }); -}); \ 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..f148aba0 100644 --- a/cypress/e2e/chat/model-selection-persistence.cy.ts +++ b/cypress/e2e/chat/model-selection-persistence.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, SidebarSelectors, ModelSelectorSelectors } from '../../support/selectors'; +import { AddPageSelectors, PageSelectors, SidebarSelectors, ModelSelectorSelectors } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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(() => { @@ -38,7 +34,7 @@ describe('Chat Model Selection Persistence Tests', () => { }); // Step 1: Login - cy.task('log', '=== Step 1: Login ==='); + testLog.info( '=== Step 1: Login ==='); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); @@ -47,7 +43,7 @@ describe('Chat Model Selection Persistence Tests', () => { cy.url().should('include', '/app'); // Wait for the app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); // Wait for the loading screen to disappear and main app to appear cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); @@ -62,7 +58,7 @@ describe('Chat Model Selection Persistence Tests', () => { cy.wait(2000); // Step 2: Create an AI Chat - cy.task('log', '=== Step 2: Creating AI Chat ==='); + testLog.info( '=== Step 2: Creating AI Chat ==='); // Expand the first space to see its pages TestTool.expandSpace(); @@ -70,7 +66,7 @@ describe('Chat Model Selection Persistence Tests', () => { // Find the first page item and hover over it to show actions PageSelectors.items().first().then($page => { - cy.task('log', 'Hovering over first page to show action buttons...'); + testLog.info( 'Hovering over first page to show action buttons...'); // Hover over the page to reveal the action buttons cy.wrap($page) @@ -81,7 +77,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,17 +88,17 @@ 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(); - cy.task('log', 'Created AI Chat'); + testLog.info( 'Created AI Chat'); // Wait for navigation to the AI chat page cy.wait(3000); // Step 3: Open model selector and select a model - cy.task('log', '=== Step 3: Selecting a Model ==='); + testLog.info( '=== Step 3: Selecting a Model ==='); // Wait for the chat interface to load cy.wait(2000); @@ -112,7 +108,7 @@ describe('Chat Model Selection Persistence Tests', () => { .should('be.visible', { timeout: 10000 }) .click(); - cy.task('log', 'Opened model selector dropdown'); + testLog.info( 'Opened model selector dropdown'); // Wait for the dropdown to appear and models to load cy.wait(2000); @@ -129,14 +125,14 @@ describe('Chat Model Selection Persistence Tests', () => { if (nonAutoOptions.length > 0) { // Click the first non-Auto model const selectedModel = nonAutoOptions[0].getAttribute('data-testid')?.replace('model-option-', ''); - cy.task('log', `Selecting model: ${selectedModel}`); + testLog.info( `Selecting model: ${selectedModel}`); cy.wrap(nonAutoOptions[0]).click(); // Store the selected model name for verification cy.wrap(selectedModel).as('selectedModel'); } else { // If only Auto is available, select it explicitly - cy.task('log', 'Only Auto model available, selecting it'); + testLog.info( 'Only Auto model available, selecting it'); ModelSelectorSelectors.optionByName('Auto').click(); cy.wrap('Auto').as('selectedModel'); } @@ -147,27 +143,27 @@ describe('Chat Model Selection Persistence Tests', () => { // Verify the model is selected by checking the button text cy.get('@selectedModel').then((modelName) => { - cy.task('log', `Verifying model ${modelName} is displayed in button`); + testLog.info( `Verifying model ${modelName} is displayed in button`); ModelSelectorSelectors.button() .should('contain.text', modelName); }); // Step 4: Save the current URL for reload - cy.task('log', '=== Step 4: Saving current URL ==='); + testLog.info( '=== Step 4: Saving current URL ==='); cy.url().then(url => { cy.wrap(url).as('chatUrl'); - cy.task('log', `Current chat URL: ${url}`); + testLog.info( `Current chat URL: ${url}`); }); // Step 5: Reload the page - cy.task('log', '=== Step 5: Reloading page ==='); + testLog.info( '=== Step 5: Reloading page ==='); cy.reload(); // Wait for the page to reload completely cy.wait(3000); // Step 6: Verify the model selection persisted - cy.task('log', '=== Step 6: Verifying Model Selection Persisted ==='); + testLog.info( '=== Step 6: Verifying Model Selection Persisted ==='); // Wait for the model selector button to be visible again ModelSelectorSelectors.button() @@ -175,14 +171,14 @@ describe('Chat Model Selection Persistence Tests', () => { // Verify the previously selected model is still displayed cy.get('@selectedModel').then((modelName) => { - cy.task('log', `Checking if model ${modelName} is still selected after reload`); + testLog.info( `Checking if model ${modelName} is still selected after reload`); ModelSelectorSelectors.button() .should('contain.text', modelName); - cy.task('log', `āœ“ Model ${modelName} persisted after page reload!`); + testLog.info( `āœ“ Model ${modelName} persisted after page reload!`); }); // Optional: Open the dropdown again to verify the selection visually - cy.task('log', '=== Step 7: Double-checking selection in dropdown ==='); + testLog.info( '=== Step 7: Double-checking selection in dropdown ==='); ModelSelectorSelectors.button().click(); cy.wait(1000); @@ -190,16 +186,16 @@ describe('Chat Model Selection Persistence Tests', () => { cy.get('@selectedModel').then((modelName) => { ModelSelectorSelectors.optionByName(modelName as string) .should('have.class', 'bg-fill-content-select'); - cy.task('log', `āœ“ Model ${modelName} shows as selected in dropdown`); + testLog.info( `āœ“ Model ${modelName} shows as selected in dropdown`); }); // Close the dropdown cy.get('body').click(0, 0); // Final verification - cy.task('log', '=== Test completed successfully! ==='); - cy.task('log', 'āœ“āœ“āœ“ Model selection persisted after page reload'); + testLog.info( '=== Test completed successfully! ==='); + testLog.info( 'āœ“āœ“āœ“ Model selection persisted after page reload'); }); }); }); -}); \ 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..8d354146 100644 --- a/cypress/e2e/database/checkbox-column.cy.ts +++ b/cypress/e2e/database/checkbox-column.cy.ts @@ -1,16 +1,13 @@ -import { v4 as uuidv4 } from 'uuid'; import { AuthTestUtils } from '../../support/auth-utils'; import { AddPageSelectors, - DatabaseGridSelectors, CheckboxSelectors, - byTestId, + DatabaseGridSelectors, waitForReactUpdate } from '../../support/selectors'; +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,20 +57,21 @@ 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-"]'); - if (checkboxCells.length > 0) { - cy.log(`[STEP 10] Found ${checkboxCells.length} checkbox cells`); + CheckboxSelectors.allCheckboxCells().then($checkboxCells => { + if ($checkboxCells.length > 0) { + cy.log(`[STEP 10] Found ${$checkboxCells.length} checkbox cells`); - // Click first checkbox cell - CheckboxSelectors.allCheckboxCells().first().click(); - waitForReactUpdate(500); - cy.log('[STEP 11] Clicked checkbox cell'); - } else { - cy.log('[STEP 10] No checkbox cells found, cell interaction test completed'); - } + // Click first checkbox cell + CheckboxSelectors.allCheckboxCells().first().click(); + waitForReactUpdate(500); + cy.log('[STEP 11] Clicked checkbox cell'); + } else { + cy.log('[STEP 10] No checkbox cells found, cell interaction test completed'); + } + }); }); 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..b0ec0d8c 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, @@ -7,13 +5,12 @@ import { PropertyMenuSelectors, GridFieldSelectors, FieldType, - byTestId, 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') || @@ -67,28 +64,23 @@ describe('DateTime Column Type', () => { // The new column is created and the property menu should be open automatically // Let's wait for property trigger to be available cy.log('[STEP 8] Waiting for property menu to open'); - cy.get('body').then($body => { - // Check if property type trigger exists - if ($body.find(byTestId('property-type-trigger')).length > 0) { + PropertyMenuSelectors.propertyTypeTrigger().then($trigger => { + if ($trigger.length > 0) { cy.log('[STEP 9] Property type trigger found, changing to DateTime'); - PropertyMenuSelectors.propertyTypeTrigger().first().click({ force: true }); + cy.wrap($trigger.first()).click({ force: true }); waitForReactUpdate(1000); - // Select DateTime option cy.log('[STEP 10] Selecting DateTime option'); PropertyMenuSelectors.propertyTypeOption(FieldType.DateTime).click({ force: true }); waitForReactUpdate(2000); } else { cy.log('[STEP 9] Property type trigger not found, looking for field header'); - // Try clicking on the new field header first GridFieldSelectors.allFieldHeaders().last().scrollIntoView().click({ force: true }); waitForReactUpdate(1000); - // Now try to find the property type trigger PropertyMenuSelectors.propertyTypeTrigger().first().click({ force: true }); waitForReactUpdate(1000); - // Select DateTime option cy.log('[STEP 10] Selecting DateTime option'); PropertyMenuSelectors.propertyTypeOption(FieldType.DateTime).click({ force: true }); waitForReactUpdate(2000); @@ -102,21 +94,17 @@ 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-"]'); - if (datetimeCells.length > 0) { - cy.log(`[STEP 13] Found ${datetimeCells.length} datetime cells`); + DateTimeSelectors.allDateTimeCells().then($cells => { + if ($cells.length > 0) { + cy.log(`[STEP 13] Found ${$cells.length} datetime cells`); - // Try to interact with the first datetime cell DateTimeSelectors.allDateTimeCells().first().scrollIntoView().click({ force: true }); waitForReactUpdate(1000); - // Check if picker opens - cy.get('body').then($body => { - if ($body.find(byTestId('datetime-picker-popover')).length > 0) { + DateTimeSelectors.dateTimePickerPopover().then($popover => { + if ($popover.length > 0) { cy.log('[STEP 14] DateTime picker opened successfully'); - // Enter a date const today = new Date(); const dateStr = `${(today.getMonth() + 1).toString().padStart(2, '0')}/${today.getDate().toString().padStart(2, '0')}/${today.getFullYear()}`; @@ -138,4 +126,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..ea3b128c 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, @@ -8,12 +6,12 @@ import { SingleSelectSelectors, PageSelectors, FieldType, - byTestId, 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(() => { @@ -54,29 +52,24 @@ describe('Single Select Column Type', () => { cy.log('[STEP 4.1] Waiting for inline add button or new page button'); // Try to find either inline add button or new page button - cy.get('body').then($body => { - const inlineAddExists = $body.find(byTestId('inline-add-page')).length > 0; - const newPageExists = $body.find(byTestId('new-page-button')).length > 0; - - if (inlineAddExists) { - cy.log('[STEP 4.2] Using inline add button'); - return cy.wrap(null).then(() => { + AddPageSelectors.inlineAddButton().then($inlineAdd => { + const inlineAddExists = $inlineAdd.length > 0; + PageSelectors.newPageButton().then($newPage => { + const newPageExists = $newPage.length > 0; + + if (inlineAddExists) { + cy.log('[STEP 4.2] Using inline add button'); AddPageSelectors.inlineAddButton().first().click({ force: true }); - }); - } else if (newPageExists) { - cy.log('[STEP 4.2] Using new page button instead'); - return cy.wrap(null).then(() => { + } else if (newPageExists) { + cy.log('[STEP 4.2] Using new page button instead'); PageSelectors.newPageButton().first().click({ force: true }); - }); - } else { - // Wait a bit more and try inline add button - cy.log('[STEP 4.2] Waiting for UI to stabilize'); - return cy.wrap(null).then(() => { + } else { + cy.log('[STEP 4.2] Waiting for UI to stabilize'); cy.wait(3000); AddPageSelectors.inlineAddButton().should('exist', { timeout: 15000 }); AddPageSelectors.inlineAddButton().first().click({ force: true }); - }); - } + } + }); }); waitForReactUpdate(1000); @@ -166,14 +159,13 @@ describe('Single Select Column Type', () => { // Check if property menu is open and change to SingleSelect cy.log('[STEP 7] Changing column type to SingleSelect'); - cy.get('body').then($body => { - if ($body.find(byTestId('property-type-trigger')).length > 0) { - PropertyMenuSelectors.propertyTypeTrigger().first().click({ force: true }); + PropertyMenuSelectors.propertyTypeTrigger().then($trigger => { + if ($trigger.length > 0) { + cy.wrap($trigger.first()).click({ force: true }); waitForReactUpdate(1000); PropertyMenuSelectors.propertyTypeOption(FieldType.SingleSelect).click({ force: true }); waitForReactUpdate(2000); } else { - // Try clicking on the field header first GridFieldSelectors.allFieldHeaders().last().scrollIntoView().click({ force: true }); waitForReactUpdate(1000); PropertyMenuSelectors.propertyTypeTrigger().first().click({ force: true }); @@ -191,20 +183,16 @@ describe('Single Select Column Type', () => { cy.log('[STEP 8] Adding select options to cells'); // First try to find select cells - cy.get('body').then($body => { - const selectCells = $body.find('[data-testid^="select-option-cell-"]'); - - if (selectCells.length > 0) { - cy.log(`[STEP 9] Found ${selectCells.length} select cells`); + SingleSelectSelectors.allSelectOptionCells().then($cells => { + if ($cells.length > 0) { + cy.log(`[STEP 9] Found ${$cells.length} select cells`); - // Click first cell with force and add option SingleSelectSelectors.allSelectOptionCells().first().click({ force: true }); waitForReactUpdate(500); cy.focused().type('Option A{enter}'); waitForReactUpdate(1000); - // Add second option if possible - if (selectCells.length > 1) { + if ($cells.length > 1) { SingleSelectSelectors.allSelectOptionCells().eq(1).click({ force: true }); waitForReactUpdate(500); cy.focused().type('Option B{enter}'); @@ -213,18 +201,14 @@ describe('Single Select Column Type', () => { } else { cy.log('[STEP 9] No select cells found, using regular cells'); - // Get all rows and find cells in the newly added column DatabaseGridSelectors.rows().first().within(() => { - // Click the last cell in this row (should be the new column) DatabaseGridSelectors.cells().last().click({ force: true }); waitForReactUpdate(500); }); - // Type option A cy.focused().type('Option A{enter}'); waitForReactUpdate(1000); - // Try second row DatabaseGridSelectors.rows().eq(1).within(() => { DatabaseGridSelectors.cells().last().click({ force: true }); waitForReactUpdate(500); @@ -241,9 +225,9 @@ describe('Single Select Column Type', () => { waitForReactUpdate(1000); // Click edit property if available - cy.get('body').then($body => { - if ($body.find(byTestId('grid-field-edit-property')).length > 0) { - PropertyMenuSelectors.editPropertyMenuItem().click(); + PropertyMenuSelectors.editPropertyMenuItem().then($edit => { + if ($edit.length > 0) { + cy.wrap($edit).click(); waitForReactUpdate(1000); } }); @@ -285,9 +269,9 @@ describe('Single Select Column Type', () => { waitForReactUpdate(1000); // Click edit property if available - cy.get('body').then($body => { - if ($body.find(byTestId('grid-field-edit-property')).length > 0) { - PropertyMenuSelectors.editPropertyMenuItem().click(); + PropertyMenuSelectors.editPropertyMenuItem().then($edit => { + if ($edit.length > 0) { + cy.wrap($edit).click(); waitForReactUpdate(1000); } }); @@ -305,18 +289,15 @@ 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-"]'); - if (selectCells.length > 0) { - cy.log(`[STEP 17] Success! Found ${selectCells.length} select option cells after conversion`); + SingleSelectSelectors.allSelectOptionCells().then($cells => { + if ($cells.length > 0) { + cy.log(`[STEP 17] Success! Found ${$cells.length} select option cells after conversion`); - // Click on a cell to verify options are still available SingleSelectSelectors.allSelectOptionCells().first().click(); waitForReactUpdate(500); - // Check if select menu appears - cy.get('body').then($body => { - if ($body.find(byTestId('select-option-menu')).length > 0) { + SingleSelectSelectors.selectOptionMenu().then($menu => { + if ($menu.length > 0) { cy.log('[STEP 18] Select option menu opened - options preserved!'); } else { cy.log('[STEP 18] Select cells exist but menu behavior may differ'); 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..6b2429c1 100644 --- a/cypress/e2e/page/breadcrumb-navigation.cy.ts +++ b/cypress/e2e/page/breadcrumb-navigation.cy.ts @@ -1,25 +1,26 @@ -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 { + BreadcrumbSelectors, + PageSelectors, + SidebarSelectors, + SpaceSelectors +} from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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(() => { // Generate unique test data for each test testEmail = generateRandomEmail(); - + // Handle uncaught exceptions cy.on('uncaught:exception', (err: Error) => { if (err.message.includes('No workspace or service found')) { @@ -34,263 +35,848 @@ describe('Breadcrumb Navigation Complete Tests', () => { }); describe('Basic Navigation Tests', () => { - it('should navigate through space and check for breadcrumb availability', { timeout: 60000 }, () => { - // Login - cy.task('log', '=== Step 1: Login ==='); + it('should navigate through space and check for breadcrumb availability', () => { + // // Login + testLog.info('=== Step 1: Login ==='); cy.visit('/login', { failOnStatusCode: false }); - cy.wait(2000); + cy.get('body').should('be.visible'); const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - + // Wait for app to load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info('Waiting for app to fully load...'); cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); - cy.wait(2000); + // Wait for pages to be ready + PageSelectors.names().should('have.length.at.least', 1); - cy.task('log', 'App loaded successfully'); + testLog.info('App loaded successfully'); // Step 2: Expand first space - cy.task('log', '=== Step 2: Expanding first space ==='); + testLog.info('=== Step 2: Expanding first space ==='); TestTool.expandSpace(0); - cy.wait(2000); - cy.task('log', 'Expanded first space'); + // Wait for space to expand and pages to be visible + PageSelectors.names().should('be.visible', { timeout: 10000 }); + testLog.info('Expanded first space'); // Step 3: Navigate to first page - cy.task('log', '=== Step 3: Navigating to first page ==='); + testLog.info('=== Step 3: Navigating to first page ==='); PageSelectors.names().first().then($page => { const pageName = $page.text(); - cy.task('log', `Navigating to: ${pageName}`); + testLog.info(`Navigating to: ${pageName}`); cy.wrap($page).click(); }); - cy.wait(3000); + // Wait for page to load + cy.url().should('include', '/app/', { timeout: 10000 }); // 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) { - cy.task('log', 'āœ“ Breadcrumb navigation found on this page'); - - // Count breadcrumb items - cy.get('[data-testid*="breadcrumb-item-"]').then($items => { - cy.task('log', `āœ“ Found ${$items.length} breadcrumb items`); + testLog.info('=== Step 4: Checking for breadcrumb navigation ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + testLog.info('āœ“ Breadcrumb navigation found on this page'); + BreadcrumbSelectors.items().then($items => { + testLog.info(`āœ“ Found ${$items.length} breadcrumb items`); }); } else { - cy.task('log', 'No breadcrumb navigation on this page (normal for top-level pages)'); + testLog.info('No breadcrumb navigation on this page (normal for top-level pages)'); } }); // Verify no errors cy.get('body').then($body => { - const hasError = $body.text().includes('Error') || - $body.text().includes('Failed'); - + const hasError = $body.text().includes('Error') || + $body.text().includes('Failed'); + if (!hasError) { - cy.task('log', 'āœ“ Navigation completed without errors'); + testLog.info('āœ“ Navigation completed without errors'); } }); - cy.task('log', '=== Basic navigation test completed ==='); + testLog.info('=== Basic navigation test completed ==='); }); }); - it('should navigate to nested pages and use breadcrumb to go back', { timeout: 60000 }, () => { + it('should navigate to nested pages and use breadcrumb to go back', () => { // Login cy.visit('/login', { failOnStatusCode: false }); - cy.wait(2000); + cy.get('body').should('be.visible'); const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - + // Wait for app to load cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); - cy.wait(2000); + // Wait for pages to be ready + PageSelectors.names().should('have.length.at.least', 1); - cy.task('log', '=== Step 1: Expand first space ==='); + testLog.info('=== Step 1: Expand first space ==='); TestTool.expandSpace(0); - cy.wait(2000); + // Wait for space to expand and pages to be visible + PageSelectors.names().should('be.visible', { timeout: 10000 }); - cy.task('log', '=== Step 2: Navigate to first page ==='); + testLog.info('=== Step 2: Navigate to first page ==='); PageSelectors.names().first().click(); - cy.wait(3000); + // Wait for page to load + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for sidebar to update - cy.task('log', '=== Step 3: Check for nested pages ==='); + testLog.info('=== Step 3: Check for nested pages ==='); PageSelectors.names().then($pages => { - cy.task('log', `Found ${$pages.length} pages in sidebar`); - - if ($pages.length > 1) { - // Navigate to a nested page - cy.task('log', 'Navigating to nested page'); + testLog.info(`Found ${$pages.length} pages in sidebar`); + + // Find child pages by name + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let childPageFound = false; + + for (let i = 0; i < $pages.length; i++) { + const pageName = Cypress.$($pages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + testLog.info(`Found child page: ${pageName}`); + cy.wrap($pages[i]).click({ force: true }); + childPageFound = true; + break; + } + } + + if (!childPageFound && $pages.length > 1) { + // Fallback: navigate to second page + testLog.info('No known child page found, clicking second page as fallback'); cy.wrap($pages[1]).click({ force: true }); - cy.wait(3000); - - // 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) { - 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 }); - cy.task('log', 'āœ“ Clicked breadcrumb item to navigate back'); - cy.wait(2000); - cy.task('log', 'āœ“ Successfully used breadcrumb navigation'); + } + + // Wait for page to load + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); + + // Check for breadcrumb navigation + testLog.info('=== Step 4: Testing breadcrumb navigation ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + testLog.info('āœ“ Breadcrumb navigation is visible'); + BreadcrumbSelectors.items().should('have.length.at.least', 1); + BreadcrumbSelectors.items().then($items => { + if ($items.length > 1) { + cy.wrap($items).first().click({ force: true }); + testLog.info('āœ“ Clicked breadcrumb item to navigate back'); + // Wait for navigation to complete + cy.url().should('include', '/app/', { timeout: 10000 }); + testLog.info('āœ“ Successfully used breadcrumb navigation'); } else { - cy.task('log', 'Only one breadcrumb item found'); + testLog.info('Only one breadcrumb item found'); } - } else { - cy.task('log', 'No breadcrumb navigation on nested page'); - } - }); - } else { - cy.task('log', 'No nested pages available for breadcrumb testing'); - } + }); + } else { + testLog.info('No breadcrumb navigation on nested page'); + } + }); }); - cy.task('log', '=== Nested navigation test completed ==='); + testLog.info('=== Nested navigation test completed ==='); }); }); }); describe('Full Breadcrumb Flow Test', () => { - it('should navigate through General > Get Started > Desktop Guide flow (if available)', { timeout: 60000 }, () => { + it('should navigate through General > Get Started > Desktop Guide flow (if available)', () => { // Login cy.visit('/login', { failOnStatusCode: false }); - cy.wait(2000); + cy.get('body').should('be.visible'); const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - + // Wait for app to load cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); - cy.wait(2000); + // Wait for pages to be ready + PageSelectors.names().should('have.length.at.least', 1); // Step 1: Find and expand General space or first space - cy.task('log', '=== Step 1: Looking for General space ==='); + testLog.info('=== Step 1: Looking for General space ==='); SpaceSelectors.names().then($spaces => { const spaceNames = Array.from($spaces).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Available spaces: ${spaceNames.join(', ')}`); - + testLog.info(`Available spaces: ${spaceNames.join(', ')}`); + // Find General space or use first - const generalIndex = spaceNames.findIndex(name => + const generalIndex = spaceNames.findIndex(name => name?.toLowerCase().includes('general') ); - + if (generalIndex !== -1) { - cy.task('log', `Found General space at index ${generalIndex}`); + testLog.info(`Found General space at index ${generalIndex}`); TestTool.expandSpace(generalIndex); } else { - cy.task('log', 'Using first available space'); + testLog.info('Using first available space'); TestTool.expandSpace(0); } }); - cy.wait(2000); + // Wait for space to expand and pages to be visible + PageSelectors.names().should('be.visible', { timeout: 10000 }); // Step 2: Look for Get Started page or use first page - cy.task('log', '=== Step 2: Looking for Get Started page ==='); + testLog.info('=== Step 2: Looking for Get Started page ==='); PageSelectors.names().then($pages => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Available pages: ${pageNames.join(', ')}`); - + testLog.info(`Available pages: ${pageNames.join(', ')}`); + // Find Get Started or similar page const getStartedPage = Array.from($pages).find((el: Element) => { const text = el.textContent?.trim().toLowerCase(); - return text?.includes('get') || text?.includes('start') || - text?.includes('welcome') || text?.includes('guide'); + return text?.includes('get') || text?.includes('start') || + text?.includes('welcome') || text?.includes('guide'); }); - + if (getStartedPage) { cy.wrap(getStartedPage).click(); - cy.task('log', `Clicked on: ${getStartedPage.textContent?.trim()}`); + testLog.info(`Clicked on: ${getStartedPage.textContent?.trim()}`); } else { PageSelectors.names().first().click(); - cy.task('log', 'Clicked first available page'); + testLog.info('Clicked first available page'); } }); - cy.wait(3000); + // Wait for page to load + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for sidebar to update // Step 3: Look for Desktop Guide or sub-page - cy.task('log', '=== Step 3: Looking for Desktop Guide or sub-pages ==='); + testLog.info('=== Step 3: Looking for Desktop Guide or sub-pages ==='); PageSelectors.names().then($subPages => { - if ($subPages.length > 1) { - const subPageNames = Array.from($subPages).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Found sub-pages: ${subPageNames.join(', ')}`); - - // Look for Desktop Guide or any guide - limit search to avoid hanging - const maxIndex = Math.min($subPages.length, 5); - let guidePage = null; - - for (let i = 0; i < maxIndex; i++) { - const el = $subPages[i]; - const text = el.textContent?.trim().toLowerCase(); - if (text?.includes('desktop') || text?.includes('guide') || text?.includes('tutorial')) { - guidePage = el; + const subPageNames = Array.from($subPages).map((el: Element) => el.textContent?.trim()); + testLog.info(`Found pages: ${subPageNames.join(', ')}`); + + // Look for Desktop Guide or any guide + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let guidePage = null; + + for (let i = 0; i < $subPages.length; i++) { + const el = $subPages[i]; + const text = el.textContent?.trim().toLowerCase(); + if (text?.includes('desktop') || childPageNames.some(name => text.includes(name.toLowerCase()))) { + guidePage = el; + break; + } + } + + if (guidePage) { + cy.wrap(guidePage).click({ force: true }); + testLog.info(`Navigated to: ${guidePage.textContent?.trim()}`); + } else if ($subPages.length > 1) { + // Try to find a child page by name + let childFound = false; + for (let i = 0; i < $subPages.length; i++) { + const pageName = Cypress.$($subPages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + cy.wrap($subPages[i]).click({ force: true }); + testLog.info(`Navigated to: ${pageName}`); + childFound = true; break; } } - - if (guidePage) { - cy.wrap(guidePage).click({ force: true }); - cy.task('log', `Navigated to: ${guidePage.textContent?.trim()}`); - } else if ($subPages.length > 1) { + if (!childFound) { cy.wrap($subPages[1]).click({ force: true }); - cy.task('log', 'Navigated to second page'); + testLog.info('Navigated to second page'); } - cy.wait(3000); - - // 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) { - cy.task('log', 'āœ“ Breadcrumb navigation is visible'); - - // Check breadcrumb items with timeout - cy.get('[data-testid*="breadcrumb-item-"]', { timeout: 10000 }).then($items => { - cy.task('log', `Found ${$items.length} breadcrumb items`); - - if ($items.length > 1) { - // Click second-to-last breadcrumb (parent page) - const targetIndex = Math.max(0, $items.length - 2); - cy.wrap($items[targetIndex]).click({ force: true }); - cy.task('log', `āœ“ Clicked breadcrumb at index ${targetIndex} to go back`); - cy.wait(2000); - - // Verify navigation worked - cy.task('log', 'āœ“ Successfully navigated back using breadcrumb'); - } + } + // Wait for page to load + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); + + // Step 4: Test breadcrumb navigation + testLog.info('=== Step 4: Testing breadcrumb navigation ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + testLog.info('āœ“ Breadcrumb navigation is visible'); + BreadcrumbSelectors.items().should('have.length.at.least', 1); + BreadcrumbSelectors.items().then($items => { + testLog.info(`Found ${$items.length} breadcrumb items`); + if ($items.length > 1) { + const targetIndex = Math.max(0, $items.length - 2); + cy.wrap($items[targetIndex]).click({ force: true }); + testLog.info(`āœ“ Clicked breadcrumb at index ${targetIndex} to go back`); + // Wait for navigation to complete + cy.url().should('include', '/app/', { timeout: 10000 }); + testLog.info('āœ“ Successfully navigated back using breadcrumb'); + } + }); + } else { + testLog.info('Breadcrumb navigation not available on this page'); + } + }); + }); + + // Final verification + cy.get('body').then($body => { + const hasError = $body.text().includes('Error') || + $body.text().includes('Failed') || + $body.find('[role="alert"]').length > 0; + + if (!hasError) { + testLog.info('āœ“ Test completed without errors'); + } + }); + + testLog.info('=== Full breadcrumb flow test completed ==='); + }); + }); + }); + + describe('Breadcrumb Item Verification Tests', () => { + it('should verify breadcrumb items display correct names and are clickable', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate to nested page ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + // Navigate to first page + PageSelectors.names().first().click(); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for sidebar to update + + // Navigate to nested page if available + PageSelectors.names().then($pages => { + // Find child pages by name + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let childPageFound = false; + + for (let i = 0; i < $pages.length; i++) { + const pageName = Cypress.$($pages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + testLog.info(`Found child page: ${pageName}`); + cy.wrap($pages[i]).click({ force: true }); + childPageFound = true; + break; + } + } + + if (!childPageFound && $pages.length > 1) { + cy.wrap($pages[1]).click({ force: true }); + } + + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); + + testLog.info('=== Step 2: Verify breadcrumb items ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + BreadcrumbSelectors.items().then($items => { + testLog.info(`Found ${$items.length} breadcrumb items`); + + // Verify each breadcrumb item has text + $items.each((index, item) => { + const $item = Cypress.$(item); + const itemText = $item.text().trim(); + testLog.info(`Breadcrumb ${index}: "${itemText}"`); + expect(itemText).to.not.be.empty; }); - } else { - cy.task('log', 'Breadcrumb navigation not available on this page'); + + // Verify last item is not clickable (should be disabled) + if ($items.length > 0) { + const lastItem = $items.last(); + cy.wrap(lastItem).should('exist'); + testLog.info('āœ“ Verified breadcrumb items structure'); + } + }); + } else { + testLog.info('No breadcrumb navigation available'); + } + }); + }); + }); + }); + + it('should verify breadcrumb navigation updates correctly when navigating', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate to parent page ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + let parentPageName: string; + PageSelectors.names().first().then($page => { + parentPageName = $page.text().trim(); + testLog.info(`Parent page: ${parentPageName}`); + cy.wrap($page).click(); + }); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for sidebar to update + + testLog.info('=== Step 2: Navigate to nested page ==='); + PageSelectors.names().then($pages => { + // Find child pages by name + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let childPageFound = false; + + for (let i = 0; i < $pages.length; i++) { + const pageName = Cypress.$($pages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + testLog.info(`Found child page: ${pageName}`); + cy.wrap($pages[i]).click({ force: true }); + childPageFound = true; + break; + } + } + + if (!childPageFound && $pages.length > 1) { + cy.wrap($pages[1]).click({ force: true }); + } + + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); + + testLog.info('=== Step 3: Verify breadcrumb shows parent ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + BreadcrumbSelectors.items().then($items => { + if ($items.length > 1) { + // Verify parent page appears in breadcrumb + const breadcrumbTexts = Array.from($items).map(el => Cypress.$(el).text().trim()); + const hasParent = breadcrumbTexts.some(text => text.includes(parentPageName)); + if (hasParent) { + testLog.info('āœ“ Parent page found in breadcrumb'); + } + } + }); + } + }); + + testLog.info('=== Step 4: Navigate back via breadcrumb ==='); + BreadcrumbSelectors.items().then($items => { + if ($items.length > 1) { + // Click first breadcrumb (parent) + cy.wrap($items).first().click({ force: true }); + cy.url().should('include', '/app/', { timeout: 10000 }); + testLog.info('āœ“ Successfully navigated back via breadcrumb'); + } + }); + }); + }); + }); + }); + + describe('Deep Navigation Tests', () => { + it('should handle breadcrumb navigation for 3+ level deep pages', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate to first level ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + // Get initial page count + let initialPageCount = 0; + PageSelectors.names().then($pages => { + initialPageCount = $pages.length; + testLog.info(`Initial pages in sidebar: ${initialPageCount}`); + }); + + // Click first page and wait for navigation + PageSelectors.names().first().then($firstPage => { + const firstPageName = $firstPage.text().trim(); + testLog.info(`Clicking on first page: ${firstPageName}`); + cy.wrap($firstPage).click(); + }); + cy.url().should('include', '/app/', { timeout: 10000 }); + + // Wait for page to load and sidebar to potentially update + cy.wait(2000); + testLog.info('Waiting for sidebar to update with nested pages...'); + + testLog.info('=== Step 2: Navigate to second level ==='); + // Look for nested pages - they should be children of the first page + // In the sidebar, nested pages appear after their parent when expanded + PageSelectors.names().then($pages => { + testLog.info(`Pages in sidebar after navigation: ${$pages.length}`); + const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); + testLog.info(`Available pages: ${pageNames.join(', ')}`); + + // Find "Desktop guide" or another child page (usually appears after "Getting started") + // Children of "Getting started" are: Desktop guide, Mobile guide, Web guide + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let childPageFound = false; + + for (let i = 0; i < $pages.length; i++) { + const pageName = Cypress.$($pages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + testLog.info(`Found child page: ${pageName} at index ${i}`); + cy.wrap($pages[i]).click({ force: true }); + childPageFound = true; + break; + } + } + + if (!childPageFound && $pages.length > 1) { + // Fallback: click second page if no known child found + testLog.info('No known child page found, clicking second page as fallback'); + cy.wrap($pages[1]).click({ force: true }); + } + + cy.url().should('include', '/app/', { timeout: 10000 }); + + // Wait for sidebar to update again + cy.wait(2000); + testLog.info('Waiting for sidebar to update with third level pages...'); + + testLog.info('=== Step 3: Navigate to third level if available ==='); + // Wait for third level pages to appear + PageSelectors.names().should('exist', { timeout: 10000 }); + + PageSelectors.names().then($subPages => { + testLog.info(`Pages in sidebar after second navigation: ${$subPages.length}`); + const subPageNames = Array.from($subPages).map((el: Element) => el.textContent?.trim()); + testLog.info(`Available sub-pages: ${subPageNames.join(', ')}`); + + // Try to find another nested page or click a different page + if ($subPages.length > 2) { + // Click a page that's different from what we've already clicked + // Skip first two (Getting started and Desktop guide) and try third + cy.wrap($subPages[2]).click({ force: true }); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for page to fully load + + testLog.info('=== Step 4: Verify breadcrumb shows all levels ==='); + // Wait a bit more for breadcrumb to render + cy.wait(1000); + + BreadcrumbSelectors.navigation().should('exist', { timeout: 10000 }).then($nav => { + if ($nav.length > 0) { + BreadcrumbSelectors.items().should('have.length.at.least', 1); + BreadcrumbSelectors.items().then($items => { + testLog.info(`Found ${$items.length} breadcrumb items (3+ levels deep)`); + + // Log breadcrumb item texts for debugging + const breadcrumbTexts = Array.from($items).map(el => Cypress.$(el).text().trim()); + testLog.info(`Breadcrumb items: ${breadcrumbTexts.join(' > ')}`); + + expect($items.length).to.be.at.least(2); + + // Verify we can navigate back through breadcrumbs + if ($items.length > 2) { + // Click second-to-last breadcrumb + const targetIndex = $items.length - 2; + testLog.info(`Clicking breadcrumb at index ${targetIndex} (${breadcrumbTexts[targetIndex]})`); + cy.wrap($items[targetIndex]).click({ force: true }); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(1000); + testLog.info('āœ“ Successfully navigated back from deep level'); + } else { + testLog.info(`Only ${$items.length} breadcrumb items found, expected at least 3 for deep navigation`); + } + }); + } else { + testLog.info('Breadcrumb not available for deep navigation'); + } + }); + } else { + testLog.info(`No third level pages available (found ${$subPages.length} pages)`); + } + }); + }); + }); + }); + }); + + describe('Breadcrumb After Page Creation Tests', () => { + it('should show breadcrumb after creating a new nested page', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate to a page ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + PageSelectors.names().first().click(); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for page to load + + testLog.info('=== Step 2: Create a new nested page ==='); + const newPageName = `Test Page ${Date.now()}`; + + // Create page using the new page button + PageSelectors.newPageButton().should('be.visible').click(); + cy.wait(1000); + + // Close any modals that might appear + cy.get('body').then($body => { + if ($body.find('[role="dialog"]').length > 0) { + cy.get('body').type('{esc}'); + cy.wait(500); + } + }); + + // Wait for page to be created and navigate to it + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for page to fully load + + // Set page title if title input is available (use force to bypass modal backdrop) + PageSelectors.titleInput().then($titleInput => { + if ($titleInput.length > 0) { + cy.wrap($titleInput).first().click({ force: true }); + cy.wait(500); + cy.wrap($titleInput).first().type('{selectall}', { force: true }); + cy.wrap($titleInput).first().type(newPageName, { force: true }); + cy.wrap($titleInput).first().type('{enter}', { force: true }); + cy.wait(1000); + } + }); + + testLog.info('=== Step 3: Verify breadcrumb appears for new page ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + testLog.info('āœ“ Breadcrumb navigation found after page creation'); + BreadcrumbSelectors.items().should('have.length.at.least', 1); + BreadcrumbSelectors.items().then($items => { + testLog.info(`Found ${$items.length} breadcrumb items for new page`); + + // Verify we can navigate back + if ($items.length > 1) { + cy.wrap($items).first().click({ force: true }); + cy.url().should('include', '/app/', { timeout: 10000 }); + testLog.info('āœ“ Successfully navigated back from newly created page'); } }); } else { - cy.task('log', 'No sub-pages found for breadcrumb testing'); + testLog.info('No breadcrumb navigation (page may be top-level)'); } }); + }); + }); + }); - // Final verification - cy.get('body').then($body => { - const hasError = $body.text().includes('Error') || - $body.text().includes('Failed') || - $body.find('[role="alert"]').length > 0; - - if (!hasError) { - cy.task('log', 'āœ“ Test completed without errors'); + describe('Breadcrumb Text Content Tests', () => { + it('should verify breadcrumb items contain correct page names', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate through pages and collect names ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + const pageNames: string[] = []; + PageSelectors.names().then($pages => { + // Collect first 3 page names + const maxPages = Math.min($pages.length, 3); + for (let i = 0; i < maxPages; i++) { + const pageName = Cypress.$($pages[i]).text().trim(); + pageNames.push(pageName); + } + testLog.info(`Collected page names: ${pageNames.join(', ')}`); + + // Navigate to first page + if (pageNames.length > 0) { + cy.wrap($pages[0]).click(); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for sidebar to update + + // Find and navigate to nested page + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let childFound = false; + + PageSelectors.names().then($subPages => { + for (let i = 0; i < $subPages.length; i++) { + const pageName = Cypress.$($subPages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + cy.wrap($subPages[i]).click({ force: true }); + childFound = true; + break; + } + } + + if (!childFound && $subPages.length > 1) { + cy.wrap($subPages[1]).click({ force: true }); + } + + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); + + testLog.info('=== Step 2: Verify breadcrumb contains page names ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + BreadcrumbSelectors.items().then($items => { + const breadcrumbTexts = Array.from($items).map(el => + Cypress.$(el).text().trim() + ); + testLog.info(`Breadcrumb texts: ${breadcrumbTexts.join(' > ')}`); + + // Verify parent page name appears in breadcrumb + if (pageNames.length > 0 && breadcrumbTexts.length > 0) { + const hasParentName = breadcrumbTexts.some(text => + text.includes(pageNames[0]) + ); + if (hasParentName) { + testLog.info('āœ“ Parent page name found in breadcrumb'); + } + } + }); + } + }); + }); + } + }); + }); + }); + }); + + describe('Breadcrumb Edge Cases', () => { + it('should handle breadcrumb when navigating between different spaces', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate to first space ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + PageSelectors.names().first().click(); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for sidebar to update + + testLog.info('=== Step 2: Check breadcrumb state ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length > 0) { + BreadcrumbSelectors.items().then($items => { + testLog.info(`Breadcrumb items before navigation: ${$items.length}`); + + // Navigate to nested page + PageSelectors.names().then($pages => { + // Find child pages by name + const childPageNames = ['Desktop guide', 'Mobile guide', 'Web guide']; + let childFound = false; + + for (let i = 0; i < $pages.length; i++) { + const pageName = Cypress.$($pages[i]).text().trim(); + if (childPageNames.includes(pageName)) { + cy.wrap($pages[i]).click({ force: true }); + childFound = true; + break; + } + } + + if (!childFound && $pages.length > 1) { + cy.wrap($pages[1]).click({ force: true }); + } + + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); + + // Verify breadcrumb updates + BreadcrumbSelectors.items().then($newItems => { + testLog.info(`Breadcrumb items after navigation: ${$newItems.length}`); + if ($newItems.length > $items.length) { + testLog.info('āœ“ Breadcrumb updated correctly after navigation'); + } + }); + }); + }); } }); + }); + }); + + it('should verify breadcrumb does not appear on top-level pages', () => { + cy.visit('/login', { failOnStatusCode: false }); + cy.get('body').should('be.visible'); + + const authUtils = new AuthTestUtils(); + authUtils.signInWithTestUrl(testEmail).then(() => { + cy.url().should('include', '/app'); + + // Wait for app to load + cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); + SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); + PageSelectors.names().should('exist', { timeout: 30000 }); + PageSelectors.names().should('have.length.at.least', 1); + + testLog.info('=== Step 1: Navigate to top-level page ==='); + TestTool.expandSpace(0); + PageSelectors.names().should('be.visible', { timeout: 10000 }); + + // Click first page (likely top-level) + PageSelectors.names().first().click(); + cy.url().should('include', '/app/', { timeout: 10000 }); + cy.wait(2000); // Wait for page to load - cy.task('log', '=== Full breadcrumb flow test completed ==='); + testLog.info('=== Step 2: Verify breadcrumb behavior on top-level page ==='); + BreadcrumbSelectors.navigation().then($nav => { + if ($nav.length === 0) { + testLog.info('āœ“ No breadcrumb on top-level page (expected behavior)'); + } else { + BreadcrumbSelectors.items().then($items => { + testLog.info(`Found ${$items.length} breadcrumb items on top-level page`); + // Top-level pages may or may not have breadcrumbs depending on structure + }); + } + }); }); }); }); -}); \ 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..ec93aba8 100644 --- a/cypress/e2e/page/create-delete-page.cy.ts +++ b/cypress/e2e/page/create-delete-page.cy.ts @@ -1,20 +1,16 @@ -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'; +import { testLog } from '../../support/test-helpers'; 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(() => { @@ -42,7 +38,7 @@ describe('Page Create and Delete Tests', () => { cy.url().should('include', '/app'); // Wait for the app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); // Wait for the loading screen to disappear and main app to appear cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); @@ -57,15 +53,15 @@ describe('Page Create and Delete Tests', () => { cy.wait(2000); // Now wait for the new page button to be available - cy.task('log', 'Looking for new page button...'); + testLog.info( 'Looking for new page button...'); PageSelectors.newPageButton() .should('exist', { timeout: 20000 }) .then(() => { - cy.task('log', 'New page button found!'); + testLog.info( 'New page button found!'); }); // Step 2: Since user already has a workspace, just create a new page - cy.task('log', `Creating page with title: ${testPageName}`); + testLog.info( `Creating page with title: ${testPageName}`); // Click new page button PageSelectors.newPageButton().click(); @@ -87,7 +83,7 @@ describe('Page Create and Delete Tests', () => { cy.get('body').then(($body: JQuery) => { // Check if there's a modal dialog open if ($body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0) { - cy.task('log', 'Closing modal dialog'); + testLog.info( 'Closing modal dialog'); // Click the close button or press ESC cy.get('body').type('{esc}'); cy.wait(1000); @@ -110,7 +106,7 @@ describe('Page Create and Delete Tests', () => { .clear({ force: true }) .type(testPageName, { force: true }) .type('{enter}'); // Press enter to save the title - cy.task('log', `Set page title to: ${testPageName}`); + testLog.info( `Set page title to: ${testPageName}`); } }); @@ -133,18 +129,18 @@ describe('Page Create and Delete Tests', () => { PageSelectors.names().then($pages => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); initialPageCount = pageNames.length; - cy.task('log', `Found pages after creating new page: ${pageNames.join(', ')}`); + testLog.info( `Found pages after creating new page: ${pageNames.join(', ')}`); // The created page should have our test name if (pageNames.includes(testPageName)) { createdPageName = testPageName; - cy.task('log', `Found the created page with correct name: ${testPageName}`); + testLog.info( `Found the created page with correct name: ${testPageName}`); } else { // If title didn't save properly, find the newest "Untitled" page const untitledPages = pageNames.filter(name => name === 'Untitled'); if (untitledPages.length > 0) { createdPageName = 'Untitled'; - cy.task('log', `Warning: Page title didn't save. Page exists as "Untitled"`); + testLog.info( `Warning: Page title didn't save. Page exists as "Untitled"`); } else { throw new Error(`Could not find created page. Expected "${testPageName}", found: ${pageNames.join(', ')}`); } @@ -155,9 +151,9 @@ describe('Page Create and Delete Tests', () => { cy.then(() => { // Use the stored createdPageName from step 3 if (createdPageName) { - cy.task('log', `Attempting to delete the created page: ${createdPageName}`); + testLog.info( `Attempting to delete the created page: ${createdPageName}`); TestTool.deletePageByName(createdPageName); - cy.task('log', `Deleted page: ${createdPageName}`); + testLog.info( `Deleted page: ${createdPageName}`); } else { throw new Error('No page was created to delete'); } @@ -175,7 +171,7 @@ describe('Page Create and Delete Tests', () => { cy.then(() => { PageSelectors.names().then($pages => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Pages after delete and reload: ${pageNames.join(', ')}`); + testLog.info( `Pages after delete and reload: ${pageNames.join(', ')}`); // Check that the created page (whatever its final name was) no longer exists const pageStillExists = pageNames.some(name => @@ -183,8 +179,8 @@ describe('Page Create and Delete Tests', () => { ); if (!pageStillExists) { - cy.task('log', `āœ“ Verified test page "${createdPageName}" is gone after reload`); - cy.task('log', `Remaining pages: ${pageNames.join(', ')}`); + testLog.info( `āœ“ Verified test page "${createdPageName}" is gone after reload`); + testLog.info( `Remaining pages: ${pageNames.join(', ')}`); } else { throw new Error(`Test page "${createdPageName}" still exists after delete. Found pages: ${pageNames.join(', ')}`); } @@ -193,4 +189,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..95f214d9 100644 --- a/cypress/e2e/page/delete-page-verify-trash.cy.ts +++ b/cypress/e2e/page/delete-page-verify-trash.cy.ts @@ -1,20 +1,16 @@ -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 { ModalSelectors, PageSelectors, SidebarSelectors, TrashSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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(() => { @@ -34,44 +30,44 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { }); // Step 1: Login - cy.task('log', '=== Step 1: Login ==='); + testLog.info('=== Step 1: Login ==='); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - + // Wait for the app to fully load - cy.task('log', 'Waiting for app to fully load...'); - + testLog.info('Waiting for app to fully load...'); + // Wait for the loading screen to disappear and main app to appear cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); - + // Wait for the sidebar to be visible (indicates app is loaded) SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); - + // Wait for at least one page to exist in the sidebar PageSelectors.names().should('exist', { timeout: 30000 }); - + // Additional wait for stability cy.wait(2000); - + // Now wait for the new page button to be available - cy.task('log', 'Looking for new page button...'); + testLog.info('Looking for new page button...'); PageSelectors.newPageButton() .should('exist', { timeout: 20000 }) .then(() => { - cy.task('log', 'New page button found!'); + testLog.info('New page button found!'); }); // Step 2: Create a new page - cy.task('log', `=== Step 2: Creating page with title: ${testPageName} ===`); - + testLog.info(`=== Step 2: Creating page with title: ${testPageName} ===`); + // Click new page button PageSelectors.newPageButton().click(); waitForReactUpdate(1000); - + // Handle the new page modal ModalSelectors.newPageModal().should('be.visible').within(() => { // Select the first available space @@ -80,23 +76,23 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { // Click Add button cy.contains('button', 'Add').click(); }); - + // Wait for navigation to the new page cy.wait(3000); - + // Close any share/modal dialogs that might be open cy.get('body').then(($body: JQuery) => { if ($body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0) { - cy.task('log', 'Closing modal dialog'); + testLog.info('Closing modal dialog'); cy.get('body').type('{esc}'); cy.wait(1000); } }); - + // Set the page title PageSelectors.titleInput().should('exist'); cy.wait(1000); - + PageSelectors.titleInput() .first() .should('be.visible') @@ -107,16 +103,16 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { .clear({ force: true }) .type(testPageName, { force: true }) .type('{enter}'); - cy.task('log', `Set page title to: ${testPageName}`); + testLog.info(`Set page title to: ${testPageName}`); } }); - + // Wait for the title to be saved cy.wait(2000); // Step 3: Verify the page exists in sidebar - cy.task('log', '=== Step 3: Verifying page exists in sidebar ==='); - + testLog.info('=== Step 3: Verifying page exists in sidebar ==='); + // Expand the first space to see its pages TestTool.expandSpace(); cy.wait(1000); @@ -124,27 +120,27 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { // Verify the page exists PageSelectors.names().then($pages => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Found pages: ${pageNames.join(', ')}`); - + testLog.info(`Found pages: ${pageNames.join(', ')}`); + // Check if our page exists - const pageExists = pageNames.some(name => + const pageExists = pageNames.some(name => name === testPageName || name === 'Untitled' ); - + if (pageExists) { - cy.task('log', `āœ“ Page created successfully`); + testLog.info(`āœ“ Page created successfully`); } else { throw new Error(`Could not find created page. Expected "${testPageName}", found: ${pageNames.join(', ')}`); } }); // Step 4: Delete the page - cy.task('log', `=== Step 4: Deleting page: ${testPageName} ===`); - + testLog.info(`=== Step 4: Deleting page: ${testPageName} ===`); + // Find the page we want to delete PageSelectors.names().then($pages => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); - + // Determine the actual name of the page to delete let pageToDelete = testPageName; if (!pageNames.includes(testPageName)) { @@ -152,164 +148,168 @@ describe('Delete Page, Verify in Trash, and Restore Tests', () => { const untitledPages = pageNames.filter(name => name === 'Untitled'); if (untitledPages.length > 0) { pageToDelete = 'Untitled'; - cy.task('log', `Warning: Page title didn't save. Deleting "Untitled" page instead`); + testLog.info(`Warning: Page title didn't save. Deleting "Untitled" page instead`); } } - + // Delete the page TestTool.deletePageByName(pageToDelete); - cy.task('log', `āœ“ Deleted page: ${pageToDelete}`); + testLog.info(`āœ“ Deleted page: ${pageToDelete}`); }); // Wait for deletion to complete cy.wait(2000); // Step 5: Navigate to trash page - cy.task('log', '=== Step 5: Navigating to trash page ==='); - + testLog.info('=== Step 5: Navigating to trash page ==='); + // Click on the trash button in the sidebar - cy.get('[data-testid="sidebar-trash-button"]').click(); - + TrashSelectors.sidebarTrashButton().click(); + // Wait for navigation cy.wait(2000); - + // Verify we're on the trash page cy.url().should('include', '/app/trash'); - cy.task('log', 'āœ“ Successfully navigated to trash page'); + testLog.info('āœ“ Successfully navigated to trash page'); // Step 6: Verify the deleted page exists in trash - cy.task('log', '=== Step 6: Verifying deleted page exists in trash ==='); - + testLog.info('=== 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'); - + TrashSelectors.table().should('be.visible'); + // Look for our deleted page in the trash table - cy.get('[data-testid="trash-table-row"]').then($rows => { + TrashSelectors.rows().then($rows => { let foundPage = false; - + // Check each row for our page name $rows.each((index, row) => { const rowText = Cypress.$(row).text(); - cy.task('log', `Trash row ${index + 1}: ${rowText}`); - + testLog.info(`Trash row ${index + 1}: ${rowText}`); + // Check if this row contains our page (might be named as testPageName or "Untitled") if (rowText.includes(testPageName) || rowText.includes('Untitled')) { foundPage = true; - cy.task('log', `āœ“ Found deleted page in trash: ${rowText}`); + testLog.info(`āœ“ Found deleted page in trash: ${rowText}`); } }); - + // Verify we found the page if (foundPage) { - cy.task('log', 'āœ“āœ“āœ“ Test Passed: Deleted page was found in trash'); + testLog.info('āœ“āœ“āœ“ Test Passed: Deleted page was found in trash'); } else { throw new Error(`Deleted page not found in trash. Expected to find "${testPageName}" or "Untitled"`); } }); // 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(() => { + testLog.info('=== Step 7: Verifying trash actions are available ==='); + + TrashSelectors.rows().first().within(() => { // Check for restore button - cy.get('[data-testid="trash-restore-button"]').should('exist'); - cy.task('log', 'āœ“ Restore button found'); - + TrashSelectors.restoreButton().should('exist'); + testLog.info('āœ“ Restore button found'); + // Check for permanent delete button - cy.get('[data-testid="trash-delete-button"]').should('exist'); - cy.task('log', 'āœ“ Permanent delete button found'); + TrashSelectors.deleteButton().should('exist'); + testLog.info('āœ“ Permanent delete button found'); }); // Step 8: Restore the deleted page - cy.task('log', '=== Step 8: Restoring the deleted page ==='); - + testLog.info('=== Step 8: Restoring the deleted page ==='); + // Store the actual page name we'll be restoring 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(() => { + TrashSelectors.rows().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}`); + testLog.info(`Restoring page: ${restoredPageName}`); }); - + // Click restore button - cy.get('[data-testid="trash-restore-button"]').click(); + TrashSelectors.restoreButton().click(); }); - + // Wait for restore to complete cy.wait(2000); - cy.task('log', 'āœ“ Restore button clicked'); + testLog.info('āœ“ Restore button clicked'); // Step 9: Verify the page is removed from trash - cy.task('log', '=== Step 9: Verifying page is removed from trash ==='); - + testLog.info('=== Step 9: Verifying page is removed from trash ==='); + + // Wait a bit for the UI to update after restore + cy.wait(2000); + // 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 + // Use a more defensive approach - check if rows exist first + cy.get('body').then($body => { + // Check if trash table rows exist const rowsExist = $body.find('[data-testid="trash-table-row"]').length > 0; - + if (!rowsExist) { - cy.task('log', 'āœ“ Trash is now empty - page successfully removed from trash'); + testLog.info('āœ“ 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 => { + // Rows still exist, check if our page is among them + TrashSelectors.rows().then($rows => { let pageStillInTrash = false; - + $rows.each((index, row) => { const rowText = Cypress.$(row).text(); if (rowText.includes(restoredPageName)) { pageStillInTrash = true; } }); - + if (pageStillInTrash) { throw new Error(`Page "${restoredPageName}" is still in trash after restore`); } else { - cy.task('log', `āœ“ Page "${restoredPageName}" successfully removed from trash`); + testLog.info(`āœ“ Page "${restoredPageName}" successfully removed from trash`); } }); } }); // Step 10: Navigate back to the main workspace - cy.task('log', '=== Step 10: Navigating back to workspace ==='); - + testLog.info('=== Step 10: Navigating back to workspace ==='); + // Click on the workspace/home to go back cy.visit(`/app`); cy.wait(3000); - + // Wait for the sidebar to load SidebarSelectors.pageHeader().should('be.visible', { timeout: 10000 }); - cy.task('log', 'āœ“ Navigated back to workspace'); + testLog.info('āœ“ Navigated back to workspace'); // Step 11: Verify the restored page exists in sidebar - cy.task('log', '=== Step 11: Verifying restored page exists in sidebar ==='); - + testLog.info('=== Step 11: Verifying restored page exists in sidebar ==='); + // Expand the space to see all pages TestTool.expandSpace(); cy.wait(1000); - + // Verify the restored page exists in the sidebar PageSelectors.names().then($pages => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Pages in sidebar after restore: ${pageNames.join(', ')}`); - + testLog.info(`Pages in sidebar after restore: ${pageNames.join(', ')}`); + // Check if our restored page exists - const pageRestored = pageNames.some(name => + const pageRestored = pageNames.some(name => name === restoredPageName || name === testPageName || name === 'Untitled' ); - + if (pageRestored) { - cy.task('log', `āœ“āœ“āœ“ SUCCESS: Page "${restoredPageName}" has been successfully restored to the sidebar!`); + testLog.info(`āœ“āœ“āœ“ SUCCESS: Page "${restoredPageName}" has been successfully restored to the sidebar!`); } else { throw new Error(`Restored page not found in sidebar. Expected to find "${restoredPageName}", found: ${pageNames.join(', ')}`); } }); - - cy.task('log', '=== Test completed successfully! Page was deleted, verified in trash, and successfully restored! ==='); + + testLog.info('=== Test completed successfully! Page was deleted, verified in trash, and successfully restored! ==='); }); }); }); -}); \ 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..0b091116 100644 --- a/cypress/e2e/page/edit-page.cy.ts +++ b/cypress/e2e/page/edit-page.cy.ts @@ -1,12 +1,10 @@ -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'; +import { testLog } from '../../support/test-helpers'; 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 +14,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.`, @@ -50,8 +47,8 @@ describe('Page Edit Tests', () => { cy.wait(2000); // Step 2: Create a new page using the simpler approach - cy.task('log', '=== Starting Page Creation for Edit Test ==='); - cy.task('log', `Target page name: ${testPageName}`); + testLog.info( '=== Starting Page Creation for Edit Test ==='); + testLog.info( `Target page name: ${testPageName}`); // Click new page button PageSelectors.newPageButton().should('be.visible').click(); @@ -72,18 +69,18 @@ describe('Page Edit Tests', () => { // Close any modal dialogs cy.get('body').then(($body: JQuery) => { if ($body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0) { - cy.task('log', 'Closing modal dialog'); + testLog.info( 'Closing modal dialog'); cy.get('body').type('{esc}'); cy.wait(1000); } }); // Step 3: Add content to the page editor - cy.task('log', '=== Adding Content to Page ==='); + testLog.info( '=== Adding Content to Page ==='); // Find the editor and add content cy.get('[contenteditable="true"]').then($editors => { - cy.task('log', `Found ${$editors.length} editable elements`); + testLog.info( `Found ${$editors.length} editable elements`); // Look for the main editor (not the title) let editorFound = false; @@ -91,7 +88,7 @@ describe('Page Edit Tests', () => { const $el = Cypress.$(el); // Skip title inputs if (!$el.attr('data-testid')?.includes('title') && !$el.hasClass('editor-title')) { - cy.task('log', `Using editor at index ${index}`); + testLog.info( `Using editor at index ${index}`); cy.wrap(el).click().type(testContent.join('{enter}')); editorFound = true; return false; // break the loop @@ -100,7 +97,7 @@ describe('Page Edit Tests', () => { if (!editorFound) { // Fallback: use the last contenteditable element - cy.task('log', 'Using fallback: last contenteditable element'); + testLog.info( 'Using fallback: last contenteditable element'); cy.wrap($editors.last()).click().type(testContent.join('{enter}')); } }); @@ -109,15 +106,15 @@ describe('Page Edit Tests', () => { cy.wait(2000); // Step 4: Verify the content was added - cy.task('log', '=== Verifying Content ==='); + testLog.info( '=== Verifying Content ==='); // Verify each line of content exists in the page testContent.forEach(line => { cy.contains(line).should('exist'); - cy.task('log', `āœ“ Found content: "${line}"`); + testLog.info( `āœ“ Found content: "${line}"`); }); - cy.task('log', '=== Test completed successfully ==='); + testLog.info( '=== 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..f11b22c7 100644 --- a/cypress/e2e/page/more-page-action.cy.ts +++ b/cypress/e2e/page/more-page-action.cy.ts @@ -1,12 +1,10 @@ -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, ModalSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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; @@ -39,11 +37,11 @@ describe('More Page Actions', () => { cy.wait(2000); // Skip expanding space since Getting started is already visible - cy.task('log', 'Page already visible, skipping expand'); + testLog.info( 'Page already visible, skipping expand'); // Open the first available page from the sidebar, then trigger inline ViewActionsPopover via "..." on the row // Find the Getting started page and hover to reveal the more actions - cy.task('log', 'Looking for Getting started page'); + testLog.info( 'Looking for Getting started page'); // Find the page by its text content cy.contains('Getting started') @@ -57,7 +55,7 @@ describe('More Page Actions', () => { // Look for the more actions button - using PageSelectors PageSelectors.moreActionsButton().first().click({ force: true }); - cy.task('log', 'Clicked more actions button'); + testLog.info( 'Clicked more actions button'); // Verify core items in ViewActionsPopover // The menu should be open now, verify at least one of the common actions exists @@ -97,7 +95,7 @@ describe('More Page Actions', () => { // Find the Getting started page and open its more actions menu const originalPageName = 'Getting started'; - cy.task('log', `Opening More Actions for page: ${originalPageName}`); + testLog.info( `Opening More Actions for page: ${originalPageName}`); // Find the page by its text content and hover cy.contains(originalPageName) @@ -111,13 +109,13 @@ describe('More Page Actions', () => { // Look for the more actions button - using PageSelectors PageSelectors.moreActionsButton().first().click({ force: true }); - cy.task('log', 'Clicked more actions button'); + testLog.info( 'Clicked more actions button'); // Click on Duplicate option which is available in the dropdown cy.get('[data-slot="dropdown-menu-content"]').within(() => { cy.contains('Duplicate').click(); }); - cy.task('log', 'Clicked Duplicate option'); + testLog.info( 'Clicked Duplicate option'); // Wait for the duplication to complete waitForReactUpdate(2000); @@ -131,10 +129,10 @@ describe('More Page Actions', () => { const pageCount = $pages.filter((index: number, el: HTMLElement) => el.textContent?.includes('Getting started')).length; expect(pageCount).to.be.at.least(1); - cy.task('log', `Found ${pageCount} pages with 'Getting started' in the name`); + testLog.info( `Found ${pageCount} pages with 'Getting started' in the name`); }); - cy.task('log', 'Page successfully duplicated'); + testLog.info( 'Page successfully duplicated'); }); it.skip('should rename a page and verify the name persists after refresh', () => { @@ -164,7 +162,7 @@ describe('More Page Actions', () => { const originalPageName = 'Getting started'; const renamedPageName = `Renamed Page ${Date.now()}`; - cy.task('log', `Starting rename test: ${originalPageName} -> ${renamedPageName}`); + testLog.info( `Starting rename test: ${originalPageName} -> ${renamedPageName}`); // Find the page by its text content and hover cy.contains(originalPageName) @@ -178,7 +176,7 @@ describe('More Page Actions', () => { // Look for the more actions button - using PageSelectors PageSelectors.moreActionsButton().first().click({ force: true }); - cy.task('log', 'Clicked more actions button'); + testLog.info( 'Clicked more actions button'); // Wait for the dropdown menu to be visible cy.get('[data-slot="dropdown-menu-content"]', { timeout: 5000 }).should('be.visible'); @@ -188,33 +186,33 @@ describe('More Page Actions', () => { cy.contains('Rename').click(); }); - cy.task('log', 'Clicked Rename option'); + testLog.info( 'Clicked Rename option'); // Wait for the rename modal to appear - cy.get('[data-testid="rename-modal-input"]', { timeout: 5000 }) - .should('be.visible') + ModalSelectors.renameInput() + .should('be.visible', { timeout: 5000 }) .clear() .type(renamedPageName); - cy.task('log', `Entered new page name: ${renamedPageName}`); + testLog.info( `Entered new page name: ${renamedPageName}`); // Click the save button - cy.get('[data-testid="rename-modal-save"]').click(); + ModalSelectors.renameSaveButton().click(); - cy.task('log', 'Clicked save button'); + testLog.info( 'Clicked save button'); // Wait for the modal to close and the page to update waitForReactUpdate(2000); // Verify the page was renamed in the sidebar cy.contains(renamedPageName, { timeout: 10000 }).should('exist'); - cy.task('log', 'Page renamed successfully in sidebar'); + testLog.info( 'Page renamed successfully in sidebar'); // Also verify the original name doesn't exist anymore cy.contains(originalPageName).should('not.exist'); // Now refresh the page to verify the rename persisted - cy.task('log', 'Refreshing page to verify persistence...'); + testLog.info( 'Refreshing page to verify persistence...'); cy.reload(); // Wait for the page to reload completely @@ -224,7 +222,7 @@ describe('More Page Actions', () => { // Verify the renamed page still exists after refresh cy.contains(renamedPageName, { timeout: 10000 }).should('exist'); - cy.task('log', 'Renamed page persisted after refresh'); + testLog.info( 'Renamed page persisted after refresh'); // Verify the original name is still gone cy.contains(originalPageName).should('not.exist'); @@ -236,6 +234,6 @@ describe('More Page Actions', () => { // Verify we're on the renamed page by checking the URL or page content cy.url().should('include', '/app'); - cy.task('log', 'Rename test completed successfully - name persisted after refresh'); + testLog.info( '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..972255e1 100644 --- a/cypress/e2e/page/publish-page.cy.ts +++ b/cypress/e2e/page/publish-page.cy.ts @@ -1,29 +1,40 @@ -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 { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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(() => { testEmail = generateRandomEmail(); + + // Handle uncaught exceptions + cy.on('uncaught:exception', (err: Error) => { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found') || + err.message.includes('Failed to execute \'writeText\' on \'Clipboard\': Document is not focused') || + err.name === 'NotAllowedError') { + return false; + } + return true; + }); }); it('publish page, copy URL, open in browser, unpublish, and verify inaccessible', () => { // Handle uncaught exceptions during workspace creation cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -35,66 +46,66 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); // Wait for app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info('Waiting for app to fully load...'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); cy.wait(2000); // 2. Open share popover TestTool.openSharePopover(); - cy.task('log', 'Share popover opened'); + testLog.info('Share popover opened'); // Verify that the Share and Publish tabs are visible cy.contains('Share').should('exist'); cy.contains('Publish').should('exist'); - cy.task('log', 'Share and Publish tabs verified'); + testLog.info('Share and Publish tabs verified'); // 3. Switch to Publish tab cy.contains('Publish').should('exist').click({ force: true }); cy.wait(1000); - cy.task('log', 'Switched to Publish tab'); + testLog.info('Switched to Publish tab'); // Verify Publish to Web section is visible cy.contains('Publish to Web').should('exist'); - cy.task('log', 'Publish to Web section verified'); + testLog.info('Publish to Web section verified'); // 4. Wait for the publish button to be visible and enabled - cy.task('log', 'Waiting for publish button to appear...'); + testLog.info('Waiting for publish button to appear...'); ShareSelectors.publishConfirmButton().should('be.visible').should('not.be.disabled'); - cy.task('log', 'Publish button is visible and enabled'); + testLog.info('Publish button is visible and enabled'); // 5. Click Publish button ShareSelectors.publishConfirmButton().click({ force: true }); - cy.task('log', 'Clicked Publish button'); + testLog.info('Clicked Publish button'); // Wait for publish to complete and URL to appear 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.task('log', 'Page published successfully, URL elements visible'); + ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 }); + testLog.info('Page published successfully, URL elements visible'); // 6. Get the published URL by constructing it from UI elements cy.window().then((win) => { 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) => { + ShareSelectors.publishNamespace().should('be.visible').invoke('text').then((namespace) => { + ShareSelectors.publishNameInput().should('be.visible').invoke('val').then((publishName) => { const namespaceText = namespace.trim(); const publishNameText = String(publishName).trim(); const publishedUrl = `${origin}/${namespaceText}/${publishNameText}`; - cy.task('log', `Constructed published URL: ${publishedUrl}`); + testLog.info(`Constructed published URL: ${publishedUrl}`); // 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"]') + ShareSelectors.publishNameInput() .closest('div.flex.w-full.items-center.overflow-hidden') .find('div.p-1.text-text-primary') .should('be.visible') @@ -103,19 +114,19 @@ describe('Publish Page Test', () => { .click({ force: true }); }); - cy.task('log', 'Clicked copy link button'); + testLog.info('Clicked copy link button'); // Wait for copy operation and notification to appear cy.wait(2000); - cy.task('log', 'Copy operation completed'); + testLog.info('Copy operation completed'); // 8. Open the URL in browser (copy button was clicked, URL is ready) - cy.task('log', `Opening published URL in browser: ${publishedUrl}`); + testLog.info(`Opening published URL in browser: ${publishedUrl}`); cy.visit(publishedUrl, { failOnStatusCode: false }); // 9. Verify the published page loads cy.url({ timeout: 10000 }).should('include', `/${namespaceText}/${publishNameText}`); - cy.task('log', 'Published page opened successfully'); + testLog.info('Published page opened successfully'); // Wait for page content to load cy.wait(3000); @@ -127,14 +138,14 @@ describe('Publish Page Test', () => { cy.get('body').then(($body) => { const bodyText = $body.text(); if (bodyText.includes('404') || bodyText.includes('Not Found')) { - cy.task('log', '⚠ Warning: Page might not be accessible (404 detected)'); + testLog.info('⚠ Warning: Page might not be accessible (404 detected)'); } else { - cy.task('log', 'āœ“ Published page verified and accessible'); + testLog.info('āœ“ Published page verified and accessible'); } }); // 10. Go back to the app to unpublish the page - cy.task('log', 'Going back to app to unpublish the page'); + testLog.info('Going back to app to unpublish the page'); cy.visit('/app', { failOnStatusCode: false }); cy.wait(2000); @@ -144,34 +155,34 @@ describe('Publish Page Test', () => { // 11. Open share popover again to unpublish TestTool.openSharePopover(); - cy.task('log', 'Share popover opened for unpublishing'); + testLog.info('Share popover opened for unpublishing'); // Make sure we're on the Publish tab cy.contains('Publish').should('exist').click({ force: true }); cy.wait(1000); - cy.task('log', 'Switched to Publish tab for unpublishing'); + testLog.info('Switched to Publish tab for unpublishing'); // Wait for unpublish button to be visible ShareSelectors.unpublishButton().should('be.visible', { timeout: 10000 }); - cy.task('log', 'Unpublish button is visible'); + testLog.info('Unpublish button is visible'); // 12. Click Unpublish button ShareSelectors.unpublishButton().click({ force: true }); - cy.task('log', 'Clicked Unpublish button'); + testLog.info('Clicked Unpublish button'); // Wait for unpublish to complete cy.wait(3000); // Verify the page is now unpublished (Publish button should be visible again) ShareSelectors.publishConfirmButton().should('be.visible', { timeout: 10000 }); - cy.task('log', 'āœ“ Page unpublished successfully'); + testLog.info('āœ“ Page unpublished successfully'); // Close the share popover cy.get('body').type('{esc}'); cy.wait(1000); // 13. Try to visit the previously published URL - it should not be accessible - cy.task('log', `Attempting to visit unpublished URL: ${publishedUrl}`); + testLog.info(`Attempting to visit unpublished URL: ${publishedUrl}`); cy.visit(publishedUrl, { failOnStatusCode: false }); // Wait a bit for the page to load @@ -188,7 +199,7 @@ describe('Publish Page Test', () => { }).then((response) => { // Check status code first if (response.status !== 200) { - cy.task('log', `āœ“ Published page is no longer accessible (HTTP status: ${response.status})`); + testLog.info(`āœ“ Published page is no longer accessible (HTTP status: ${response.status})`); } else { // If status is 200, check the response body for error indicators const responseBody = response.body || ''; @@ -215,16 +226,16 @@ describe('Publish Page Test', () => { const wasRedirected = !currentUrl.includes(`/${namespaceText}/${publishNameText}`); if (hasErrorInResponse || hasErrorInBody || wasRedirected) { - cy.task('log', `āœ“ Published page is no longer accessible (unpublish verified)`); + testLog.info(`āœ“ Published page is no longer accessible (unpublish verified)`); } else { // If we still see the URL but no clear errors, check if page content is minimal/error-like // A valid published page would have substantial content const contentLength = bodyText.trim().length; if (contentLength < 100) { - cy.task('log', `āœ“ Published page is no longer accessible (minimal/empty content)`); + testLog.info(`āœ“ Published page is no longer accessible (minimal/empty content)`); } else { // This shouldn't happen, but log it for debugging - cy.task('log', `⚠ Note: Page appears accessible, but unpublish was executed successfully`); + testLog.info(`⚠ Note: Page appears accessible, but unpublish was executed successfully`); } } }); @@ -239,7 +250,9 @@ describe('Publish Page Test', () => { it('publish page and use Visit Site button to open URL', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -250,7 +263,7 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -262,30 +275,30 @@ describe('Publish Page Test', () => { cy.wait(1000); ShareSelectors.publishConfirmButton().should('be.visible').should('not.be.disabled').click({ force: true }); - cy.task('log', 'Clicked Publish button'); + testLog.info('Clicked Publish button'); cy.wait(5000); // Verify published - cy.get('[data-testid="publish-namespace"]').should('be.visible', { timeout: 10000 }); + ShareSelectors.publishNamespace().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) => { + ShareSelectors.publishNamespace().should('be.visible').invoke('text').then((namespace) => { + ShareSelectors.publishNameInput().should('be.visible').invoke('val').then((publishName) => { const publishedUrl = `${origin}/${namespace.trim()}/${String(publishName).trim()}`; - cy.task('log', `Published URL: ${publishedUrl}`); + testLog.info(`Published URL: ${publishedUrl}`); // Click the Visit Site button ShareSelectors.visitSiteButton().should('be.visible').click({ force: true }); - cy.task('log', 'Clicked Visit Site button'); + testLog.info('Clicked Visit Site button'); // Wait for new window/tab to open cy.wait(2000); // Note: Cypress can't directly test window.open in a new tab, // but we can verify the button works by checking if it exists and is clickable - cy.task('log', 'āœ“ Visit Site button is functional'); + testLog.info('āœ“ Visit Site button is functional'); }); }); }); @@ -294,7 +307,9 @@ describe('Publish Page Test', () => { it('publish page, edit publish name, and verify new URL works', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -305,7 +320,7 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -318,35 +333,35 @@ 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 }); + ShareSelectors.publishNamespace().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) => { + ShareSelectors.publishNamespace().invoke('text').then((namespace) => { + ShareSelectors.publishNameInput().invoke('val').then((originalName) => { const namespaceText = namespace.trim(); const originalNameText = String(originalName).trim(); - cy.task('log', `Original publish name: ${originalNameText}`); + testLog.info(`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"]') + ShareSelectors.publishNameInput() .clear() .type(newPublishName) .blur(); - cy.task('log', `Changed publish name to: ${newPublishName}`); + testLog.info(`Changed publish name to: ${newPublishName}`); cy.wait(3000); // Wait for name update // Verify the new URL works const newPublishedUrl = `${origin}/${namespaceText}/${newPublishName}`; - cy.task('log', `New published URL: ${newPublishedUrl}`); + testLog.info(`New published URL: ${newPublishedUrl}`); cy.visit(newPublishedUrl, { failOnStatusCode: false }); cy.wait(3000); cy.url().should('include', `/${namespaceText}/${newPublishName}`); - cy.task('log', 'āœ“ New publish name URL works correctly'); + testLog.info('āœ“ New publish name URL works correctly'); }); }); }); @@ -355,7 +370,9 @@ describe('Publish Page Test', () => { it('publish, modify content, republish, and verify content changes', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -369,14 +386,14 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); cy.wait(2000); // Add initial content to the page - cy.task('log', 'Adding initial content to page'); + testLog.info('Adding initial content to page'); cy.get('[contenteditable="true"]').then(($editors) => { let editorFound = false; $editors.each((index: number, el: HTMLElement) => { @@ -400,26 +417,26 @@ 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.task('log', 'āœ“ First publish successful'); + ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 }); + testLog.info('āœ“ 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) => { + ShareSelectors.publishNamespace().invoke('text').then((namespace) => { + ShareSelectors.publishNameInput().invoke('val').then((publishName) => { const publishedUrl = `${origin}/${namespace.trim()}/${String(publishName).trim()}`; - cy.task('log', `Published URL: ${publishedUrl}`); + testLog.info(`Published URL: ${publishedUrl}`); // Verify initial content is published - cy.task('log', 'Verifying initial published content'); + testLog.info('Verifying initial published content'); cy.visit(publishedUrl, { failOnStatusCode: false }); cy.wait(3000); cy.get('body').should('contain.text', initialContent); - cy.task('log', 'āœ“ Initial content verified on published page'); + testLog.info('āœ“ Initial content verified on published page'); // Go back to app and modify content - cy.task('log', 'Going back to app to modify content'); + testLog.info('Going back to app to modify content'); cy.visit('/app', { failOnStatusCode: false }); cy.wait(2000); SidebarSelectors.pageHeader().should('be.visible', { timeout: 10000 }); @@ -430,7 +447,7 @@ describe('Publish Page Test', () => { cy.wait(3000); // Modify the page content - cy.task('log', 'Modifying page content'); + testLog.info('Modifying page content'); cy.get('[contenteditable="true"]').then(($editors) => { let editorFound = false; $editors.each((index: number, el: HTMLElement) => { @@ -448,7 +465,7 @@ describe('Publish Page Test', () => { cy.wait(5000); // Wait for content to save // Republish to sync the updated content - cy.task('log', 'Republishing to sync updated content'); + testLog.info('Republishing to sync updated content'); TestTool.openSharePopover(); cy.contains('Publish').should('exist').click({ force: true }); cy.wait(1000); @@ -461,17 +478,17 @@ 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.task('log', 'āœ“ Republished successfully'); + ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 }); + testLog.info('āœ“ Republished successfully'); // Verify updated content is published - cy.task('log', 'Verifying updated content on published page'); + testLog.info('Verifying updated content on published page'); cy.visit(publishedUrl, { failOnStatusCode: false }); cy.wait(5000); // Verify the updated content appears (with retry logic) cy.get('body', { timeout: 15000 }).should('contain.text', updatedContent); - cy.task('log', 'āœ“ Updated content verified on published page'); + testLog.info('āœ“ Updated content verified on published page'); }); }); }); @@ -480,7 +497,9 @@ describe('Publish Page Test', () => { it('test publish name validation - invalid characters', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -491,7 +510,7 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -504,14 +523,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 }); + ShareSelectors.publishNamespace().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.task('log', `Original name: ${originalName}`); + ShareSelectors.publishNameInput().invoke('val').then((originalName) => { + testLog.info(`Original name: ${originalName}`); // Try to set name with space (should be rejected) - cy.get('[data-testid="publish-name-input"]') + ShareSelectors.publishNameInput() .clear() .type('invalid name with spaces') .blur(); @@ -522,12 +541,12 @@ 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) => { + ShareSelectors.publishNameInput().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'); + testLog.info('⚠ Warning: Invalid characters were not rejected'); } else { - cy.task('log', 'āœ“ Invalid characters (spaces) were rejected'); + testLog.info('āœ“ Invalid characters (spaces) were rejected'); } }); }); @@ -537,7 +556,9 @@ describe('Publish Page Test', () => { it('test publish settings - toggle comments and duplicate switches', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -548,7 +569,7 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -561,26 +582,26 @@ 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 }); + ShareSelectors.publishNamespace().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(() => { cy.get('input[type="checkbox"]').then(($checkbox) => { - const initialCommentsState = $checkbox.is(':checked'); - cy.task('log', `Initial comments state: ${initialCommentsState}`); + const initialCommentsState = ($checkbox[0] as HTMLInputElement).checked; + testLog.info(`Initial comments state: ${initialCommentsState}`); // Toggle comments by clicking the switch cy.get('input[type="checkbox"]').click({ force: true }); cy.wait(2000); cy.get('input[type="checkbox"]').then(($checkboxAfter) => { - const newCommentsState = $checkboxAfter.is(':checked'); - cy.task('log', `Comments state after toggle: ${newCommentsState}`); + const newCommentsState = ($checkboxAfter[0] as HTMLInputElement).checked; + testLog.info(`Comments state after toggle: ${newCommentsState}`); expect(newCommentsState).to.not.equal(initialCommentsState); - cy.task('log', 'āœ“ Comments switch toggled successfully'); + testLog.info('āœ“ Comments switch toggled successfully'); }); }); }); @@ -588,18 +609,18 @@ describe('Publish Page Test', () => { // Test duplicate switch cy.get('div.flex.items-center.justify-between').contains(/duplicate|template/i).parent().within(() => { cy.get('input[type="checkbox"]').then(($checkbox) => { - const initialDuplicateState = $checkbox.is(':checked'); - cy.task('log', `Initial duplicate state: ${initialDuplicateState}`); + const initialDuplicateState = ($checkbox[0] as HTMLInputElement).checked; + testLog.info(`Initial duplicate state: ${initialDuplicateState}`); // Toggle duplicate cy.get('input[type="checkbox"]').click({ force: true }); cy.wait(2000); cy.get('input[type="checkbox"]').then(($checkboxAfter) => { - const newDuplicateState = $checkboxAfter.is(':checked'); - cy.task('log', `Duplicate state after toggle: ${newDuplicateState}`); + const newDuplicateState = ($checkboxAfter[0] as HTMLInputElement).checked; + testLog.info(`Duplicate state after toggle: ${newDuplicateState}`); expect(newDuplicateState).to.not.equal(initialDuplicateState); - cy.task('log', 'āœ“ Duplicate switch toggled successfully'); + testLog.info('āœ“ Duplicate switch toggled successfully'); }); }); }); @@ -609,7 +630,9 @@ describe('Publish Page Test', () => { it('publish page multiple times - verify URL remains consistent', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -620,7 +643,7 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -635,15 +658,15 @@ 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 }); + ShareSelectors.publishNamespace().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) => { + ShareSelectors.publishNamespace().invoke('text').then((namespace) => { + ShareSelectors.publishNameInput().invoke('val').then((publishName) => { firstPublishedUrl = `${origin}/${namespace.trim()}/${String(publishName).trim()}`; - cy.task('log', `First published URL: ${firstPublishedUrl}`); + testLog.info(`First published URL: ${firstPublishedUrl}`); // Close and reopen share popover cy.get('body').type('{esc}'); @@ -654,14 +677,14 @@ 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) => { + ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 }); + ShareSelectors.publishNamespace().invoke('text').then((namespace2) => { + ShareSelectors.publishNameInput().invoke('val').then((publishName2) => { const secondPublishedUrl = `${origin}/${namespace2.trim()}/${String(publishName2).trim()}`; - cy.task('log', `Second check URL: ${secondPublishedUrl}`); + testLog.info(`Second check URL: ${secondPublishedUrl}`); expect(secondPublishedUrl).to.equal(firstPublishedUrl); - cy.task('log', 'āœ“ Published URL remains consistent across multiple opens'); + testLog.info('āœ“ Published URL remains consistent across multiple opens'); }); }); }); @@ -672,7 +695,9 @@ describe('Publish Page Test', () => { it('publish database (To-dos) and visit published link', () => { cy.on('uncaught:exception', (err: Error) => { - if (err.message.includes('No workspace or service found')) { + if (err.message.includes('No workspace or service found') || + err.message.includes('createThemeNoVars_default is not a function') || + err.message.includes('View not found')) { return false; } return true; @@ -683,14 +708,14 @@ describe('Publish Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(testEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'Signed in'); + testLog.info('Signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); cy.wait(2000); // Navigate to the To-dos database - cy.task('log', 'Navigating to To-dos database'); + testLog.info('Navigating to To-dos database'); cy.contains('To-dos', { timeout: 10000 }).should('be.visible').click({ force: true }); cy.wait(5000); // Wait for database to load @@ -698,7 +723,7 @@ describe('Publish Page Test', () => { cy.get('body').then(($body: JQuery) => { const hasDialog = $body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0; if (hasDialog) { - cy.task('log', 'Closing modal dialog'); + testLog.info('Closing modal dialog'); cy.get('body').type('{esc}'); cy.wait(2000); // Try again if still open @@ -712,7 +737,7 @@ describe('Publish Page Test', () => { }); // Verify we're on a database view (not a document) - cy.task('log', 'Verifying database view loaded'); + testLog.info('Verifying database view loaded'); cy.get('body').should('exist'); // Database should be loaded // Wait a bit more for database to fully initialize and ensure no modals @@ -722,59 +747,59 @@ describe('Publish Page Test', () => { ShareSelectors.shareButton().should('be.visible', { timeout: 10000 }); // Open share popover and publish - cy.task('log', 'Opening share popover to publish database'); + testLog.info('Opening share popover to publish database'); TestTool.openSharePopover(); - cy.task('log', 'Share popover opened'); + testLog.info('Share popover opened'); // Verify that the Share and Publish tabs are visible cy.contains('Share').should('exist'); cy.contains('Publish').should('exist'); - cy.task('log', 'Share and Publish tabs verified'); + testLog.info('Share and Publish tabs verified'); // Switch to Publish tab cy.contains('Publish').should('exist').click({ force: true }); cy.wait(1000); - cy.task('log', 'Switched to Publish tab'); + testLog.info('Switched to Publish tab'); // Verify Publish to Web section is visible cy.contains('Publish to Web').should('exist'); - cy.task('log', 'Publish to Web section verified'); + testLog.info('Publish to Web section verified'); // Wait for the publish button to be visible and enabled - cy.task('log', 'Waiting for publish button to appear...'); + testLog.info('Waiting for publish button to appear...'); ShareSelectors.publishConfirmButton().should('be.visible').should('not.be.disabled'); - cy.task('log', 'Publish button is visible and enabled'); + testLog.info('Publish button is visible and enabled'); // Click Publish button ShareSelectors.publishConfirmButton().click({ force: true }); - cy.task('log', 'Clicked Publish button'); + testLog.info('Clicked Publish button'); // Wait for publish to complete and URL to appear 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.task('log', 'Database published successfully, URL elements visible'); + ShareSelectors.publishNamespace().should('be.visible', { timeout: 10000 }); + testLog.info('Database published successfully, URL elements visible'); // Get the published URL cy.window().then((win) => { 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) => { + ShareSelectors.publishNamespace().should('be.visible').invoke('text').then((namespace) => { + ShareSelectors.publishNameInput().should('be.visible').invoke('val').then((publishName) => { const namespaceText = namespace.trim(); const publishNameText = String(publishName).trim(); const publishedUrl = `${origin}/${namespaceText}/${publishNameText}`; - cy.task('log', `Constructed published database URL: ${publishedUrl}`); + testLog.info(`Constructed published database URL: ${publishedUrl}`); // Visit the published database URL - cy.task('log', `Opening published database URL: ${publishedUrl}`); + testLog.info(`Opening published database URL: ${publishedUrl}`); cy.visit(publishedUrl, { failOnStatusCode: false }); // Verify the published database loads cy.url({ timeout: 10000 }).should('include', `/${namespaceText}/${publishNameText}`); - cy.task('log', 'Published database opened successfully'); + testLog.info('Published database opened successfully'); // Wait for database content to load cy.wait(5000); @@ -786,15 +811,15 @@ describe('Publish Page Test', () => { cy.get('body').then(($body) => { const bodyText = $body.text(); if (bodyText.includes('404') || bodyText.includes('Not Found')) { - cy.task('log', '⚠ Warning: Database might not be accessible (404 detected)'); + testLog.info('⚠ Warning: Database might not be accessible (404 detected)'); } else { // Database should be visible - might have grid/board/calendar elements - cy.task('log', 'āœ“ Published database verified and accessible'); + testLog.info('āœ“ Published database verified and accessible'); // Additional verification: Check if database-specific elements exist // Databases typically have table/grid structures or views cy.get('body').should('exist'); - cy.task('log', 'āœ“ Database view elements present'); + testLog.info('āœ“ Database view elements present'); } }); }); @@ -803,5 +828,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..828afdaf 100644 --- a/cypress/e2e/page/share-page.cy.ts +++ b/cypress/e2e/page/share-page.cy.ts @@ -1,18 +1,15 @@ -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'; +import { testLog } from '../../support/test-helpers'; 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(() => { @@ -35,39 +32,39 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); // Wait for app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); cy.wait(2000); // 2. Open share popover TestTool.openSharePopover(); - cy.task('log', 'Share popover opened'); + testLog.info( 'Share popover opened'); // Verify that the Share and Publish tabs are visible cy.contains('Share').should('exist'); cy.contains('Publish').should('exist'); - cy.task('log', 'Share and Publish tabs verified'); + testLog.info( '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) { - cy.task('log', 'Switching to Share tab'); + testLog.info( 'Switching to Share tab'); cy.contains('Share').should('exist').click({ force: true }); waitForReactUpdate(1000); } else { - cy.task('log', 'Already on Share tab'); + testLog.info( 'Already on Share tab'); } }); // 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(() => { + testLog.info( `Inviting user B: ${userBEmail}`); + ShareSelectors.sharePopover().within(() => { // Find the input field inside the email-tag-input container cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') @@ -90,23 +87,23 @@ describe('Share Page Test', () => { .should('not.be.disabled') .click({ force: true }); - cy.task('log', 'Clicked Invite button'); + testLog.info( 'Clicked Invite button'); }); // 5. Wait for the invite to be sent and user B to appear in the list - cy.task('log', 'Waiting for user B to appear in the people list...'); + testLog.info( 'Waiting for user B to appear in the people list...'); 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'); + testLog.info( 'User B successfully added to the page'); }); // 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(() => { + testLog.info( 'Finding user B\'s access dropdown...'); + 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) @@ -124,13 +121,13 @@ describe('Share Page Test', () => { .should('be.visible') .click({ force: true }); - cy.task('log', 'Opened access level dropdown'); + testLog.info( 'Opened access level dropdown'); waitForReactUpdate(500); }); }); // 7. Click "Remove access" option in the dropdown menu - cy.task('log', 'Clicking Remove access...'); + testLog.info( 'Clicking Remove access...'); // The dropdown menu has role="menu" or uses DropdownMenuContent cy.get('[role="menu"]', { timeout: 5000 }) .should('be.visible') @@ -147,15 +144,15 @@ describe('Share Page Test', () => { waitForReactUpdate(3000); // 8. Verify user B is removed from the list - cy.task('log', 'Verifying user B is removed...'); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( 'Verifying user B is removed...'); + 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'); + testLog.info( 'āœ“ User B successfully removed from access list'); }); // 9. Close the share popover and verify user A still has access to the page - cy.task('log', 'Closing share popover and verifying page is still accessible...'); + testLog.info( 'Closing share popover and verifying page is still accessible...'); cy.get('body').type('{esc}'); waitForReactUpdate(1000); @@ -165,8 +162,8 @@ describe('Share Page Test', () => { // Verify the page content is still visible (user A should still have access) // Check that we can still see page elements cy.get('body').should('be.visible'); - cy.task('log', 'āœ“ User A still has access to the page after removing user B'); - cy.task('log', 'Test completed successfully'); + testLog.info( 'āœ“ User A still has access to the page after removing user B'); + testLog.info( 'Test completed successfully'); }); }); @@ -183,7 +180,7 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -191,7 +188,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 +196,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 +216,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') @@ -227,12 +224,12 @@ describe('Share Page Test', () => { // Should show "Can view" or "Read only" initially cy.get('button').contains(/view|read/i).should('be.visible'); }); - cy.task('log', 'User B added with default view access'); + testLog.info( 'User B added with default view access'); }); // Change access level to "Can edit" - cy.task('log', 'Changing user B access level to "Can edit"...'); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( 'Changing user B access level to "Can edit"...'); + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .closest('div.group') .within(() => { @@ -259,18 +256,18 @@ 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(() => { // Should now show "Can edit" or "Read and write" cy.get('button').contains(/edit|write/i, { timeout: 10000 }).should('be.visible'); - cy.task('log', 'āœ“ User B access level successfully changed to "Can edit"'); + testLog.info( 'āœ“ User B access level successfully changed to "Can edit"'); }); }); cy.get('body').type('{esc}'); - cy.task('log', 'Test completed successfully'); + testLog.info( 'Test completed successfully'); }); }); @@ -290,14 +287,14 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); 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 }); @@ -306,8 +303,8 @@ describe('Share Page Test', () => { }); // Invite multiple users - cy.task('log', `Inviting multiple users: ${userBEmail}, ${userCEmail}, ${userDEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( `Inviting multiple users: ${userBEmail}, ${userCEmail}, ${userDEmail}`); + ShareSelectors.sharePopover().within(() => { const emails = [userBEmail, userCEmail, userDEmail]; emails.forEach((email, index) => { @@ -333,16 +330,16 @@ 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'); cy.contains(userDEmail, { timeout: 10000 }).should('be.visible'); - cy.task('log', 'āœ“ All users successfully added to the page'); + testLog.info( 'āœ“ All users successfully added to the page'); }); cy.get('body').type('{esc}'); - cy.task('log', 'Test completed successfully'); + testLog.info( 'Test completed successfully'); }); }); @@ -359,14 +356,14 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); 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 }); @@ -375,8 +372,8 @@ 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(() => { + testLog.info( `Inviting user B with "Can edit" access level`); + 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,16 +412,16 @@ 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'); + testLog.info( 'User B successfully invited'); // Note: The actual access level verification depends on UI implementation // If the access level selector works, user B should have edit access }); cy.get('body').type('{esc}'); - cy.task('log', 'Test completed successfully'); + testLog.info( 'Test completed successfully'); }); }); @@ -441,14 +438,14 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); 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 +454,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 +474,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 @@ -489,16 +486,16 @@ describe('Share Page Test', () => { const groupText = $elements.text().toLowerCase(); const hasPending = groupText.includes('pending'); if (hasPending) { - cy.task('log', 'āœ“ User B shows pending status'); + testLog.info( 'āœ“ User B shows pending status'); } else { - cy.task('log', 'Note: Pending status may not be visible immediately'); + testLog.info( 'Note: Pending status may not be visible immediately'); } }); }); }); cy.get('body').type('{esc}'); - cy.task('log', 'Test completed successfully'); + testLog.info( 'Test completed successfully'); }); }); @@ -517,14 +514,14 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); 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 }); @@ -533,8 +530,8 @@ describe('Share Page Test', () => { }); // Invite two users - cy.task('log', `Inviting users: ${userBEmail}, ${userCEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( `Inviting users: ${userBEmail}, ${userCEmail}`); + ShareSelectors.sharePopover().within(() => { [userBEmail, userCEmail].forEach((email) => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') @@ -557,15 +554,15 @@ 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'); + testLog.info( 'Both users added successfully'); }); // Remove user B's access - cy.task('log', 'Removing user B access...'); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( 'Removing user B access...'); + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .closest('div.group') .within(() => { @@ -591,15 +588,15 @@ 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'); + testLog.info( 'āœ“ User B removed, User C still has access'); }); // Remove user C's access - cy.task('log', 'Removing user C access...'); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( 'Removing user C access...'); + ShareSelectors.sharePopover().within(() => { cy.contains(userCEmail) .closest('div.group') .within(() => { @@ -625,10 +622,10 @@ 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'); + testLog.info( 'āœ“ Both users successfully removed'); }); // Verify user A still has access @@ -636,8 +633,8 @@ describe('Share Page Test', () => { waitForReactUpdate(1000); cy.url().should('include', '/app'); cy.get('body').should('be.visible'); - cy.task('log', 'āœ“ User A still has access after removing all guests'); - cy.task('log', 'Test completed successfully'); + testLog.info( 'āœ“ User A still has access after removing all guests'); + testLog.info( 'Test completed successfully'); }); }); @@ -654,7 +651,7 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -662,12 +659,12 @@ describe('Share Page Test', () => { // Get the current page URL to verify we stay on it cy.url().then((initialUrl) => { - cy.task('log', `Initial URL: ${initialUrl}`); + testLog.info( `Initial URL: ${initialUrl}`); TestTool.openSharePopover(); - cy.task('log', 'Share popover opened'); + testLog.info( '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 }); @@ -676,8 +673,8 @@ describe('Share Page Test', () => { }); // Invite user B - cy.task('log', `Inviting user B: ${userBEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( `Inviting user B: ${userBEmail}`); + ShareSelectors.sharePopover().within(() => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') .should('be.visible') @@ -697,15 +694,15 @@ 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'); + testLog.info( 'User B successfully added'); }); // 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(() => { + testLog.info( 'Removing user B\'s access (NOT user A\'s own access)...'); + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .should('be.visible') .closest('div.group') @@ -733,16 +730,16 @@ 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'); + testLog.info( 'āœ“ User B removed'); }); // CRITICAL: Verify we're still on the SAME page URL (no navigation happened) cy.url().should('eq', initialUrl); - cy.task('log', `āœ“ URL unchanged: ${initialUrl}`); - cy.task('log', 'āœ“ Navigation did NOT occur when removing another user\'s access'); - cy.task('log', 'āœ“ Fix verified: No navigation when removing someone else\'s access'); + testLog.info( `āœ“ URL unchanged: ${initialUrl}`); + testLog.info( 'āœ“ Navigation did NOT occur when removing another user\'s access'); + testLog.info( 'āœ“ Fix verified: No navigation when removing someone else\'s access'); }); }); }); @@ -763,7 +760,7 @@ describe('Share Page Test', () => { const authUtils = new AuthTestUtils(); authUtils.signInWithTestUrl(userAEmail).then(() => { cy.url().should('include', '/app'); - cy.task('log', 'User A signed in'); + testLog.info( 'User A signed in'); SidebarSelectors.pageHeader().should('be.visible', { timeout: 30000 }); PageSelectors.names().should('exist', { timeout: 30000 }); @@ -771,12 +768,12 @@ describe('Share Page Test', () => { // Get the current page URL to verify we stay on it cy.url().then((initialUrl) => { - cy.task('log', `Initial URL: ${initialUrl}`); + testLog.info( `Initial URL: ${initialUrl}`); TestTool.openSharePopover(); - cy.task('log', 'Share popover opened'); + testLog.info( '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 }); @@ -785,8 +782,8 @@ describe('Share Page Test', () => { }); // Invite user B - cy.task('log', `Inviting user B: ${userBEmail}`); - cy.get('[data-testid="share-popover"]').within(() => { + testLog.info( `Inviting user B: ${userBEmail}`); + ShareSelectors.sharePopover().within(() => { cy.get('[data-slot="email-tag-input"]') .find('input[type="text"]') .should('be.visible') @@ -806,19 +803,19 @@ 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'); + testLog.info( 'User B successfully added'); }); // Record time before removal to verify outline refresh timing const startTime = Date.now(); - cy.task('log', `Start time: ${startTime}`); + testLog.info( `Start time: ${startTime}`); // 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(() => { + testLog.info( 'Removing user B\'s access (verifying outline refresh mechanism)...'); + ShareSelectors.sharePopover().within(() => { cy.contains(userBEmail) .should('be.visible') .closest('div.group') @@ -849,22 +846,21 @@ describe('Share Page Test', () => { const endTime = Date.now(); const elapsed = endTime - startTime; - cy.task('log', `End time: ${endTime}, Elapsed: ${elapsed}ms`); + testLog.info( `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'); + testLog.info( 'āœ“ User B removed'); }); // CRITICAL: Verify we're still on the SAME page URL (no navigation happened) cy.url().should('eq', initialUrl); - cy.task('log', `āœ“ URL unchanged: ${initialUrl}`); - cy.task('log', 'āœ“ Navigation did NOT occur when removing another user\'s access'); - cy.task('log', 'āœ“ Outline refresh mechanism verified - fix working correctly'); - cy.task('log', `āœ“ Operation completed in ${elapsed}ms (includes outline refresh time)`); + testLog.info( `āœ“ URL unchanged: ${initialUrl}`); + testLog.info( 'āœ“ Navigation did NOT occur when removing another user\'s access'); + testLog.info( 'āœ“ Outline refresh mechanism verified - fix working correctly'); + testLog.info( `āœ“ Operation completed in ${elapsed}ms (includes outline refresh time)`); }); }); }); }); - diff --git a/cypress/e2e/space/create-space.cy.ts b/cypress/e2e/space/create-space.cy.ts index 6de2dd7d..185c3544 100644 --- a/cypress/e2e/space/create-space.cy.ts +++ b/cypress/e2e/space/create-space.cy.ts @@ -1,20 +1,16 @@ -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, ModalSelectors, waitForReactUpdate } from '../../support/selectors'; +import { generateRandomEmail, logAppFlowyEnvironment } from '../../support/test-config'; +import { testLog } from '../../support/test-helpers'; 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(() => { @@ -38,7 +34,7 @@ describe('Space Creation Tests', () => { }); // Step 1: Login - cy.task('log', '=== Step 1: Login ==='); + testLog.info( '=== Step 1: Login ==='); cy.visit('/login', { failOnStatusCode: false }); cy.wait(2000); @@ -47,7 +43,7 @@ describe('Space Creation Tests', () => { cy.url().should('include', '/app'); // Wait for the app to fully load - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); // Wait for the loading screen to disappear and main app to appear cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); @@ -61,81 +57,81 @@ describe('Space Creation Tests', () => { // Additional wait for stability cy.wait(2000); - cy.task('log', 'App loaded successfully'); + testLog.info( 'App loaded successfully'); // Step 2: Find the first space and open its more actions menu - cy.task('log', '=== Step 2: Opening space more actions menu ==='); + testLog.info( '=== Step 2: Opening space more actions menu ==='); // Get the first space item and click more actions // With the test environment check, the button is always visible in tests SpaceSelectors.items().first().then($space => { - cy.task('log', 'Found first space, clicking more actions...'); + testLog.info( 'Found first space, clicking more actions...'); // 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(); - cy.task('log', 'Clicked space more actions button'); + testLog.info( 'Clicked space more actions button'); }); // Wait for the dropdown menu to appear cy.wait(1000); // Step 3: Click on "Create New Space" option - cy.task('log', '=== Step 3: Clicking Create New Space option ==='); + testLog.info( '=== Step 3: Clicking Create New Space option ==='); - cy.get('[data-testid="create-new-space-button"]') + SpaceSelectors.createNewSpaceButton() .should('be.visible') .click(); - cy.task('log', 'Clicked Create New Space button'); + testLog.info( 'Clicked Create New Space button'); // Wait for modal to appear cy.wait(1000); // Step 4: Fill in the space details - cy.task('log', '=== Step 4: Filling space creation form ==='); + testLog.info( '=== Step 4: Filling space creation form ==='); // Verify the modal is visible - cy.get('[data-testid="create-space-modal"]') + SpaceSelectors.createSpaceModal() .should('be.visible'); - cy.task('log', 'Create Space modal is visible'); + testLog.info( 'Create Space modal is visible'); // Enter space name - cy.get('[data-testid="space-name-input"]') + SpaceSelectors.spaceNameInput() .should('be.visible') .clear() .type(spaceName); - cy.task('log', `Entered space name: ${spaceName}`); + testLog.info( `Entered space name: ${spaceName}`); // Optional: Click on space icon button to set an icon (skip for simplicity) // Optional: Change space permission (default is Public, keep it) // Step 5: Save the new space - cy.task('log', '=== Step 5: Saving new space ==='); + testLog.info( '=== Step 5: Saving new space ==='); // Click the Save button - cy.get('[data-testid="modal-ok-button"]') + ModalSelectors.okButton() .should('be.visible') .click(); - cy.task('log', 'Clicked Save button'); + testLog.info( 'Clicked Save button'); // Wait for the modal to close and space to be created cy.wait(3000); // Step 6: Verify the new space appears in the sidebar - cy.task('log', '=== Step 6: Verifying new space in sidebar ==='); + testLog.info( '=== Step 6: Verifying new space in sidebar ==='); // Check that the new space exists in the sidebar SpaceSelectors.names().then($spaces => { const spaceNames = Array.from($spaces).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Spaces in sidebar: ${spaceNames.join(', ')}`); + testLog.info( `Spaces in sidebar: ${spaceNames.join(', ')}`); // Check if our space exists const spaceExists = spaceNames.some(name => @@ -143,11 +139,11 @@ describe('Space Creation Tests', () => { ); if (spaceExists) { - cy.task('log', `āœ“ New space "${spaceName}" found in sidebar`); + testLog.info( `āœ“ New space "${spaceName}" found in sidebar`); } else { // Sometimes the space might be created but not immediately visible // Let's refresh the outline - cy.task('log', 'Space not immediately visible, checking again...'); + testLog.info( 'Space not immediately visible, checking again...'); cy.wait(2000); // Check again @@ -158,16 +154,16 @@ describe('Space Creation Tests', () => { ); if (spaceExistsNow) { - cy.task('log', `āœ“ New space "${spaceName}" found after refresh`); + testLog.info( `āœ“ New space "${spaceName}" found after refresh`); } else { - cy.task('log', `Warning: Could not find space "${spaceName}" in sidebar, but creation likely succeeded`); + testLog.info( `Warning: Could not find space "${spaceName}" in sidebar, but creation likely succeeded`); } }); } }); // Step 7: Optional - Verify the new space is clickable - cy.task('log', '=== Step 7: Testing space functionality ==='); + testLog.info( '=== Step 7: Testing space functionality ==='); // Simply verify the space exists and is clickable SpaceSelectors.names() @@ -175,14 +171,14 @@ describe('Space Creation Tests', () => { .should('exist') .click({ force: true }); - cy.task('log', 'āœ“ Clicked on the new space'); + testLog.info( 'āœ“ Clicked on the new space'); // Wait briefly to ensure no errors cy.wait(1000); // Final verification - cy.task('log', '=== Test completed successfully! ==='); - cy.task('log', 'āœ“āœ“āœ“ New space created successfully'); + testLog.info( '=== Test completed successfully! ==='); + testLog.info( 'āœ“āœ“āœ“ New space created successfully'); // Verify no errors on the page cy.get('body').then($body => { @@ -191,10 +187,10 @@ describe('Space Creation Tests', () => { $body.find('[role="alert"]').length > 0; if (!hasError) { - cy.task('log', 'āœ“ No errors detected on page'); + testLog.info( 'āœ“ No errors detected on page'); } }); }); }); }); -}); \ No newline at end of file +}); diff --git a/cypress/e2e/user/user.cy.ts b/cypress/e2e/user/user.cy.ts index 0965fcd3..95545dba 100644 --- a/cypress/e2e/user/user.cy.ts +++ b/cypress/e2e/user/user.cy.ts @@ -1,19 +1,18 @@ -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'; +import { testLog } from '../../support/test-helpers'; 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} + testLog.info( `Test Environment Configuration: + - APPFLOWY_BASE_URL: ${env.appflowyBaseUrl} + - APPFLOWY_GOTRUE_BASE_URL: ${env.appflowyGotrueBaseUrl} + - APPFLOWY_WS_BASE_URL: ${APPFLOWY_WS_BASE_URL ?? ''} `); }); @@ -50,10 +49,10 @@ describe('User Feature Tests', () => { // Verify we're on the app page cy.url().should('include', '/app'); - cy.task('log', 'Authentication flow completed successfully'); + testLog.info( 'Authentication flow completed successfully'); // Wait for workspace to be fully loaded by checking for key elements - cy.task('log', 'Waiting for app to fully load...'); + testLog.info( 'Waiting for app to fully load...'); // Wait for the loading screen to disappear and main app to appear cy.get('body', { timeout: 30000 }).should('not.contain', 'Welcome!'); @@ -67,7 +66,7 @@ describe('User Feature Tests', () => { // Wait for workspace dropdown to be available WorkspaceSelectors.dropdownTrigger().should('be.visible', { timeout: 30000 }); - cy.task('log', 'App fully loaded'); + testLog.info( 'App fully loaded'); // Additional wait for stability cy.wait(1000); @@ -82,12 +81,12 @@ describe('User Feature Tests', () => { WorkspaceSelectors.dropdownContent().within(() => { cy.contains(randomEmail).should('be.visible'); }); - cy.task('log', `Verified email ${randomEmail} is displayed in dropdown`); + testLog.info( `Verified email ${randomEmail} is displayed in dropdown`); // Verify one member count TestTool.getWorkspaceMemberCounts() .should('contain', '1 member'); - cy.task('log', 'Verified workspace has 1 member'); + testLog.info( 'Verified workspace has 1 member'); // Verify exactly one workspace exists TestTool.getWorkspaceItems() @@ -97,11 +96,11 @@ describe('User Feature Tests', () => { WorkspaceSelectors.itemName() .should('exist') .and('not.be.empty'); - cy.task('log', 'Verified one workspace exists'); + testLog.info( 'Verified one workspace exists'); }); }); }); -}); \ 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/auth-utils.ts b/cypress/support/auth-utils.ts index f6e7884f..782e7c90 100644 --- a/cypress/support/auth-utils.ts +++ b/cypress/support/auth-utils.ts @@ -1,3 +1,4 @@ +import { testLog } from './test-helpers'; /// export interface AuthConfig { @@ -168,7 +169,7 @@ export class AuthTestUtils { // First, we need to call the verify endpoint to create the user profile // This endpoint creates the user in the AppFlowy backend - cy.task('log', 'Calling verify endpoint to create user profile'); + testLog.info( 'Calling verify endpoint to create user profile'); // Make the verify call with retry logic for CI environment const verifyWithRetry = (retries = 3): Cypress.Chainable => { @@ -178,11 +179,11 @@ export class AuthTestUtils { failOnStatusCode: false, timeout: 30000, }).then((verifyResponse) => { - cy.task('log', `Verify response status: ${verifyResponse.status}`); + testLog.info( `Verify response status: ${verifyResponse.status}`); // If we get a 502 or 503 error, retry if ((verifyResponse.status === 502 || verifyResponse.status === 503) && retries > 0) { - cy.task('log', `Retrying verify endpoint, ${retries} attempts remaining`); + testLog.info( `Retrying verify endpoint, ${retries} attempts remaining`); return cy.wait(2000).then(() => verifyWithRetry(retries - 1)); } @@ -265,4 +266,4 @@ Cypress.Commands.add('signIn', (email: string = 'test@example.com') => { Cypress.Commands.add('generateSignInUrl', (email: string) => { const authUtils = new AuthTestUtils(); return authUtils.generateSignInUrl(email); -}); \ No newline at end of file +}); 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/page-utils.ts b/cypress/support/page-utils.ts index 294a4b72..665156d6 100644 --- a/cypress/support/page-utils.ts +++ b/cypress/support/page-utils.ts @@ -1,3 +1,4 @@ +import { testLog } from './test-helpers'; /** * Main export file for Cypress E2E test utilities * This file aggregates and re-exports all utility functions used in tests @@ -158,7 +159,7 @@ export class TestTool { * Used in create-delete-page.cy.ts */ static verifyPageExists(pageName: string) { - cy.task('log', `Verifying page exists: ${pageName}`); + testLog.info( `Verifying page exists: ${pageName}`); PageSelectors.nameContaining(pageName) .should('exist') .should('be.visible'); @@ -169,11 +170,11 @@ export class TestTool { * Used in create-delete-page.cy.ts */ static verifyPageNotExists(pageName: string) { - cy.task('log', `Verifying page does not exist: ${pageName}`); + testLog.info( `Verifying page does not exist: ${pageName}`); PageSelectors.nameContaining(pageName) .should('not.exist'); } } // Export all individual functions for convenience -export default TestTool; \ No newline at end of file +export default TestTool; diff --git a/cypress/support/page/flows.ts b/cypress/support/page/flows.ts index 72c64fc1..e1eee04d 100644 --- a/cypress/support/page/flows.ts +++ b/cypress/support/page/flows.ts @@ -1,3 +1,4 @@ +import { testLog } from '../test-helpers'; /** * Flow utility functions for Cypress E2E tests * Contains high-level test flow operations that orchestrate multiple page interactions @@ -17,7 +18,7 @@ import { * @returns Cypress chainable */ export function waitForPageLoad(waitTime: number = 3000) { - cy.task('log', `Waiting for page load (${waitTime}ms)`); + testLog.info( `Waiting for page load (${waitTime}ms)`); return cy.wait(waitTime); } @@ -27,7 +28,7 @@ export function waitForPageLoad(waitTime: number = 3000) { * @returns Cypress chainable */ export function waitForSidebarReady(timeout: number = 10000) { - cy.task('log', 'Waiting for sidebar to be ready'); + testLog.info( 'Waiting for sidebar to be ready'); return SidebarSelectors.pageHeader() .should('be.visible', { timeout }); } @@ -39,19 +40,19 @@ export function waitForSidebarReady(timeout: number = 10000) { * @param content - Array of content lines to add to the page */ export function createPageAndAddContent(pageName: string, content: string[]) { - cy.task('log', `Creating page "${pageName}" with ${content.length} lines of content`); + testLog.info( `Creating page "${pageName}" with ${content.length} lines of content`); // Create the page first - this navigates to the new page automatically createPage(pageName); - cy.task('log', 'Page created successfully and we are now on the page'); + testLog.info( 'Page created successfully and we are now on the page'); // We're already on the newly created page, just add content - cy.task('log', 'Adding content to the page'); + testLog.info( 'Adding content to the page'); typeLinesInVisibleEditor(content); - cy.task('log', 'Content typed successfully'); + testLog.info( 'Content typed successfully'); waitForReactUpdate(1000); assertEditorContentEquals(content); - cy.task('log', 'Content verification completed'); + testLog.info( 'Content verification completed'); } /** @@ -60,7 +61,7 @@ export function createPageAndAddContent(pageName: string, content: string[]) { * @param pageName - Name of the page to open */ export function openPageFromSidebar(pageName: string) { - cy.task('log', `Opening page from sidebar: ${pageName}`); + testLog.info( `Opening page from sidebar: ${pageName}`); // Ensure sidebar is visible SidebarSelectors.pageHeader().should('be.visible'); @@ -68,11 +69,11 @@ export function openPageFromSidebar(pageName: string) { // Try to find the page - it might be named differently in the sidebar PageSelectors.names().then(($pages: JQuery) => { const pageNames = Array.from($pages).map((el: Element) => el.textContent?.trim()); - cy.task('log', `Available pages in sidebar: ${pageNames.join(', ')}`); + testLog.info( `Available pages in sidebar: ${pageNames.join(', ')}`); // Try to find exact match first if (pageNames.includes(pageName)) { - cy.task('log', `Found exact match for: ${pageName}`); + testLog.info( `Found exact match for: ${pageName}`); PageSelectors.nameContaining(pageName) .first() .scrollIntoView() @@ -80,12 +81,12 @@ export function openPageFromSidebar(pageName: string) { .click(); } else { // If no exact match, try to find the most recently created page (usually last or first untitled) - cy.task('log', `No exact match for "${pageName}", clicking most recent page`); + testLog.info( `No exact match for "${pageName}", clicking most recent page`); // Look for "Untitled" or the first/last page const untitledPage = pageNames.find(name => name === 'Untitled' || name?.includes('Untitled')); if (untitledPage) { - cy.task('log', `Found untitled page: ${untitledPage}`); + testLog.info( `Found untitled page: ${untitledPage}`); PageSelectors.nameContaining('Untitled') .first() .scrollIntoView() @@ -94,7 +95,7 @@ export function openPageFromSidebar(pageName: string) { } else { // Just click the first non-"Getting started" page const targetPage = pageNames.find(name => name !== 'Getting started') || pageNames[0]; - cy.task('log', `Clicking page: ${targetPage}`); + testLog.info( `Clicking page: ${targetPage}`); PageSelectors.names() .first() .scrollIntoView() @@ -106,7 +107,7 @@ export function openPageFromSidebar(pageName: string) { // Wait for page to load waitForReactUpdate(2000); - cy.task('log', `Page opened successfully`); + testLog.info( `Page opened successfully`); } /** @@ -115,17 +116,17 @@ export function openPageFromSidebar(pageName: string) { * @param spaceIndex - Index of the space to expand (default: 0 for first space) */ export function expandSpace(spaceIndex: number = 0) { - cy.task('log', `Expanding space at index ${spaceIndex}`); + testLog.info( `Expanding space at index ${spaceIndex}`); SpaceSelectors.items().eq(spaceIndex).within(() => { SpaceSelectors.expanded().then(($expanded: JQuery) => { const isExpanded = $expanded.attr('data-expanded') === 'true'; if (!isExpanded) { - cy.task('log', 'Space is collapsed, expanding it'); + testLog.info( 'Space is collapsed, expanding it'); SpaceSelectors.names().first().click(); } else { - cy.task('log', 'Space is already expanded'); + testLog.info( 'Space is already expanded'); } }); }); @@ -140,7 +141,7 @@ export function expandSpace(spaceIndex: number = 0) { * Internal function used by createPageAndAddContent */ function createPage(pageName: string) { - cy.task('log', `Creating page: ${pageName}`); + testLog.info( `Creating page: ${pageName}`); // Click new page button PageSelectors.newPageButton().should('be.visible').click(); @@ -161,7 +162,7 @@ function createPage(pageName: string) { // Close any modal dialogs cy.get('body').then(($body: JQuery) => { if ($body.find('[role="dialog"]').length > 0) { - cy.task('log', 'Closing modal dialog'); + testLog.info( 'Closing modal dialog'); cy.get('body').type('{esc}'); waitForReactUpdate(1000); } @@ -183,12 +184,12 @@ function createPage(pageName: string) { .type('{enter}'); }); - cy.task('log', `Set page title to: ${pageName}`); + testLog.info( `Set page title to: ${pageName}`); waitForReactUpdate(2000); // Also update the page name in the sidebar if possible // This ensures the page can be found later by name - cy.task('log', 'Page created and title set'); + testLog.info( 'Page created and title set'); } /** @@ -196,7 +197,7 @@ function createPage(pageName: string) { * Internal function used by createPageAndAddContent */ function typeLinesInVisibleEditor(lines: string[]) { - cy.task('log', `Typing ${lines.length} lines in editor`); + testLog.info( `Typing ${lines.length} lines in editor`); // Wait for any template to load waitForReactUpdate(1000); @@ -204,7 +205,7 @@ function typeLinesInVisibleEditor(lines: string[]) { // Check if we need to dismiss welcome content or click to create editor cy.get('body').then(($body: JQuery) => { if ($body.text().includes('Welcome to AppFlowy')) { - cy.task('log', 'Welcome template detected, looking for editor area'); + testLog.info( 'Welcome template detected, looking for editor area'); } }); @@ -212,7 +213,7 @@ function typeLinesInVisibleEditor(lines: string[]) { cy.get('[contenteditable="true"]', { timeout: 10000 }).should('exist'); cy.get('[contenteditable="true"]').then(($editors: JQuery) => { - cy.task('log', `Found ${$editors.length} editable elements`); + testLog.info( `Found ${$editors.length} editable elements`); if ($editors.length === 0) { throw new Error('No editable elements found on page'); @@ -227,7 +228,7 @@ function typeLinesInVisibleEditor(lines: string[]) { $el.attr('id')?.includes('title'); if (!isTitle && el) { - cy.task('log', `Using editor at index ${index}`); + testLog.info( `Using editor at index ${index}`); cy.wrap(el).click({ force: true }).clear().type(lines.join('{enter}'), { force: true }); editorFound = true; return false; // break the loop @@ -235,7 +236,7 @@ function typeLinesInVisibleEditor(lines: string[]) { }); if (!editorFound && $editors.length > 0) { - cy.task('log', 'Using fallback: last contenteditable element'); + testLog.info( 'Using fallback: last contenteditable element'); const lastEditor = $editors.last().get(0); if (lastEditor) { cy.wrap(lastEditor).click({ force: true }).clear().type(lines.join('{enter}'), { force: true }); @@ -249,11 +250,11 @@ function typeLinesInVisibleEditor(lines: string[]) { * Internal function used by createPageAndAddContent */ function assertEditorContentEquals(lines: string[]) { - cy.task('log', 'Verifying editor content'); + testLog.info( 'Verifying editor content'); lines.forEach(line => { cy.contains(line).should('exist'); - cy.task('log', `āœ“ Found content: "${line}"`); + testLog.info( `āœ“ Found content: "${line}"`); }); } @@ -264,7 +265,7 @@ function assertEditorContentEquals(lines: string[]) { * Referenced in page-utils.ts */ export function closeSidebar() { - cy.task('log', 'Closing sidebar'); + testLog.info( 'Closing sidebar'); // Implementation would depend on how sidebar is closed in the UI // This is a placeholder to maintain compatibility } @@ -274,7 +275,7 @@ export function closeSidebar() { * Referenced in page-utils.ts */ export function createNewPageViaBackendQuickAction(pageName?: string) { - cy.task('log', `Creating new page via backend quick action: ${pageName || 'unnamed'}`); + testLog.info( `Creating new page via backend quick action: ${pageName || 'unnamed'}`); // Implementation would depend on the backend quick action flow // This is a placeholder to maintain compatibility } @@ -284,7 +285,7 @@ export function createNewPageViaBackendQuickAction(pageName?: string) { * Referenced in page-utils.ts */ export function openCommandPalette() { - cy.task('log', 'Opening command palette'); + testLog.info( 'Opening command palette'); // Implementation would depend on how command palette is opened // This is a placeholder to maintain compatibility } @@ -294,6 +295,6 @@ export function openCommandPalette() { * Referenced in page-utils.ts */ export function navigateTo(route: string) { - cy.task('log', `Navigating to: ${route}`); + testLog.info( `Navigating to: ${route}`); cy.visit(route); } \ No newline at end of file diff --git a/cypress/support/page/modal.ts b/cypress/support/page/modal.ts index 6a00b0e9..34a1f3a1 100644 --- a/cypress/support/page/modal.ts +++ b/cypress/support/page/modal.ts @@ -1,3 +1,4 @@ +import { testLog } from '../test-helpers'; /** * Modal utility functions for Cypress E2E tests * Contains functions for interacting with modal dialogs and popovers @@ -11,12 +12,12 @@ import { ShareSelectors, waitForReactUpdate } from '../selectors'; * @returns Cypress chainable */ export function openSharePopover() { - cy.task('log', 'Opening share popover'); + testLog.info( 'Opening share popover'); // Close any modals that might be blocking the share button cy.get('body').then(($body: JQuery) => { if ($body.find('[role="dialog"]').length > 0 || $body.find('.MuiDialog-container').length > 0) { - cy.task('log', 'Closing modal dialog before opening share popover'); + testLog.info( 'Closing modal dialog before opening share popover'); cy.get('body').type('{esc}'); waitForReactUpdate(1000); } @@ -33,7 +34,7 @@ export function openSharePopover() { // Verify popover is visible ShareSelectors.sharePopover().should('be.visible'); - cy.task('log', 'Share popover opened successfully'); + testLog.info( 'Share popover opened successfully'); } /** @@ -41,7 +42,7 @@ export function openSharePopover() { * Referenced in page-utils.ts */ export function clickOutsideModal() { - cy.task('log', 'Clicking outside modal to close it'); + testLog.info( 'Clicking outside modal to close it'); // Click at the top-left corner of the page cy.get('body').click(0, 0); @@ -49,5 +50,5 @@ export function clickOutsideModal() { // Wait for modal to close waitForReactUpdate(500); - cy.task('log', 'Modal closed'); + testLog.info( 'Modal closed'); } \ No newline at end of file diff --git a/cypress/support/page/page-actions.ts b/cypress/support/page/page-actions.ts index 861776b4..735d5474 100644 --- a/cypress/support/page/page-actions.ts +++ b/cypress/support/page/page-actions.ts @@ -1,3 +1,4 @@ +import { testLog } from '../test-helpers'; /** * Page actions utility functions for Cypress E2E tests * Contains functions for page context menu actions @@ -18,7 +19,7 @@ import { * @returns Cypress chainable */ export function openViewActionsPopoverForPage(pageName: string) { - cy.task('log', `Opening view actions popover for page: ${pageName}`); + testLog.info( `Opening view actions popover for page: ${pageName}`); // Find the page item by name const pageItem = PageSelectors.itemByName(pageName); @@ -48,7 +49,7 @@ export function openViewActionsPopoverForPage(pageName: string) { cy.get('[data-slot="dropdown-menu-content"]', { timeout: 5000 }) .should('exist'); - cy.task('log', 'View actions popover opened successfully'); + testLog.info( 'View actions popover opened successfully'); } /** @@ -58,7 +59,7 @@ export function openViewActionsPopoverForPage(pageName: string) { * @param pageName - The name of the page to delete */ export function deletePageByName(pageName: string) { - cy.task('log', `=== Deleting page: ${pageName} ===`); + testLog.info( `=== Deleting page: ${pageName} ===`); // Find and hover over the page to show actions // Use itemByName to ensure we get the right page item @@ -79,7 +80,7 @@ export function deletePageByName(pageName: string) { ViewActionSelectors.popover() .should('be.visible'); - cy.task('log', 'View actions popover is visible, looking for delete button...'); + testLog.info( 'View actions popover is visible, looking for delete button...'); // Click delete option - look in body since it's portalled ViewActionSelectors.deleteButton() @@ -87,7 +88,7 @@ export function deletePageByName(pageName: string) { .should('be.visible') .click(); - cy.task('log', 'Clicked delete button, checking if confirmation is needed...'); + testLog.info( 'Clicked delete button, checking if confirmation is needed...'); waitForReactUpdate(500); @@ -95,7 +96,7 @@ export function deletePageByName(pageName: string) { // For unpublished pages, deletion happens immediately cy.get('body').then(($body: JQuery) => { if ($body.find('[data-testid="delete-page-confirm-modal"]').length > 0) { - cy.task('log', 'Confirmation modal appeared, clicking confirm...'); + testLog.info( 'Confirmation modal appeared, clicking confirm...'); // Confirm deletion in the confirmation dialog ModalSelectors.confirmDeleteButton() @@ -103,13 +104,13 @@ export function deletePageByName(pageName: string) { .should('be.visible') .click(); - cy.task('log', 'Clicked confirm delete button'); + testLog.info( 'Clicked confirm delete button'); } else { - cy.task('log', 'No confirmation needed (unpublished page), deletion completed'); + testLog.info( 'No confirmation needed (unpublished page), deletion completed'); } }); waitForReactUpdate(1000); - cy.task('log', `āœ“ Page "${pageName}" deleted successfully`); + testLog.info( `āœ“ Page "${pageName}" deleted successfully`); } \ No newline at end of file diff --git a/cypress/support/page/pages.ts b/cypress/support/page/pages.ts index 02bf885c..507be552 100644 --- a/cypress/support/page/pages.ts +++ b/cypress/support/page/pages.ts @@ -1,3 +1,4 @@ +import { testLog } from '../test-helpers'; /** * Page management utility functions for Cypress E2E tests * Contains functions for interacting with pages in the sidebar @@ -12,7 +13,7 @@ import { PageSelectors, waitForReactUpdate } from '../selectors'; * @returns Cypress chainable element */ export function getPageByName(pageName: string) { - cy.task('log', `Getting page by name: ${pageName}`); + testLog.info( `Getting page by name: ${pageName}`); return PageSelectors.itemByName(pageName); } @@ -22,7 +23,7 @@ export function getPageByName(pageName: string) { * @returns Cypress chainable element */ export function getPageTitleInput() { - cy.task('log', 'Getting page title input element'); + testLog.info( 'Getting page title input element'); return PageSelectors.titleInput().first(); } @@ -31,7 +32,7 @@ export function getPageTitleInput() { * Used in more-page-action.cy.ts after editing page titles */ export function savePageTitle() { - cy.task('log', 'Saving page title'); + testLog.info( 'Saving page title'); cy.focused().type('{enter}'); waitForReactUpdate(1000); // Wait for save to complete } @@ -42,7 +43,7 @@ export function savePageTitle() { * @param pageName - The name of the page */ export function openPageMoreActions(pageName: string) { - cy.task('log', `Opening more actions for page: ${pageName}`); + testLog.info( `Opening more actions for page: ${pageName}`); // Find the page and trigger hover to show actions PageSelectors.nameContaining(pageName) diff --git a/cypress/support/page/share-publish.ts b/cypress/support/page/share-publish.ts index c80f648e..612b9fde 100644 --- a/cypress/support/page/share-publish.ts +++ b/cypress/support/page/share-publish.ts @@ -1,3 +1,4 @@ +import { testLog } from '../test-helpers'; /** * Share and Publish utility functions for Cypress E2E tests * Contains functions for publishing pages and verifying published content @@ -11,16 +12,16 @@ import { ShareSelectors, waitForReactUpdate } from '../selectors'; * @returns Cypress chainable with the publish URL */ export function publishCurrentPage() { - cy.task('log', '=== Publishing Current Page ==='); + testLog.info( '=== Publishing Current Page ==='); // Check if share popover is already open cy.get('body').then(($body: JQuery) => { if (!$body.find('[data-testid="share-popover"]').length) { - cy.task('log', 'Share popover not open, opening it'); + testLog.info( 'Share popover not open, opening it'); ShareSelectors.shareButton().should('be.visible').click(); waitForReactUpdate(1000); } else { - cy.task('log', 'Share popover already open'); + testLog.info( 'Share popover already open'); } }); @@ -29,17 +30,17 @@ export function publishCurrentPage() { // Check if we're already on the Publish tab by looking for "Publish to Web" text if (!$body.text().includes('Publish to Web')) { // If we don't see "Publish to Web", we need to click on Publish tab - cy.task('log', 'Switching to Publish tab'); + testLog.info( 'Switching to Publish tab'); cy.contains('Publish').should('be.visible').click(); waitForReactUpdate(500); } else { - cy.task('log', 'Already on Publish tab'); + testLog.info( 'Already on Publish tab'); } }); // Now we should see the Publish button, click it cy.contains('button', 'Publish').should('be.visible').click(); - cy.task('log', 'Clicked Publish button, waiting for publish to complete'); + testLog.info( 'Clicked Publish button, waiting for publish to complete'); // Wait longer for the publish action to complete and URL to appear waitForReactUpdate(5000); @@ -59,7 +60,7 @@ export function publishCurrentPage() { }); if (publishedUrl) { - cy.task('log', `Page published at: ${publishedUrl}`); + testLog.info( `Page published at: ${publishedUrl}`); return publishedUrl; } @@ -71,12 +72,12 @@ export function publishCurrentPage() { if (urlText.length > 0) { const url = urlText.first().text().match(/(https?:\/\/[^\s]+)/)?.[0] || ''; - cy.task('log', `Page published at: ${url}`); + testLog.info( `Page published at: ${url}`); return url; } // If still not found, return a dummy URL for testing - cy.task('log', 'Warning: Could not find published URL, using dummy URL'); + testLog.info( 'Warning: Could not find published URL, using dummy URL'); return 'http://localhost/published/test-page'; }); } @@ -87,7 +88,7 @@ export function publishCurrentPage() { * @returns Cypress chainable with the publish URL */ export function readPublishUrlFromPanel() { - cy.task('log', 'Reading publish URL from panel'); + testLog.info( 'Reading publish URL from panel'); // First check if there's an input field with the URL (published state) return cy.get('body').then(($body: JQuery) => { @@ -98,7 +99,7 @@ export function readPublishUrlFromPanel() { if (urlInput.length > 0) { const url = urlInput.val(); - cy.task('log', `Found publish URL: ${url}`); + testLog.info( `Found publish URL: ${url}`); return url; } else { // If not found, try the selector @@ -106,7 +107,7 @@ export function readPublishUrlFromPanel() { .should('be.visible') .invoke('val') .then((url) => { - cy.task('log', `Found publish URL: ${url}`); + testLog.info( `Found publish URL: ${url}`); return url; }); } @@ -119,7 +120,7 @@ export function readPublishUrlFromPanel() { * @param expectedContent - Array of content strings to verify */ export function verifyPublishedContentMatches(expectedContent: string[]) { - cy.task('log', `=== Verifying Published Content ===`); + testLog.info( `=== Verifying Published Content ===`); // The page should already be loaded, just verify content waitForReactUpdate(2000); @@ -127,10 +128,10 @@ export function verifyPublishedContentMatches(expectedContent: string[]) { // Verify each content line exists expectedContent.forEach(content => { cy.contains(content).should('be.visible'); - cy.task('log', `āœ“ Found published content: "${content}"`); + testLog.info( `āœ“ Found published content: "${content}"`); }); - cy.task('log', 'All published content verified successfully'); + testLog.info( 'All published content verified successfully'); } /** @@ -139,16 +140,16 @@ export function verifyPublishedContentMatches(expectedContent: string[]) { * @param publishUrl - The URL to verify is no longer accessible */ export function unpublishCurrentPageAndVerify(publishUrl: string) { - cy.task('log', '=== Unpublishing Current Page ==='); + testLog.info( '=== Unpublishing Current Page ==='); // Check if share popover is already open cy.get('body').then(($body: JQuery) => { if (!$body.find('[data-testid="share-popover"]').length) { - cy.task('log', 'Share popover not open, opening it'); + testLog.info( 'Share popover not open, opening it'); ShareSelectors.shareButton().should('be.visible').click(); waitForReactUpdate(1000); } else { - cy.task('log', 'Share popover already open'); + testLog.info( 'Share popover already open'); } }); @@ -156,11 +157,11 @@ export function unpublishCurrentPageAndVerify(publishUrl: string) { cy.get('body').then(($body: JQuery) => { if (!$body.text().includes('Publish to Web')) { // If we don't see "Publish to Web", click on Publish tab - cy.task('log', 'Switching to Publish tab'); + testLog.info( 'Switching to Publish tab'); cy.contains('Publish').click(); waitForReactUpdate(500); } else { - cy.task('log', 'Already on Publish tab'); + testLog.info( 'Already on Publish tab'); } }); @@ -173,13 +174,13 @@ export function unpublishCurrentPageAndVerify(publishUrl: string) { waitForReactUpdate(1000); // Verify the page is no longer accessible - cy.task('log', `Verifying ${publishUrl} is no longer accessible`); + testLog.info( `Verifying ${publishUrl} is no longer accessible`); cy.request({ url: publishUrl, failOnStatusCode: false }).then((response: Cypress.Response) => { expect(response.status).to.not.equal(200); - cy.task('log', `āœ“ Published page is no longer accessible (status: ${response.status})`); + testLog.info( `āœ“ Published page is no longer accessible (status: ${response.status})`); }); } @@ -191,7 +192,7 @@ export function unpublishCurrentPageAndVerify(publishUrl: string) { * @param pageContent - The content of the page (unused but kept for compatibility) */ export function unpublishFromSettingsAndVerify(publishUrl: string, pageName?: string, pageContent?: string) { - cy.task('log', '=== Unpublishing from Settings ==='); + testLog.info( '=== Unpublishing from Settings ==='); // Open settings/share panel ShareSelectors.pageSettingsButton().click(); @@ -210,13 +211,13 @@ export function unpublishFromSettingsAndVerify(publishUrl: string, pageName?: st waitForReactUpdate(2000); // Verify the page is no longer accessible - cy.task('log', `Verifying ${publishUrl} is no longer accessible`); + testLog.info( `Verifying ${publishUrl} is no longer accessible`); cy.request({ url: publishUrl, failOnStatusCode: false }).then((response: Cypress.Response) => { expect(response.status).to.not.equal(200); - cy.task('log', `āœ“ Published page is no longer accessible (status: ${response.status})`); + testLog.info( `āœ“ Published page is no longer accessible (status: ${response.status})`); }); } @@ -225,7 +226,7 @@ export function unpublishFromSettingsAndVerify(publishUrl: string, pageName?: st * Used in share-publish.cy.ts (though not exported from page-utils.ts anymore) */ export function openShareLink(shareUrl: string) { - cy.task('log', `Opening share link: ${shareUrl}`); + testLog.info( `Opening share link: ${shareUrl}`); // Visit the share URL cy.visit(shareUrl); @@ -233,5 +234,5 @@ export function openShareLink(shareUrl: string) { // Wait for the page to load cy.url().should('include', '/publish'); - cy.task('log', 'Share link opened successfully'); + testLog.info( 'Share link opened successfully'); } \ No newline at end of file diff --git a/cypress/support/page/workspace.ts b/cypress/support/page/workspace.ts index cd55e9dd..85dd234f 100644 --- a/cypress/support/page/workspace.ts +++ b/cypress/support/page/workspace.ts @@ -1,3 +1,4 @@ +import { testLog } from '../test-helpers'; /** * Workspace utility functions for Cypress E2E tests * Contains functions for interacting with workspace dropdown and settings @@ -10,7 +11,7 @@ import { WorkspaceSelectors, waitForReactUpdate } from '../selectors'; * Used in user.cy.ts to access workspace options */ export function openWorkspaceDropdown() { - cy.task('log', 'Opening workspace dropdown'); + testLog.info( 'Opening workspace dropdown'); WorkspaceSelectors.dropdownTrigger().click(); waitForReactUpdate(500); } @@ -21,7 +22,7 @@ export function openWorkspaceDropdown() { * @returns Cypress chainable containing workspace items */ export function getWorkspaceItems() { - cy.task('log', 'Getting workspace items from dropdown'); + testLog.info( 'Getting workspace items from dropdown'); return WorkspaceSelectors.item(); } @@ -31,7 +32,7 @@ export function getWorkspaceItems() { * @returns Cypress chainable with array of member count strings */ export function getWorkspaceMemberCounts() { - cy.task('log', 'Getting workspace member counts'); + testLog.info( 'Getting workspace member counts'); return WorkspaceSelectors.memberCount() .then(($elements: JQuery) => { @@ -39,7 +40,7 @@ export function getWorkspaceMemberCounts() { $elements.each((index: number, el: HTMLElement) => { counts.push(el.textContent?.trim() || ''); }); - cy.task('log', `Found member counts: ${counts.join(', ')}`); + testLog.info( `Found member counts: ${counts.join(', ')}`); return cy.wrap(counts); }); } @@ -49,7 +50,7 @@ export function getWorkspaceMemberCounts() { * This function is referenced in page-utils.ts but implementation may vary */ export function createWorkspace(workspaceName: string) { - cy.task('log', `Creating workspace: ${workspaceName}`); + testLog.info( `Creating workspace: ${workspaceName}`); // Implementation would go here based on the actual UI flow // This is a placeholder to maintain compatibility } diff --git a/cypress/support/selectors.ts b/cypress/support/selectors.ts index d5798f38..d2ff579c 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 */ @@ -70,6 +81,19 @@ export const SpaceSelectors = { // Get more actions button for spaces moreActionsButton: () => cy.get(byTestId('inline-more-actions')), + + // New space creation controls + createNewSpaceButton: () => cy.get(byTestId('create-new-space-button')), + createSpaceModal: () => cy.get(byTestId('create-space-modal')), + spaceNameInput: () => cy.get(byTestId('space-name-input')), +}; + +/** + * Breadcrumb selectors + */ +export const BreadcrumbSelectors = { + navigation: () => cy.get(byTestId('breadcrumb-navigation')), + items: () => cy.get(byTestIdContains('breadcrumb-item-')), }; /** @@ -113,6 +137,13 @@ export const ModalSelectors = { // Get space item in modal spaceItemInModal: () => cy.get(byTestId('space-item')), + + // Generic modal accept/ok button + okButton: () => cy.get(byTestId('modal-ok-button')), + + // Rename modal inputs + renameInput: () => cy.get(byTestId('rename-modal-input')), + renameSaveButton: () => cy.get(byTestId('rename-modal-save')), }; /** @@ -143,6 +174,10 @@ export const ShareSelectors = { // Publish URL input publishUrlInput: () => cy.get(byTestId('publish-url-input')), + // Publish namespace and name inputs + publishNamespace: () => cy.get(byTestId('publish-namespace')), + publishNameInput: () => cy.get(byTestId('publish-name-input')), + // Page settings button pageSettingsButton: () => cy.get(byTestId('page-settings-button')), @@ -190,6 +225,17 @@ export const SidebarSelectors = { pageHeader: () => cy.get(byTestId('sidebar-page-header')), }; +/** + * Trash view selectors + */ +export const TrashSelectors = { + sidebarTrashButton: () => cy.get(byTestId('sidebar-trash-button')), + table: () => cy.get(byTestId('trash-table')), + rows: () => cy.get(byTestId('trash-table-row')), + restoreButton: () => cy.get(byTestId('trash-restore-button')), + deleteButton: () => cy.get(byTestId('trash-delete-button')), +}; + /** * Chat Model Selector-related selectors * Used for testing AI model selection in chat interface @@ -211,6 +257,19 @@ export const ModelSelectorSelectors = { selectedOption: () => cy.get('[data-testid^="model-option-"]').filter('.bg-fill-content-select'), }; +/** + * Chat UI selectors + */ +export const ChatSelectors = { + aiChatContainer: () => cy.get(byTestId('ai-chat-container')), + formatToggle: () => cy.get(byTestId('chat-input-format-toggle')), + formatGroup: () => cy.get(byTestId('chat-format-group')), + browsePromptsButton: () => cy.get(byTestId('chat-input-browse-prompts')), + relatedViewsButton: () => cy.get(byTestId('chat-input-related-views')), + relatedViewsPopover: () => cy.get(byTestId('chat-related-views-popover')), + sendButton: () => cy.get(byTestId('chat-input-send')), +}; + /** * Database Grid-related selectors */ @@ -417,6 +476,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')), @@ -430,6 +493,27 @@ export const AuthSelectors = { logoutConfirmButton: () => cy.get(byTestId('logout-confirm-button')), }; +/** + * Account settings selectors + */ +export const AccountSelectors = { + settingsButton: () => cy.get(byTestId('account-settings-button')), + settingsDialog: () => cy.get(byTestId('account-settings-dialog')), + dateFormatDropdown: () => cy.get(byTestId('date-format-dropdown')), + dateFormatOptionYearMonthDay: () => cy.get(byTestId('date-format-1')), + timeFormatDropdown: () => cy.get(byTestId('time-format-dropdown')), + timeFormatOption24: () => cy.get(byTestId('time-format-1')), + startWeekDropdown: () => cy.get(byTestId('start-week-on-dropdown')), + startWeekMonday: () => cy.get(byTestId('start-week-1')), +}; + +/** + * Avatar display selectors + */ +export const AvatarUiSelectors = { + image: () => cy.get(byTestId('avatar-image')), +}; + 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..dad9dae9 --- /dev/null +++ b/cypress/support/test-config.ts @@ -0,0 +1,76 @@ +import { v4 as uuidv4 } from 'uuid'; +import { testLog } from './test-helpers'; + +/** + * 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) => { + testLog.info(` +╔════════════════════════════════════════════════════════════════╗ +ā•‘ 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' }); +};