mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2025-11-30 19:37:55 +08:00
chore: fix test
This commit is contained in:
93
.github/workflows/integration-test.yml
vendored
93
.github/workflows/integration-test.yml
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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}
|
||||
`);
|
||||
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || '';
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user