diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 8eb7bb38..57104ca2 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,12 +1,12 @@ name: E2E Tests -on: +on: pull_request: types: [opened, synchronize, reopened] jobs: e2e-tests: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -21,14 +21,50 @@ jobs: checkInterval: 10 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + + - name: Network diagnostics for deploy preview + run: | + echo "## 🌐 Network Diagnostics" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Testing connectivity to Netlify deploy preview..." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + DEPLOY_URL="https://deploy-preview-${{ github.event.pull_request.number }}--leafy-mooncake-7c2e5e.netlify.app" + + echo "### Deploy Preview URL" >> $GITHUB_STEP_SUMMARY + echo "\`$DEPLOY_URL\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Test DNS resolution + echo "### DNS Resolution" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + nslookup deploy-preview-${{ github.event.pull_request.number }}--leafy-mooncake-7c2e5e.netlify.app || echo "DNS lookup failed" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Test HTTP response time and headers + echo "### HTTP Response Test" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + time curl -I -L -s -w "\nHTTP Code: %{http_code}\nTime Total: %{time_total}s\nTime Connect: %{time_connect}s\nTime Start Transfer: %{time_starttransfer}s\n" "$DEPLOY_URL" || echo "HTTP request failed" + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Test navigation API endpoint + echo "### Navigation API Test" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + time curl -s -w "\nHTTP Code: %{http_code}\nTime Total: %{time_total}s\n" "$DEPLOY_URL/api/navigation" | head -20 + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + - name: Run Cypress E2E tests uses: cypress-io/github-action@v6 with: wait-on: 'https://deploy-preview-${{ github.event.pull_request.number }}--leafy-mooncake-7c2e5e.netlify.app' wait-on-timeout: 120 + build: echo "Skipping build - testing against Netlify preview" env: CYPRESS_baseUrl: https://deploy-preview-${{ github.event.pull_request.number }}--leafy-mooncake-7c2e5e.netlify.app + DEBUG: 'cypress:server:*' + ELECTRON_ENABLE_LOGGING: 1 - name: Upload Cypress screenshots on failure if: failure() @@ -53,13 +89,13 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Deployment URL:** https://deploy-preview-${{ github.event.pull_request.number }}--leafy-mooncake-7c2e5e.netlify.app" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + if [[ "${{ job.status }}" == "success" ]]; then echo "✅ All tests passed!" >> $GITHUB_STEP_SUMMARY else echo "❌ Some tests failed. Check the logs for details." >> $GITHUB_STEP_SUMMARY fi - + if [[ -f "./cypress.log" ]]; then echo "" >> $GITHUB_STEP_SUMMARY echo "### Failed Tests" >> $GITHUB_STEP_SUMMARY diff --git a/cypress.config.cjs b/cypress.config.cjs index 5256074d..f4e01042 100644 --- a/cypress.config.cjs +++ b/cypress.config.cjs @@ -16,6 +16,13 @@ module.exports = defineConfig({ specPattern: 'src/tests/cypress/integration/**/*.cy.{js,jsx,ts,tsx}', supportFile: 'src/tests/cypress/support/index.js', baseUrl: 'http://localhost:3030', - pageLoadTimeout: 120000, // Increase timeout to 120 seconds for Netlify preview + pageLoadTimeout: 30000, // Fail fast - max 30 seconds for page load + defaultCommandTimeout: 10000, // 10 seconds for commands + requestTimeout: 10000, // 10 seconds for resource requests - fail faster on network issues + responseTimeout: 10000, // 10 seconds for response - fail faster + retries: { + runMode: 2, // Retry failed tests 2 times in CI + openMode: 0, // No retries in interactive mode + }, }, }) diff --git a/src/tests/cypress/integration/copy-for-llm.cy.js b/src/tests/cypress/integration/copy-for-llm.cy.js index e2210fa6..8787d0b9 100644 --- a/src/tests/cypress/integration/copy-for-llm.cy.js +++ b/src/tests/cypress/integration/copy-for-llm.cy.js @@ -63,9 +63,35 @@ describe('Copy for AI Feature', () => { ? tutorialsSection.slugPrefix.slice(0, -1) : tutorialsSection.slugPrefix tutorialUrl = `${prefix}/${slug}` - cy.log(`Using tutorial: ${tutorialUrl}`) + cy.log(`📄 Selected tutorial: ${tutorialUrl}`) cy.log( - `Language slugs - EN: ${tutorialSlugs.en}, ES: ${tutorialSlugs.es}, PT: ${tutorialSlugs.pt}` + `🌐 Language slugs - EN: ${tutorialSlugs.en}, ES: ${tutorialSlugs.es}, PT: ${tutorialSlugs.pt}` + ) + + // Write to cypress log file for CI visibility + cy.writeFile( + 'cypress.log', + `Selected tutorial URL: ${tutorialUrl}\n`, + { + flag: 'a+', + } + ) + cy.writeFile('cypress.log', `EN slug: ${tutorialSlugs.en}\n`, { + flag: 'a+', + }) + cy.writeFile( + 'cypress.log', + `ES slug: ${tutorialSlugs.es || 'N/A'}\n`, + { + flag: 'a+', + } + ) + cy.writeFile( + 'cypress.log', + `PT slug: ${tutorialSlugs.pt || 'N/A'}\n\n`, + { + flag: 'a+', + } ) } else { throw new Error('No markdown tutorial found in navigation') @@ -79,11 +105,15 @@ describe('Copy for AI Feature', () => { beforeEach(() => { // Handle hydration errors globally for all tests cy.on('uncaught:exception', (err) => { + // Log all uncaught exceptions for debugging + cy.log(`âš ī¸ Uncaught exception: ${err.message}`) + // Ignore hydration-related errors if ( err.message.includes('Suspense boundary') || err.message.includes('hydrating') ) { + cy.log(`â„šī¸ (Ignoring hydration error)`) return false } return true @@ -99,14 +129,34 @@ describe('Copy for AI Feature', () => { }) it('Should find the Copy for AI button in English', () => { - cy.visit(tutorialUrl) + const startTime = Date.now() + cy.log(`🔗 Visiting: ${tutorialUrl}`) + + cy.visit(tutorialUrl, { + timeout: 30000, + failOnStatusCode: false, + }).then( + () => { + const loadTime = Date.now() - startTime + cy.log(`✅ Page loaded in ${loadTime}ms`) + }, + (err) => { + const loadTime = Date.now() - startTime + cy.log(`❌ Visit failed after ${loadTime}ms: ${err.message}`) + throw err + } + ) + cy.contains('button', 'Copy for AI', { timeout: 10000 }).should( 'be.visible' ) }) it('Should copy content to clipboard in English and verify', () => { - cy.visit(tutorialUrl) + cy.visit(tutorialUrl, { + timeout: 30000, + failOnStatusCode: false, + }) // Stub document.execCommand to capture copy operations cy.document().then((doc) => { @@ -149,7 +199,10 @@ describe('Copy for AI Feature', () => { }) it('Should switch to Spanish and find Copy for AI button', () => { - cy.visit(tutorialUrl) + cy.visit(tutorialUrl, { + timeout: 30000, + failOnStatusCode: false, + }) // Click language switcher cy.contains('button', 'EN').click() @@ -176,7 +229,10 @@ describe('Copy for AI Feature', () => { tutorialSlugs.es }` - cy.visit(spanishUrl) + cy.visit(spanishUrl, { + timeout: 30000, + failOnStatusCode: false, + }) // Wait for page to be fully loaded and hydrated cy.get('article', { timeout: 15000 }).should('be.visible') @@ -229,7 +285,10 @@ describe('Copy for AI Feature', () => { }) it('Should switch to Portuguese and find Copy for AI button', () => { - cy.visit(tutorialUrl) + cy.visit(tutorialUrl, { + timeout: 30000, + failOnStatusCode: false, + }) // Click language switcher cy.contains('button', 'EN').click() @@ -256,7 +315,10 @@ describe('Copy for AI Feature', () => { tutorialSlugs.pt }` - cy.visit(portugueseUrl) + cy.visit(portugueseUrl, { + timeout: 30000, + failOnStatusCode: false, + }) // Wait for page to be fully loaded and hydrated cy.get('article', { timeout: 15000 }).should('be.visible') diff --git a/src/tests/cypress/integration/helpcenter-navigation-status.cy.js b/src/tests/cypress/integration/helpcenter-navigation-status.cy.js index ad1cbe2d..bf8b3b6a 100644 --- a/src/tests/cypress/integration/helpcenter-navigation-status.cy.js +++ b/src/tests/cypress/integration/helpcenter-navigation-status.cy.js @@ -88,6 +88,9 @@ describe('Help Center Navigation Status Test', () => { it('should successfully load randomly selected pages from each navbar section', () => { // Handle hydration and React errors during page loads cy.on('uncaught:exception', (err) => { + // Log all uncaught exceptions for debugging + cy.task('log', ` âš ī¸ Uncaught exception: ${err.message}`) + // Ignore hydration-related errors and React minified errors if ( err.message.includes('Suspense boundary') || @@ -95,6 +98,7 @@ describe('Help Center Navigation Status Test', () => { err.message.includes('Minified React error') || err.message.includes('invariant') ) { + cy.task('log', ` â„šī¸ (Ignoring hydration/React error)`) return false } return true @@ -111,7 +115,28 @@ describe('Help Center Navigation Status Test', () => { cy.task('log', ` Page: "${page.name}"`) cy.task('log', ` URL: ${page.url}`) - cy.visit(page.url, { failOnStatusCode: false }) + const startTime = Date.now() + + cy.visit(page.url, { + timeout: 30000, + failOnStatusCode: false, + onBeforeLoad: () => { + cy.task('log', ` âąī¸ Page load started`) + }, + }).then( + () => { + const totalTime = Date.now() - startTime + cy.task('log', ` âąī¸ Visit completed in ${totalTime}ms`) + }, + (err) => { + const totalTime = Date.now() - startTime + cy.task( + 'log', + ` ❌ Visit failed after ${totalTime}ms: ${err.message}` + ) + throw err + } + ) // Verify page loads successfully cy.url().should('include', page.url.replace(baseUrl, ''))