From 335a31f56ce0ec77cb784b7992981132c08b0b59 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 27 Aug 2025 13:53:56 +0800 Subject: [PATCH] chore: fix test --- .github/workflows/integration-test.yml | 93 +++++-- cypress.config.ts | 15 +- cypress/e2e/page/create-delete-page.cy.ts | 8 +- cypress/e2e/page/edit-page.cy.ts | 4 +- cypress/e2e/page/more-page-action.cy.ts | 4 +- cypress/e2e/page/publish-page.cy.ts | 6 +- cypress/e2e/user/user.cy.ts | 12 +- cypress/support/auth-utils.ts | 6 +- cypress/support/console-logger.ts | 2 +- cypress/support/page-utils.ts | 2 + cypress/support/page/flows.ts | 256 ++++++++++++++++-- cypress/support/page/pages.ts | 4 + deploy/server.ts | 2 +- package.json | 2 +- src/application/slate-yjs/utils/applyToYjs.ts | 1 + src/components/view-meta/TitleEditable.tsx | 43 ++- src/components/view-meta/ViewMetaPreview.tsx | 53 ++-- 17 files changed, 412 insertions(+), 101 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index b3df0a50..5b6e0e30 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -9,7 +9,7 @@ on: env: CLOUD_VERSION: latest-amd64 - CYPRESS_BASE_URL: http://localhost:3000 + CYPRESS_BASE_URL: http://localhost:80 GOTRUE_ADMIN_EMAIL: admin@example.com GOTRUE_ADMIN_PASSWORD: password @@ -45,12 +45,18 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install dependencies - run: pnpm install --frozen-lockfile + run: | + echo "Node version: $(node --version)" + echo "pnpm version: $(pnpm --version)" + echo "Installing dependencies..." + pnpm install --frozen-lockfile + echo "Pre-installing Cypress to avoid server timeout issues..." + npx cypress install - - name: Start web server (background) + - name: Prepare web app environment run: | cp deploy.env .env - nohup pnpm run dev > web.log 2>&1 & + echo "Web app will be served via Docker Compose and nginx proxy" - name: Checkout AppFlowy-Cloud-Premium code uses: actions/checkout@v4 @@ -68,6 +74,12 @@ jobs: sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env sed -i 's|AI_OPENAI_API_KEY=.*|AI_OPENAI_API_KEY=${{ secrets.CI_OPENAI_API_KEY }}|' .env sed -i 's|APPFLOWY_SPAM_DETECT_ENABLED=.*|APPFLOWY_SPAM_DETECT_ENABLED=false|' .env + + # Update nginx CORS configuration to allow localhost (port 80) for testing + echo "Backing up original nginx.conf..." + cp nginx/nginx.conf nginx/nginx.conf.backup + echo "Updating nginx CORS configuration for testing..." + sed -i 's|"~^http://localhost:3000$"|"~^http://localhost(:[0-9]+)?$"|g' nginx/nginx.conf - name: Log in to Docker Hub uses: docker/login-action@v3 @@ -75,6 +87,19 @@ jobs: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + - name: Build AppFlowy Web Docker Image + run: | + echo "Building local AppFlowy Web Docker image..." + # Get current commit SHA for versioning + COMMIT_SHA=$(git rev-parse --short HEAD) + WEB_VERSION="local-${COMMIT_SHA}" + + # Build the Docker image using the docker/Dockerfile + docker build -t appflowyinc/appflowy_web:${WEB_VERSION} -f docker/Dockerfile . + + echo "Built image: appflowyinc/appflowy_web:${WEB_VERSION}" + echo "WEB_VERSION=${WEB_VERSION}" >> $GITHUB_ENV + - name: Run Docker-Compose working-directory: AppFlowy-Cloud-Premium env: @@ -82,25 +107,23 @@ jobs: APPFLOWY_AI_VERSION: ${{ env.CLOUD_VERSION }} APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }} APPFLOWY_ADMIN_FRONTEND_VERSION: ${{ env.CLOUD_VERSION }} + APPFLOWY_WEB_VERSION: ${{ env.WEB_VERSION }} RUST_LOG: appflowy_cloud=debug run: | - docker compose pull + # Pull all services except appflowy_web (since we built it locally) + docker compose pull --ignore-pull-failures appflowy_cloud gotrue admin_frontend ai appflowy_worker nginx minio postgres redis docker compose up -d echo "Waiting for the containers to be ready..." sleep 10 + echo "=== Docker Container Status ===" docker ps -a - docker logs appflowy-cloud-premium-appflowy_cloud-1 + echo "" + echo "=== Checking AppFlowy Web Container ===" + docker inspect appflowy-cloud-premium-appflowy_web-1 --format='{{.State.Status}}' || echo "Web container not found" + echo "" + echo "=== Docker Images ===" + docker images | grep appflowy - - name: Wait for frontend to be ready - shell: bash - run: | - for i in {1..120}; do - if curl -sSf http://localhost:3000 > /dev/null; then - echo "Frontend is up"; exit 0; - fi - sleep 10 - done - echo "Frontend did not become ready in time"; exit 1 - name: Wait for backend to be ready shell: bash @@ -115,20 +138,33 @@ jobs: - name: Run integration tests env: - AF_BASE_URL: http://localhost - AF_GOTRUE_URL: http://localhost/gotrue - AF_WS_V2_URL: ws://localhost/ws/v2 + APPFLOWY_BASE_URL: http://localhost + APPFLOWY_GOTRUE_BASE_URL: http://localhost/gotrue + APPFLOWY_WS_BASE_URL: ws://localhost/ws/v2 CYPRESS_chromeWebSecurity: 'false' run: | cp deploy.env .env - npx cypress install - pnpm run test:integration:page:create-delete - # pnpm run test:integration + # Add extra debugging for CI + echo "Testing backend connectivity..." + curl -v http://localhost/health || true + curl -v http://localhost/api/health || true + echo "Running Cypress tests..." + npx cypress run --spec 'cypress/e2e/page/create-delete-page.cy.ts' - name: Cloud server logs if: always() run: | - docker logs appflowy-cloud-premium-appflowy_cloud-1 + echo "=== AppFlowy Cloud Logs ===" + docker logs appflowy-cloud-premium-appflowy_cloud-1 2>&1 | tail -100 + echo "" + echo "=== GoTrue Auth Logs ===" + docker logs appflowy-cloud-premium-gotrue-1 2>&1 | tail -50 + echo "" + echo "=== AppFlowy Web Logs ===" + docker logs appflowy-cloud-premium-appflowy_web-1 2>&1 | tail -50 || echo "No web container logs" + echo "" + echo "=== Nginx Logs ===" + docker logs appflowy-cloud-premium-nginx-1 2>&1 | tail -50 - name: Upload Cypress screenshots if: always() @@ -157,6 +193,17 @@ jobs: if-no-files-found: ignore retention-days: 1 + - name: Cleanup and restore nginx config + if: always() + run: | + echo "Restoring original nginx configuration..." + if [ -f "AppFlowy-Cloud-Premium/nginx/nginx.conf.backup" ]; then + mv AppFlowy-Cloud-Premium/nginx/nginx.conf.backup AppFlowy-Cloud-Premium/nginx/nginx.conf + echo "✅ Original nginx.conf restored" + else + echo "⚠️ No backup found, nginx.conf left modified" + fi + - name: Upload Cypress console logs if: always() uses: actions/upload-artifact@v4 diff --git a/cypress.config.ts b/cypress.config.ts index 25323be2..290f1c5e 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -10,9 +10,9 @@ export default defineConfig({ exclude: ['cypress/**/*.*', '**/__tests__/**/*.*', '**/*.test.*'], }, // Backend URL configuration - load from .env or use defaults - AF_BASE_URL: process.env.AF_BASE_URL || 'http://localhost:8000', - AF_GOTRUE_URL: process.env.AF_GOTRUE_URL || 'http://localhost:9999', - AF_WS_V2_URL: process.env.AF_WS_V2_URL || 'ws://localhost:8000/ws/v2', + APPFLOWY_BASE_URL: process.env.APPFLOWY_BASE_URL || 'http://localhost', + APPFLOWY_GOTRUE_BASE_URL: process.env.APPFLOWY_GOTRUE_BASE_URL || 'http://localhost/gotrue', + APPFLOWY_WS_BASE_URL: process.env.APPFLOWY_WS_BASE_URL || 'ws://localhost/ws/v2', GOTRUE_ADMIN_EMAIL: process.env.GOTRUE_ADMIN_EMAIL || 'admin@example.com', GOTRUE_ADMIN_PASSWORD: process.env.GOTRUE_ADMIN_PASSWORD || 'password', // WebSocket mocking configuration @@ -33,9 +33,9 @@ export default defineConfig({ } // Pass environment variables to Cypress - config.env.AF_BASE_URL = process.env.AF_BASE_URL || config.env.AF_BASE_URL; - config.env.AF_GOTRUE_URL = process.env.AF_GOTRUE_URL || config.env.AF_GOTRUE_URL; - config.env.AF_WS_V2_URL = process.env.AF_WS_V2_URL || config.env.AF_WS_V2_URL; + config.env.APPFLOWY_BASE_URL = process.env.APPFLOWY_BASE_URL || config.env.APPFLOWY_BASE_URL; + config.env.APPFLOWY_GOTRUE_BASE_URL = process.env.APPFLOWY_GOTRUE_BASE_URL || config.env.APPFLOWY_GOTRUE_BASE_URL; + config.env.APPFLOWY_WS_BASE_URL = process.env.APPFLOWY_WS_BASE_URL || config.env.APPFLOWY_WS_BASE_URL; config.env.GOTRUE_ADMIN_EMAIL = process.env.GOTRUE_ADMIN_EMAIL || config.env.GOTRUE_ADMIN_EMAIL; config.env.GOTRUE_ADMIN_PASSWORD = process.env.GOTRUE_ADMIN_PASSWORD || config.env.GOTRUE_ADMIN_PASSWORD; // Pass WebSocket mock configuration @@ -51,7 +51,8 @@ export default defineConfig({ }, async httpCheck({ url, method = 'HEAD' }: { url: string; method?: string }) { try { - const response = await fetch(url, { method: method as any }); + const response = await fetch(url, { method: method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PATCH' }); + return response.ok; } catch (error) { return false; diff --git a/cypress/e2e/page/create-delete-page.cy.ts b/cypress/e2e/page/create-delete-page.cy.ts index 76e6acb4..8d06c740 100644 --- a/cypress/e2e/page/create-delete-page.cy.ts +++ b/cypress/e2e/page/create-delete-page.cy.ts @@ -3,8 +3,8 @@ import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; describe('Page Create and Delete Tests', () => { - const AF_BASE_URL = Cypress.env('AF_BASE_URL'); - const AF_GOTRUE_URL = Cypress.env('AF_GOTRUE_URL'); + 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; @@ -12,8 +12,8 @@ describe('Page Create and Delete Tests', () => { before(() => { // Log environment configuration for debugging cy.task('log', `Test Environment Configuration: - - AF_BASE_URL: ${AF_BASE_URL} - - AF_GOTRUE_URL: ${AF_GOTRUE_URL}`); + - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} + - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); }); beforeEach(() => { diff --git a/cypress/e2e/page/edit-page.cy.ts b/cypress/e2e/page/edit-page.cy.ts index 0044d260..57928023 100644 --- a/cypress/e2e/page/edit-page.cy.ts +++ b/cypress/e2e/page/edit-page.cy.ts @@ -3,8 +3,8 @@ import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; describe('Page Edit Tests', () => { - const AF_BASE_URL = Cypress.env('AF_BASE_URL'); - const AF_GOTRUE_URL = Cypress.env('AF_GOTRUE_URL'); + 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; diff --git a/cypress/e2e/page/more-page-action.cy.ts b/cypress/e2e/page/more-page-action.cy.ts index e733ff58..5569a92b 100644 --- a/cypress/e2e/page/more-page-action.cy.ts +++ b/cypress/e2e/page/more-page-action.cy.ts @@ -3,8 +3,8 @@ import { uuidv4 } from 'lib0/random'; import { TestTool } from '../../support/page-utils'; describe('More Page Actions', () => { - const AF_BASE_URL = Cypress.env('AF_BASE_URL'); - const AF_GOTRUE_URL = Cypress.env('AF_GOTRUE_URL'); + const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); + const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); const newPageName = 'Renamed Test Page'; let testEmail: string; let testEmail2: string; diff --git a/cypress/e2e/page/publish-page.cy.ts b/cypress/e2e/page/publish-page.cy.ts index c8de2a6d..8a490e8f 100644 --- a/cypress/e2e/page/publish-page.cy.ts +++ b/cypress/e2e/page/publish-page.cy.ts @@ -3,8 +3,8 @@ import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; describe('Publish Page Test', () => { - const AF_BASE_URL = Cypress.env('AF_BASE_URL'); - const AF_GOTRUE_URL = Cypress.env('AF_GOTRUE_URL'); + 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; @@ -12,7 +12,7 @@ describe('Publish Page Test', () => { const pageContent = 'This is a publish page content'; before(() => { - cy.task('log', `Env:\n- AF_BASE_URL: ${AF_BASE_URL}\n- AF_GOTRUE_URL: ${AF_GOTRUE_URL}`); + cy.task('log', `Env:\n- APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL}\n- APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL}`); }); beforeEach(() => { diff --git a/cypress/e2e/user/user.cy.ts b/cypress/e2e/user/user.cy.ts index 345b0df5..e42fee2b 100644 --- a/cypress/e2e/user/user.cy.ts +++ b/cypress/e2e/user/user.cy.ts @@ -3,16 +3,16 @@ import { AuthTestUtils } from '../../support/auth-utils'; import { TestTool } from '../../support/page-utils'; describe('User Feature Tests', () => { - const AF_BASE_URL = Cypress.env('AF_BASE_URL'); - const AF_GOTRUE_URL = Cypress.env('AF_GOTRUE_URL'); - const AF_WS_URL = Cypress.env('AF_WS_V2_URL'); + const APPFLOWY_BASE_URL = Cypress.env('APPFLOWY_BASE_URL'); + const APPFLOWY_GOTRUE_BASE_URL = Cypress.env('APPFLOWY_GOTRUE_BASE_URL'); + const APPFLOWY_WS_BASE_URL = Cypress.env('APPFLOWY_WS_BASE_URL'); const generateRandomEmail = () => `${uuidv4()}@appflowy.io`; before(() => { cy.task('log', `Test Environment Configuration: - - AF_BASE_URL: ${AF_BASE_URL} - - AF_GOTRUE_URL: ${AF_GOTRUE_URL} - - AF_WS_URL: ${AF_WS_URL} + - APPFLOWY_BASE_URL: ${APPFLOWY_BASE_URL} + - APPFLOWY_GOTRUE_BASE_URL: ${APPFLOWY_GOTRUE_BASE_URL} + - APPFLOWY_WS_BASE_URL: ${APPFLOWY_WS_BASE_URL} `); }); diff --git a/cypress/support/auth-utils.ts b/cypress/support/auth-utils.ts index ae86baff..41281fb7 100644 --- a/cypress/support/auth-utils.ts +++ b/cypress/support/auth-utils.ts @@ -15,9 +15,9 @@ export class AuthTestUtils { private adminAccessToken?: string; constructor(config?: Partial) { - // Use AF_GOTRUE_URL from environment if available, otherwise construct from AF_BASE_URL - const baseUrl = config?.baseUrl || Cypress.env('AF_BASE_URL') || 'http://localhost:8000'; - const gotrueUrl = config?.gotrueUrl || Cypress.env('AF_GOTRUE_URL') || `http://localhost:9999`; + // Use APPFLOWY_GOTRUE_BASE_URL from environment if available, otherwise construct from APPFLOWY_BASE_URL + const baseUrl = config?.baseUrl || Cypress.env('APPFLOWY_BASE_URL') || 'http://localhost'; + const gotrueUrl = config?.gotrueUrl || Cypress.env('APPFLOWY_GOTRUE_BASE_URL') || `http://localhost/gotrue`; this.config = { baseUrl: baseUrl, diff --git a/cypress/support/console-logger.ts b/cypress/support/console-logger.ts index 021bbd04..01e8090f 100644 --- a/cypress/support/console-logger.ts +++ b/cypress/support/console-logger.ts @@ -44,7 +44,7 @@ function installConsoleInterceptors(win: any) { }; consoleLogs.push(logEntry); - // Immediately output to Node.js console for CI visibility + // Immediately output to Cypress task for CI visibility const message = stringifyArgs(args); const logMessage = `[${new Date().toISOString()}] [CONSOLE.${method.toUpperCase()}] ${message}`; diff --git a/cypress/support/page-utils.ts b/cypress/support/page-utils.ts index 3126e07f..d9cc81d9 100644 --- a/cypress/support/page-utils.ts +++ b/cypress/support/page-utils.ts @@ -30,6 +30,7 @@ export class TestTool { static clickPageByName = Pages.clickPageByName; static getPageById = Pages.getPageById; static getPageTitleInput = Pages.getPageTitleInput; + static savePageTitle = Pages.savePageTitle; // ========== Page Actions ========== static clickPageMoreActions = PageActions.clickPageMoreActions; @@ -141,6 +142,7 @@ export const { clickPageByName, getPageById, getPageTitleInput, + savePageTitle, // Page Actions clickPageMoreActions, diff --git a/cypress/support/page/flows.ts b/cypress/support/page/flows.ts index e4b56fd0..37eb8e53 100644 --- a/cypress/support/page/flows.ts +++ b/cypress/support/page/flows.ts @@ -131,8 +131,21 @@ export function openPageFromSidebar(pageName: string, spaceName?: string) { } export function createPage(pageName: string) { + // Log initial state + cy.task('log', '=== Starting Page Creation ==='); + cy.task('log', `Target page name: ${pageName}`); + + // Check authentication state before creating page + cy.window().then((win) => { + const storage = win.localStorage; + const authToken = storage.getItem('af_auth_token'); + const userId = storage.getItem('af_user_id'); + cy.task('log', `Auth state - Token exists: ${!!authToken}, User ID: ${userId || 'none'}`); + }); + clickNewPageButton(); cy.wait(2000); + getModal() .should('be.visible') .within(() => { @@ -141,31 +154,232 @@ export function createPage(pageName: string) { cy.contains('button', 'Add').click(); }); cy.wait(2000); - cy.task('log', 'Click first space item'); + cy.task('log', 'Clicked Add button, initiating page creation...'); - if (Cypress.env('MOCK_WEBSOCKET')) { - cy.task('log', 'Waiting for document to sync'); - cy.waitForDocumentSync(); - cy.task('log', 'Document synced'); - } - cy.wait(5000); - cy.get('body').then(($body) => { - if ($body.find('[data-testid="page-title-input"]').length > 0) { - cy.task('log', 'Found page title input in modal'); - cy.get('[data-testid="page-title-input"]').first().scrollIntoView().click({ force: true }).clear({ force: true }).type(pageName, { force: true }); - cy.get('[data-testid="page-title-input"]').first().type('{esc}'); - cy.task('log', 'Saved page title'); + // After clicking Add, the modal should close and we should navigate to the new page + // Wait for the modal to disappear first + cy.get('[role="dialog"]').should('not.exist'); + cy.task('log', 'Modal closed successfully'); + + // Capture URL before and after navigation + cy.url().then((urlBefore) => { + cy.task('log', `URL before navigation: ${urlBefore}`); + }); + + // Wait for URL to change - it should include a view ID after navigation + cy.url().should('include', '/app/', { timeout: 10000 }).then((urlAfter) => { + cy.task('log', `URL after navigation: ${urlAfter}`); + // Extract view ID from URL if present + const viewIdMatch = urlAfter.match(/\/app\/[^/]+\/([^/]+)/); + if (viewIdMatch) { + cy.task('log', `View ID from URL: ${viewIdMatch[1]}`); } else { - cy.task('log', 'No page title input found, closing modal without naming'); - cy.get('[role="dialog"]').should('be.visible'); - cy.get('body').type('{esc}'); - cy.wait(500); - cy.get('[data-testid="page-name"]').first().then(($el) => { - const currentName = $el.text().trim(); - cy.task('log', `Current page name: ${currentName}`); - }); + cy.task('log', 'WARNING: No view ID found in URL'); } }); + + if (Cypress.env('MOCK_WEBSOCKET')) { + cy.task('log', 'Waiting for document to sync (MOCK_WEBSOCKET mode)'); + cy.waitForDocumentSync(); + cy.task('log', 'Document synced'); + } else { + cy.task('log', 'WebSocket mode: Real-time sync expected'); + // Check if WebSocket is connected + cy.window().then((win) => { + // Log any global WebSocket state if available + cy.task('log', `Window has WebSocket: ${!!win.WebSocket}`); + }); + } + + // Wait a bit for the page to fully load + cy.wait(3000); + + // Now check for the page title input on the new page + cy.task('log', '=== Checking Page Title Input ==='); + cy.get('body').then(($body) => { + // Debug: Check what elements are available + const titleInputCount = $body.find('[data-testid="page-title-input"]').length; + const h1Count = $body.find('h1').length; + const contentEditableCount = $body.find('[contenteditable="true"]').length; + const readOnlyEditableCount = $body.find('[contenteditable="false"]').length; + const ariaReadOnlyCount = $body.find('[aria-readonly="true"]').length; + + // Check for ViewMetaPreview component indicators + const viewIconCount = $body.find('.view-icon').length; + const pageIconCount = $body.find('[data-testid^="page-icon"]').length; + + // Check for any error messages or loading states + const errorCount = $body.find('[role="alert"]').length; + const loadingCount = $body.find('[data-testid*="loading"], .loading, .spinner').length; + + cy.task('log', '--- Element Counts ---'); + cy.task('log', `page-title-input elements: ${titleInputCount}`); + cy.task('log', `h1 elements: ${h1Count}`); + cy.task('log', `contenteditable="true": ${contentEditableCount}`); + cy.task('log', `contenteditable="false": ${readOnlyEditableCount}`); + cy.task('log', `aria-readonly="true": ${ariaReadOnlyCount}`); + cy.task('log', `view-icon elements: ${viewIconCount}`); + cy.task('log', `page-icon elements: ${pageIconCount}`); + cy.task('log', `error alerts: ${errorCount}`); + cy.task('log', `loading indicators: ${loadingCount}`); + + // Check if we're in the right component context + if (h1Count > 0) { + cy.get('h1').first().then(($h1) => { + const h1Text = $h1.text().trim(); + const h1Classes = $h1.attr('class') || ''; + const h1Parent = $h1.parent().attr('class') || ''; + cy.task('log', `First h1 text: "${h1Text}"`); + cy.task('log', `First h1 classes: ${h1Classes}`); + cy.task('log', `First h1 parent classes: ${h1Parent}`); + + // Check if h1 contains the title input + const hasNestedInput = $h1.find('[data-testid="page-title-input"]').length > 0; + const hasContentEditable = $h1.find('[contenteditable]').length > 0; + cy.task('log', `h1 contains page-title-input: ${hasNestedInput}`); + cy.task('log', `h1 contains contenteditable: ${hasContentEditable}`); + }); + } + + // Log any contenteditable elements found + if (contentEditableCount > 0) { + cy.get('[contenteditable="true"]').first().then(($editable) => { + const editableTag = $editable.prop('tagName'); + const editableId = $editable.attr('id') || 'none'; + const editableTestId = $editable.attr('data-testid') || 'none'; + const editablePlaceholder = $editable.attr('data-placeholder') || 'none'; + cy.task('log', `First contenteditable element:`); + cy.task('log', ` - Tag: ${editableTag}`); + cy.task('log', ` - ID: ${editableId}`); + cy.task('log', ` - data-testid: ${editableTestId}`); + cy.task('log', ` - placeholder: ${editablePlaceholder}`); + }); + } + + // === ATTEMPT TO SET PAGE TITLE === + cy.task('log', '=== Attempting to Set Page Title ==='); + + if (titleInputCount > 0) { + cy.task('log', '✓ Found page-title-input element'); + cy.get('[data-testid="page-title-input"]').first().then(($input) => { + // Check if the input is actually editable + const isEditable = $input.attr('contenteditable') === 'true'; + const isReadOnly = $input.attr('aria-readonly') === 'true'; + cy.task('log', ` - Is editable: ${isEditable}`); + cy.task('log', ` - Is read-only: ${isReadOnly}`); + + if (isEditable && !isReadOnly) { + cy.wrap($input) + .scrollIntoView() + .should('be.visible') + .click({ force: true }) + .clear({ force: true }) + .type(pageName, { force: true }) + .type('{esc}'); + cy.task('log', `✓ Set page title to: ${pageName}`); + } else { + cy.task('log', '✗ Title input found but not editable!'); + cy.task('log', ' This indicates the page is in read-only mode'); + } + }); + } else if (contentEditableCount > 0) { + cy.task('log', '⚠ No page-title-input found, trying contenteditable elements...'); + cy.get('[contenteditable="true"]').first().then(($editable) => { + const editableTestId = $editable.attr('data-testid') || ''; + const isPageTitle = editableTestId.includes('title') || $editable.attr('id')?.includes('title'); + + if (isPageTitle || $editable.prop('tagName') === 'H1') { + cy.task('log', '✓ Found likely title element (contenteditable)'); + cy.wrap($editable) + .scrollIntoView() + .should('be.visible') + .click({ force: true }) + .clear({ force: true }) + .type(pageName, { force: true }) + .type('{esc}'); + cy.task('log', `✓ Set page title using contenteditable: ${pageName}`); + } else { + cy.task('log', '⚠ Found contenteditable but might not be title field'); + cy.wrap($editable) + .scrollIntoView() + .should('be.visible') + .click({ force: true }) + .clear({ force: true }) + .type(pageName, { force: true }) + .type('{esc}'); + cy.task('log', `⚠ Attempted to set title in contenteditable: ${pageName}`); + } + }); + } else { + cy.task('log', '✗ CRITICAL: No editable elements found on page!'); + cy.task('log', 'Possible causes:'); + cy.task('log', ' 1. Page is in read-only mode'); + cy.task('log', ' 2. User lacks edit permissions'); + cy.task('log', ' 3. Page failed to load properly'); + cy.task('log', ' 4. Authentication/session issue'); + + // Try to get more context + cy.get('[data-testid="page-name"]').then(($pageNames) => { + if ($pageNames.length > 0) { + cy.task('log', 'Pages visible in sidebar:'); + $pageNames.each((_, el) => { + cy.task('log', ` - "${el.textContent?.trim()}"`); + }); + } + }); + + // Check for any permission or error indicators + cy.get('body').then(($body) => { + const hasPermissionError = $body.text().includes('permission') || $body.text().includes('unauthorized'); + const hasLoadingError = $body.text().includes('error') || $body.text().includes('failed'); + if (hasPermissionError) { + cy.task('log', '⚠ Page may have permission issues'); + } + if (hasLoadingError) { + cy.task('log', '⚠ Page may have loading errors'); + } + }); + } + + cy.task('log', '=== End Page Title Setting Attempt ==='); + }); + + // Wait a moment for any changes to take effect + cy.wait(1000); + + // === VERIFY PAGE TITLE WAS SET === + cy.task('log', '=== Verifying Page Title ==='); + + // Check sidebar for the page name + cy.get('[data-testid="page-name"]').then(($pageNames) => { + const pageNamesArray = Array.from($pageNames).map(el => el.textContent?.trim()); + cy.task('log', `Pages in sidebar after title attempt: ${JSON.stringify(pageNamesArray)}`); + + const targetPageFound = pageNamesArray.includes(pageName); + if (targetPageFound) { + cy.task('log', `✓ SUCCESS: Page "${pageName}" found in sidebar`); + } else { + cy.task('log', `✗ FAILURE: Page "${pageName}" NOT found in sidebar`); + cy.task('log', ` Expected: "${pageName}"`); + cy.task('log', ` Found: ${JSON.stringify(pageNamesArray)}`); + } + }); + + // Also check the page title on the page itself + cy.get('h1').then(($h1s) => { + if ($h1s.length > 0) { + const h1Text = $h1s.first().text().trim(); + cy.task('log', `Current h1 text: "${h1Text}"`); + if (h1Text === pageName) { + cy.task('log', `✓ Page title matches expected: "${pageName}"`); + } else { + cy.task('log', `⚠ Page title doesn't match. Expected: "${pageName}", Got: "${h1Text}"`); + } + } + }); + + cy.task('log', '=== End Verification ==='); + return cy.wait(1000); } diff --git a/cypress/support/page/pages.ts b/cypress/support/page/pages.ts index d93defce..b2b8fca4 100644 --- a/cypress/support/page/pages.ts +++ b/cypress/support/page/pages.ts @@ -22,4 +22,8 @@ export function getPageTitleInput() { return cy.get('[data-testid="page-title-input"]', { timeout: 30000 }); } +export function savePageTitle() { + return cy.get('[data-testid="page-title-input"]').type('{esc}'); +} + diff --git a/deploy/server.ts b/deploy/server.ts index f9f84fb0..b6195826 100644 --- a/deploy/server.ts +++ b/deploy/server.ts @@ -9,7 +9,7 @@ import pino from 'pino'; const distDir = path.join(__dirname, 'dist'); const indexPath = path.join(distDir, 'index.html'); -const baseURL = process.env.AF_BASE_URL as string; +const baseURL = process.env.APPFLOWY_BASE_URL as string; const defaultSite = 'https://appflowy.com'; const setOrUpdateMetaTag = ($: CheerioAPI, selector: string, attribute: string, content: string) => { diff --git a/package.json b/package.json index c1a284a1..8ab0a6e0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:integration:page:create-delete": "cypress run --spec 'cypress/e2e/page/create-delete-page.cy.ts'", "test:integration:page:edit": "cypress run --spec 'cypress/e2e/page/edit-page.cy.ts'", "test:integration:publish": "cypress run --spec 'cypress/e2e/publish/**/*.cy.ts'", - "wait:backend": "AF_BASE_URL=${AF_BASE_URL:-http://localhost} node scripts/wait-for-backend.js", + "wait:backend": "APPFLOWY_BASE_URL=${APPFLOWY_BASE_URL:-http://localhost} node scripts/wait-for-backend.js", "coverage": "cross-env COVERAGE=true pnpm run test:unit && cross-env COVERAGE=true pnpm run test:components", "generate-tokens": "node scripts/system-token/convert-tokens.cjs", "generate-protobuf": "pbjs -t static-module -w es6 -o ./src/proto/messages.js ./src/proto/messages.proto & pbts -o ./src/proto/messages.d.ts ./src/proto/messages.js" diff --git a/src/application/slate-yjs/utils/applyToYjs.ts b/src/application/slate-yjs/utils/applyToYjs.ts index 41baa690..37901816 100644 --- a/src/application/slate-yjs/utils/applyToYjs.ts +++ b/src/application/slate-yjs/utils/applyToYjs.ts @@ -89,6 +89,7 @@ function insertText(ydoc: Y.Doc, editor: Editor, { path, offset, text, attribute } else { yText.insert(relativeOffset, text, attributes); } + console.log(`insert text:${text}, ytext delta:${JSON.stringify(yText.toDelta())}`); } diff --git a/src/components/view-meta/TitleEditable.tsx b/src/components/view-meta/TitleEditable.tsx index 159a0dad..f20da38a 100644 --- a/src/components/view-meta/TitleEditable.tsx +++ b/src/components/view-meta/TitleEditable.tsx @@ -61,6 +61,13 @@ function TitleEditable({ }) { const { t } = useTranslation(); + // Debug logging for initialization + console.log('[TitleEditable] Component initialized:', { + viewId, + name, + timestamp: Date.now() + }); + // Use ref to manage state, avoid re-rendering const updateStateRef = useRef({ localName: name, @@ -213,20 +220,38 @@ function TitleEditable({ useEffect(() => { const contentBox = contentRef.current; - if (!contentBox) return; + console.log('[TitleEditable] Initializing component:', { + hasContentBox: !!contentBox, + localName: updateStateRef.current.localName, + viewId + }); + + if (!contentBox) { + console.warn('[TitleEditable] contentRef not available yet'); + return; + } // Set initial content to local state contentBox.textContent = updateStateRef.current.localName; initialEditValueRef.current = updateStateRef.current.localName; - contentBox.focus(); + // Use requestAnimationFrame for better timing + const focusAndPosition = () => { + requestAnimationFrame(() => { + console.log('[TitleEditable] Focusing element'); + contentBox.focus(); + + // Move cursor to end + if (contentBox.textContent !== '') { + requestAnimationFrame(() => { + setCursorPosition(contentBox, contentBox.textContent?.length || 0); + console.log('[TitleEditable] Cursor positioned at end'); + }); + } + }); + }; - // Move cursor to end - if (contentBox.textContent !== '') { - setTimeout(() => { - setCursorPosition(contentBox, contentBox.textContent?.length || 0); - }, 0); - } + focusAndPosition(); }, []); // Only execute once when component mounts return ( @@ -246,6 +271,8 @@ function TitleEditable({ aria-readonly={false} autoFocus={true} onFocus={() => { + console.log('[TitleEditable] Focus event triggered'); + // Record initial value when starting to edit if (contentRef.current) { initialEditValueRef.current = contentRef.current.textContent || ''; diff --git a/src/components/view-meta/ViewMetaPreview.tsx b/src/components/view-meta/ViewMetaPreview.tsx index 41d43f71..e0884376 100644 --- a/src/components/view-meta/ViewMetaPreview.tsx +++ b/src/components/view-meta/ViewMetaPreview.tsx @@ -29,6 +29,15 @@ export function ViewMetaPreview({ const [cover, setCover] = React.useState(coverProp || null); const [icon, setIcon] = React.useState(iconProp || null); + // Debug logging for TitleEditable visibility issues + console.log('[ViewMetaPreview] Props:', { + viewId, + readOnly, + name, + hasUpdatePageName: !!updatePageName, + timestamp: Date.now() + }); + useEffect(() => { setCover(coverProp || null); }, [coverProp]); @@ -250,26 +259,32 @@ export function ViewMetaPreview({ ) : null} {!readOnly && viewId ? ( - + <> + {console.log('[ViewMetaPreview] Rendering TitleEditable:', { viewId, readOnly, name })} + + ) : ( -
- {name} -
+ <> + {console.log('[ViewMetaPreview] Rendering non-editable div:', { viewId, readOnly, name })} +
+ {name} +
+ )}