Files
AppFlowy-Web/cypress/support/page-utils.ts
2025-08-15 10:38:15 +08:00

483 lines
12 KiB
TypeScript

/**
* 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;