test: edit page

This commit is contained in:
Nathan
2025-08-14 14:40:00 +08:00
parent cca63877ff
commit bc9f0b0874
9 changed files with 513 additions and 73 deletions

View File

@@ -62,7 +62,7 @@ export default defineConfig({
},
chromeWebSecurity: false,
retries: {
runMode: 1,
runMode: 0,
// Configure retry attempts for `cypress open`
// Default is 0
openMode: 0,

View File

@@ -1,5 +1,6 @@
import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../support/auth-utils';
import { PageUtils } from '../../support/page-utils';
describe('Page Create and Delete Tests', () => {
const AF_BASE_URL = Cypress.env('AF_BASE_URL');
@@ -18,9 +19,6 @@ describe('Page Create and Delete Tests', () => {
});
beforeEach(() => {
// Ensure viewport is set to MacBook Pro size for each test
cy.viewport(1440, 900);
// Generate unique test data for each test
testEmail = generateRandomEmail();
testPageName = 'e2e test-create page';
@@ -47,98 +45,54 @@ describe('Page Create and Delete Tests', () => {
cy.wait(3000);
// Step 2: Create a new page
// Click on the New Page button using data-testid
cy.get('[data-testid="new-page-button"]').click();
PageUtils.clickNewPageButton();
cy.task('log', 'Clicked New Page button');
// Wait for the modal to open
cy.wait(1000);
// Select the first space in the modal
cy.get('[role="dialog"]').should('be.visible').within(() => {
// Click on the first space item
cy.get('[data-testid="space-item"]').first().click();
// Click the Add button
cy.contains('button', 'Add').click();
});
PageUtils.selectFirstSpaceInModal();
// Wait for the page to be created and modal to open
cy.wait(2000);
// The page modal should be open now with the page title input
// Clear any default text and enter the page name
cy.get('[data-testid="page-title-input"]', { timeout: 10000 })
.should('be.visible')
.focus()
.clear()
.type(testPageName);
// Enter the page name
cy.task('log', `Entering page title: ${testPageName}`);
PageUtils.enterPageTitle(testPageName);
// Press Escape to save the title and close the modal
cy.get('[data-testid="page-title-input"]').type('{esc}');
// Save the title and close the modal
PageUtils.savePageTitle();
cy.wait(1000);
cy.task('log', `Created page with title: ${testPageName}`);
// Step 3: Reload and verify the page exists
cy.reload();
cy.wait(3000);
PageUtils.waitForPageLoad(3000);
// Find and click on the first space to expand it (usually "General")
cy.get('[data-testid="space-name"]').first().parent().parent().click();
// Expand the first space to see its pages
PageUtils.expandSpace();
cy.wait(1000);
// Now verify the page exists under the space with exact title
cy.get('[data-testid="page-name"]').contains('e2e test-create page').should('exist');
// Verify the page exists
PageUtils.verifyPageExists('e2e test-create page');
cy.task('log', `Verified page exists after reload: ${testPageName}`);
// Step 4: Delete the page
// First, open the page by clicking on it
cy.get('[data-testid="page-name"]').contains('e2e test-create page').click();
cy.wait(1000);
// Click on the more actions button
cy.get('[data-testid="page-more-actions"]').click();
cy.wait(500);
// Click on delete button
cy.get('[data-testid="delete-page-button"]').click();
cy.wait(500);
// Check if there's a confirmation modal (for published pages)
cy.get('body').then($body => {
if ($body.find('[data-testid="delete-page-confirm-modal"]').length > 0) {
// If confirmation modal exists, click Delete button
cy.get('[data-testid="delete-page-confirm-modal"]').within(() => {
cy.contains('button', 'Delete').click();
});
}
});
cy.wait(2000);
PageUtils.deletePageByName('e2e test-create page');
cy.task('log', `Deleted page: ${testPageName}`);
// Step 5: Reload and verify the page is gone
cy.reload();
cy.wait(3000);
PageUtils.waitForPageLoad(3000);
// Expand the space again to check if page is gone
cy.get('[data-testid="space-name"]').first().parent().parent().click();
PageUtils.expandSpace();
cy.wait(1000);
// Verify the page no longer exists
cy.get('body').then($body => {
// Check if any page-name elements exist
if ($body.find('[data-testid="page-name"]').length > 0) {
// Verify none of them contain our test page name
cy.get('[data-testid="page-name"]').each(($el) => {
cy.wrap($el).should('not.contain', 'e2e test-create page');
});
} else {
// No pages exist, which is also valid
cy.get('[data-testid="page-name"]').should('not.exist');
}
});
PageUtils.verifyPageNotExists('e2e test-create page');
cy.task('log', `Verified page is gone after reload: ${testPageName}`);
});
});

View File

Binary file not shown.

View File

@@ -1,5 +1,6 @@
import { v4 as uuidv4 } from 'uuid';
import { AuthTestUtils } from '../../support/auth-utils';
import { PageUtils } from '../../support/page-utils';
describe('User Feature Tests', () => {
const AF_BASE_URL = Cypress.env('AF_BASE_URL');
@@ -49,10 +50,8 @@ describe('User Feature Tests', () => {
// Wait for workspace to be fully loaded
cy.wait(3000);
// Open workspace dropdown by clicking on the trigger
cy.get('[data-testid="workspace-dropdown-trigger"]', { timeout: 10000 })
.should('be.visible')
.click();
// Open workspace dropdown
PageUtils.openWorkspaceDropdown();
// Wait for dropdown to open
cy.wait(500);
@@ -64,12 +63,12 @@ describe('User Feature Tests', () => {
cy.task('log', `Verified email ${randomEmail} is displayed in dropdown`);
// Verify one member count
cy.get('[data-testid="workspace-member-count"]')
PageUtils.getWorkspaceMemberCounts()
.should('contain', '1 member');
cy.task('log', 'Verified workspace has 1 member');
// Verify exactly one workspace exists
cy.get('[data-testid="workspace-item"]')
PageUtils.getWorkspaceItems()
.should('have.length', 1);
// Verify workspace name is present

View File

@@ -172,7 +172,7 @@ export class AuthTestUtils {
url: `${this.config.baseUrl}/api/user/verify/${accessToken}`,
failOnStatusCode: false,
}).then((verifyResponse) => {
cy.task('log', `Token verification response: ${JSON.stringify(verifyResponse)}`);
// cy.task('log', `Token verification response: ${JSON.stringify(verifyResponse)}`);
if (verifyResponse.status !== 200) {
throw new Error('Token verification failed');

View File

@@ -2,6 +2,8 @@
// Import auth utilities
import './auth-utils';
// Import page utilities
import './page-utils';
// ***********************************************
// This example commands.ts shows you how to

View File

@@ -0,0 +1,483 @@
/**
* Page utilities for Cypress E2E tests
* Provides reusable functions to interact with page elements using data-testid attributes
*/
export class PageUtils {
// ========== Navigation & Sidebar ==========
/**
* Click the New Page button in the sidebar
*/
static clickNewPageButton() {
return cy.get('[data-testid="new-page-button"]').click();
}
/**
* Get all space items in the outline
*/
static getSpaceItems() {
return cy.get('[data-testid="space-item"]');
}
/**
* Click on a specific space item by index
*/
static clickSpaceItem(index: number = 0) {
return this.getSpaceItems().eq(index).click();
}
/**
* Get a space by its view ID
*/
static getSpaceById(viewId: string) {
return cy.get(`[data-testid="space-${viewId}"]`);
}
/**
* Get all space names
*/
static getSpaceNames() {
return cy.get('[data-testid="space-name"]');
}
/**
* Get a specific space name by text
*/
static getSpaceByName(name: string) {
return cy.get('[data-testid="space-name"]').contains(name);
}
/**
* Click on a space to expand/collapse it
*/
static clickSpace(spaceName?: string) {
if (spaceName) {
return this.getSpaceByName(spaceName).parent().parent().click();
}
return this.getSpaceNames().first().parent().parent().click();
}
// ========== Page Management ==========
/**
* Get all page names in the outline
*/
static getPageNames() {
return cy.get('[data-testid="page-name"]');
}
/**
* Get a specific page by name
*/
static getPageByName(name: string) {
return cy.get('[data-testid="page-name"]').contains(name);
}
/**
* Click on a page by name
*/
static clickPageByName(name: string) {
return this.getPageByName(name).click();
}
/**
* Get a page by its view ID
*/
static getPageById(viewId: string) {
return cy.get(`[data-testid="page-${viewId}"]`);
}
/**
* Get the page title input field (in modal/editor)
*/
static getPageTitleInput() {
return cy.get('[data-testid="page-title-input"]');
}
/**
* Enter a page title in the input field
*/
static enterPageTitle(title: string) {
return this.getPageTitleInput()
.should('be.visible')
.first() // Use first() to ensure we only interact with one element
.focus()
.clear()
.type(title);
}
/**
* Save page title and close modal (press Escape)
*/
static savePageTitle() {
return this.getPageTitleInput().first().type('{esc}');
}
// ========== Page Actions ==========
/**
* Click the More Actions button for the current page
*/
static clickPageMoreActions() {
return cy.get('[data-testid="page-more-actions"]').click();
}
/**
* Click the Delete Page button
*/
static clickDeletePageButton() {
return cy.get('[data-testid="delete-page-button"]').click();
}
/**
* Confirm page deletion in modal (if present)
*/
static confirmPageDeletion() {
return cy.get('body').then($body => {
if ($body.find('[data-testid="delete-page-confirm-modal"]').length > 0) {
cy.get('[data-testid="delete-page-confirm-modal"]').within(() => {
cy.contains('button', 'Delete').click();
});
}
});
}
/**
* Delete a page by name (complete flow)
*/
static deletePageByName(pageName: string) {
this.clickPageByName(pageName);
cy.wait(1000);
this.clickPageMoreActions();
cy.wait(500);
this.clickDeletePageButton();
cy.wait(500);
this.confirmPageDeletion();
return cy.wait(2000);
}
// ========== Modal & Dialog ==========
/**
* Get the modal/dialog element
*/
static getModal() {
return cy.get('[role="dialog"]');
}
/**
* Click Add button in modal
*/
static clickModalAddButton() {
return this.getModal().within(() => {
cy.contains('button', 'Add').click();
});
}
/**
* Select first space in modal and click Add
*/
static selectFirstSpaceInModal() {
return this.getModal().should('be.visible').within(() => {
this.clickSpaceItem(0);
cy.contains('button', 'Add').click();
});
// Note: The dialog doesn't close, it transitions to show the page editor
}
/**
* Select a specific space by name in modal and click Add
*/
static selectSpace(spaceName: string = 'General') {
return this.getModal().should('be.visible').within(($modal) => {
// First check what elements exist in the modal
const spaceNameElements = $modal.find('[data-testid="space-name"]');
const spaceItemElements = $modal.find('[data-testid="space-item"]');
if (spaceNameElements.length > 0) {
// Log all available spaces
cy.task('log', `Looking for space: "${spaceName}"`);
cy.task('log', 'Available spaces with space-name:');
spaceNameElements.each((index, elem) => {
cy.task('log', ` - "${elem.textContent}"`);
});
// Try to find and click the target space
cy.get('[data-testid="space-name"]').contains(spaceName).click();
} else if (spaceItemElements.length > 0) {
// If no space-name elements but space-item elements exist
cy.task('log', `Found ${spaceItemElements.length} space-item elements but no space-name elements`);
// Check if any space-item contains the target space name
let foundSpace = false;
spaceItemElements.each((index, item) => {
if (item.textContent && item.textContent.includes(spaceName)) {
foundSpace = true;
cy.get('[data-testid="space-item"]').eq(index).click();
return false; // break the loop
}
});
if (!foundSpace) {
cy.task('log', `Space "${spaceName}" not found, clicking first space-item as fallback`);
cy.get('[data-testid="space-item"]').first().click();
}
} else {
// Debug: log what's actually in the modal
const allTestIds = $modal.find('[data-testid]');
cy.task('log', 'No space elements found. Available data-testid elements in modal:');
allTestIds.each((index, elem) => {
const testId = elem.getAttribute('data-testid');
if (testId && index < 20) { // Limit output
cy.task('log', ` - ${testId}: "${elem.textContent?.slice(0, 50)}"`);
}
});
// As a last resort, try to find any clickable element that might be a space
cy.task('log', 'Attempting to find any clickable space element...');
// Try to click the first item that looks like it could be a space
cy.get('[role="button"], [role="option"], .clickable, button').first().click();
}
// Click the Add button
cy.contains('button', 'Add').click();
});
// Note: The dialog doesn't close, it transitions to show the page editor
}
// ========== Workspace ==========
/**
* Get the workspace dropdown trigger
*/
static getWorkspaceDropdownTrigger() {
return cy.get('[data-testid="workspace-dropdown-trigger"]');
}
/**
* Click to open workspace dropdown
*/
static openWorkspaceDropdown() {
return this.getWorkspaceDropdownTrigger().click();
}
/**
* Get workspace list container
*/
static getWorkspaceList() {
return cy.get('[data-testid="workspace-list"]');
}
/**
* Get all workspace items
*/
static getWorkspaceItems() {
return cy.get('[data-testid="workspace-item"]');
}
/**
* Get workspace member count elements
*/
static getWorkspaceMemberCounts() {
return cy.get('[data-testid="workspace-member-count"]');
}
/**
* Get user email in dropdown
*/
static getUserEmailInDropdown() {
return cy.get('[data-testid="user-email"]');
}
// ========== Editor & Content ==========
/**
* Get the editor element by view ID
*/
static getEditor(viewId?: string) {
if (viewId) {
return cy.get(`#editor-${viewId}`);
}
// Get any editor (when there's only one)
return cy.get('[id^="editor-"]').first();
}
/**
* Type content in the editor
*/
static typeInEditor(content: string) {
return this.getEditor()
.should('be.visible')
.focus()
.type(content);
}
/**
* Type multiple lines in the editor
*/
static typeMultipleLinesInEditor(lines: string[]) {
return this.getEditor()
.should('be.visible')
.focus()
.then($editor => {
lines.forEach((line, index) => {
if (index > 0) {
cy.wrap($editor).type('{enter}');
}
cy.wrap($editor).type(line);
});
});
}
/**
* Get editor content as text
*/
static getEditorContent() {
return this.getEditor().invoke('text');
}
/**
* Verify editor contains specific text
*/
static verifyEditorContains(text: string) {
return this.getEditor().should('contain', text);
}
/**
* Clear editor content
*/
static clearEditor() {
return this.getEditor()
.focus()
.type('{selectall}{backspace}');
}
// ========== Utility Functions ==========
/**
* Wait for page to load after navigation
*/
static waitForPageLoad(timeout: number = 3000) {
return cy.wait(timeout);
}
/**
* Verify a page exists by name
*/
static verifyPageExists(pageName: string) {
return this.getPageByName(pageName).should('exist');
}
/**
* Verify a page does not exist by name
*/
static verifyPageNotExists(pageName: string) {
return cy.get('body').then($body => {
if ($body.find('[data-testid="page-name"]').length > 0) {
cy.get('[data-testid="page-name"]').each(($el) => {
cy.wrap($el).should('not.contain', pageName);
});
} else {
cy.get('[data-testid="page-name"]').should('not.exist');
}
});
}
/**
* Create a new page with a specific name (complete flow)
*/
static createPage(pageName: string) {
this.clickNewPageButton();
cy.wait(1000);
// Select space in modal
this.getModal().should('be.visible').within(() => {
PageUtils.getSpaceItems().first().click();
cy.contains('button', 'Add').click();
});
cy.wait(2000);
// Enter page title
this.enterPageTitle(pageName);
this.savePageTitle();
return cy.wait(1000);
}
/**
* Expand a space to show its pages
*/
static expandSpace(spaceName?: string) {
return this.clickSpace(spaceName);
}
/**
* Check if a space is expanded by checking if pages are visible
*/
static isSpaceExpanded(spaceName: string) {
return this.getSpaceByName(spaceName)
.parent()
.parent()
.parent()
.find('[data-testid="page-name"]')
.should('be.visible');
}
}
// Export individual utility functions for convenience
export const {
// Navigation
clickNewPageButton,
getSpaceItems,
clickSpaceItem,
getSpaceById,
getSpaceNames,
getSpaceByName,
clickSpace,
// Page Management
getPageNames,
getPageByName,
clickPageByName,
getPageById,
getPageTitleInput,
enterPageTitle,
savePageTitle,
// Page Actions
clickPageMoreActions,
clickDeletePageButton,
confirmPageDeletion,
deletePageByName,
// Modal
getModal,
clickModalAddButton,
selectFirstSpaceInModal,
selectSpace,
// Workspace
getWorkspaceDropdownTrigger,
openWorkspaceDropdown,
getWorkspaceList,
getWorkspaceItems,
getWorkspaceMemberCounts,
getUserEmailInDropdown,
// Editor
getEditor,
typeInEditor,
typeMultipleLinesInEditor,
getEditorContent,
verifyEditorContains,
clearEditor,
// Utilities
waitForPageLoad,
verifyPageExists,
verifyPageNotExists,
createPage,
expandSpace,
isSpaceExpanded,
} = PageUtils;

View File

@@ -22,7 +22,8 @@
"test:cy": "cypress run",
"test:integration": "cypress run --spec 'cypress/e2e/**/*.cy.ts'",
"test:integration:user": "cypress run --spec 'cypress/e2e/user/**/*.cy.ts' --headed --browser chrome",
"test:integration:page": "cypress run --spec 'cypress/e2e/page/**/*.cy.ts' --headed --browser chrome",
"test:integration:page:create-delete": "cypress run --spec 'cypress/e2e/page/create-delete-page.cy.ts' --headed --browser chrome",
"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",
"coverage": "cross-env COVERAGE=true pnpm run test:unit && cross-env COVERAGE=true pnpm run test:components",

View File

@@ -124,6 +124,7 @@ const EditorEditable = () => {
<ErrorBoundary fallbackRender={ElementFallbackRender}>
<Editable
role={'textbox'}
data-testid={'editor-content'}
decorate={(entry: NodeEntry) => {
const codeDecoration = codeDecorate?.(entry);
const decoration = decorate(entry);