chore: fix test

This commit is contained in:
Nathan
2025-08-27 13:53:56 +08:00
parent 139a32a0eb
commit 335a31f56c
17 changed files with 412 additions and 101 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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(() => {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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(() => {

View File

@@ -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}
`);
});

View File

@@ -15,9 +15,9 @@ export class AuthTestUtils {
private adminAccessToken?: string;
constructor(config?: Partial<AuthConfig>) {
// 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,

View File

@@ -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}`;

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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}');
}

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -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())}`);
}

View File

@@ -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<UpdateState>({
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 || '';

View File

@@ -29,6 +29,15 @@ export function ViewMetaPreview({
const [cover, setCover] = React.useState<ViewMetaCover | null>(coverProp || null);
const [icon, setIcon] = React.useState<ViewMetaIcon | null>(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({
</CustomIconPopover>
) : null}
{!readOnly && viewId ? (
<TitleEditable
onFocus={onFocus}
viewId={viewId}
name={name || ''}
onUpdateName={handleUpdateName}
onEnter={onEnter}
/>
<>
{console.log('[ViewMetaPreview] Rendering TitleEditable:', { viewId, readOnly, name })}
<TitleEditable
onFocus={onFocus}
viewId={viewId}
name={name || ''}
onUpdateName={handleUpdateName}
onEnter={onEnter}
/>
</>
) : (
<div
style={{
wordBreak: 'break-word',
}}
className={
'relative flex-1 cursor-text whitespace-pre-wrap break-words empty:before:text-text-tertiary empty:before:content-[attr(data-placeholder)] focus:outline-none'
}
data-placeholder={t('menuAppHeader.defaultNewPageName')}
contentEditable={false}
>
{name}
</div>
<>
{console.log('[ViewMetaPreview] Rendering non-editable div:', { viewId, readOnly, name })}
<div
style={{
wordBreak: 'break-word',
}}
className={
'relative flex-1 cursor-text whitespace-pre-wrap break-words empty:before:text-text-tertiary empty:before:content-[attr(data-placeholder)] focus:outline-none'
}
data-placeholder={t('menuAppHeader.defaultNewPageName')}
contentEditable={false}
>
{name}
</div>
</>
)}
</h1>
</div>