Files
AppFlowy-Web/cypress/support/selectors.ts
Nathan.fooo 95a596e651 Fix namespace popover (#172)
* fix: open publish manage modal after closing share popover

* chore: update log
2025-11-26 15:50:40 +08:00

696 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Centralized selectors for Cypress E2E tests
* This file encapsulates all data-testid selectors to avoid hardcoding them in tests
*/
/**
* Helper function to create a data-testid selector
*/
export function byTestId(id: string): string {
return `[data-testid="${id}"]`;
}
/**
* Helper for selectors that match data-testid prefixes or substrings
*/
export function byTestIdPrefix(prefix: string): string {
return `[data-testid^="${prefix}"]`;
}
export function byTestIdContains(fragment: string): string {
return `[data-testid*="${fragment}"]`;
}
/**
* Page-related selectors
*/
export const PageSelectors = {
// Get all page items
items: () => cy.get(byTestId('page-item')),
// Get all page names
names: () => cy.get(byTestId('page-name')),
// Get page name containing specific text
nameContaining: (text: string) => cy.get(byTestId('page-name')).contains(text),
// Get page item containing specific page name
itemByName: (pageName: string) => {
return cy.get(byTestId('page-name'))
.contains(pageName)
.first()
.closest(byTestId('page-item'));
},
// Get more actions button for a specific page
moreActionsButton: (pageName?: string) => {
if (pageName) {
return PageSelectors.itemByName(pageName)
.find(byTestId('page-more-actions'))
.first(); // Ensure we only get one button even if multiple exist
}
return cy.get(byTestId('page-more-actions'));
},
// Get new page button
newPageButton: () => cy.get(byTestId('new-page-button')),
// Get page title input
titleInput: () => cy.get(byTestId('page-title-input')),
};
/**
* Space-related selectors
*/
export const SpaceSelectors = {
// Get all space items
items: () => cy.get(byTestId('space-item')),
// Get all space names
names: () => cy.get(byTestId('space-name')),
// Get space expanded indicator
expanded: () => cy.get(byTestId('space-expanded')),
// Get space by name
itemByName: (spaceName: string) => {
return cy.get(byTestId('space-name'))
.contains(spaceName)
.closest(byTestId('space-item'));
},
// Get more actions button for spaces
moreActionsButton: () => cy.get(byTestId('inline-more-actions')),
// New space creation controls
createNewSpaceButton: () => cy.get(byTestId('create-new-space-button')),
createSpaceModal: () => cy.get(byTestId('create-space-modal')),
spaceNameInput: () => cy.get(byTestId('space-name-input')),
};
/**
* Breadcrumb selectors
*/
export const BreadcrumbSelectors = {
navigation: () => cy.get(byTestId('breadcrumb-navigation')),
items: () => cy.get(byTestIdContains('breadcrumb-item-')),
};
/**
* View actions popover selectors
*/
export const ViewActionSelectors = {
// Get the popover container
popover: () => cy.get(byTestId('view-actions-popover')),
// Get delete action button
deleteButton: () => cy.get(byTestId('view-action-delete')),
// Get rename action button
renameButton: () => cy.get(byTestId('more-page-rename')),
// Get change icon action button
changeIconButton: () => cy.get(byTestId('more-page-change-icon')),
// Get open in new tab action button
openNewTabButton: () => cy.get(byTestId('more-page-open-new-tab')),
// Get duplicate button
duplicateButton: () => cy.get(byTestId('more-page-duplicate')),
// Get move to button
moveToButton: () => cy.get(byTestId('more-page-move-to')),
};
/**
* Modal-related selectors
*/
export const ModalSelectors = {
// Get confirm delete button (in delete confirmation modal)
confirmDeleteButton: () => cy.get(byTestId('confirm-delete-button')),
// Get delete page confirmation modal
deletePageModal: () => cy.get(byTestId('delete-page-confirm-modal')),
// Get new page modal
newPageModal: () => cy.get(byTestId('new-page-modal')),
// Get space item in modal
spaceItemInModal: () => cy.get(byTestId('space-item')),
// Generic modal accept/ok button
okButton: () => cy.get(byTestId('modal-ok-button')),
// Rename modal inputs
renameInput: () => cy.get(byTestId('rename-modal-input')),
renameSaveButton: () => cy.get(byTestId('rename-modal-save')),
};
/**
* Helper function to trigger hover on an element to show hidden actions
*/
export function hoverToShowActions(element: Cypress.Chainable) {
return element
.trigger('mouseenter', { force: true })
.trigger('mouseover', { force: true });
}
/**
* Share/Publish-related selectors
*/
export const ShareSelectors = {
// Share button - use first() since there might be multiple share buttons in the UI
shareButton: () => cy.get(byTestId('share-button')).first(),
// Share popover
sharePopover: () => cy.get(byTestId('share-popover')),
// Publish tab button
publishTabButton: () => cy.get(byTestId('publish-tab-button')),
// Publish switch
publishSwitch: () => cy.get(byTestId('publish-switch')),
// Publish URL input
publishUrlInput: () => cy.get(byTestId('publish-url-input')),
// Publish namespace and name inputs
publishNamespace: () => cy.get(byTestId('publish-namespace')),
publishNameInput: () => cy.get(byTestId('publish-name-input')),
openPublishSettingsButton: () => cy.get(byTestId('open-publish-settings')),
// Page settings button
pageSettingsButton: () => cy.get(byTestId('page-settings-button')),
// Publish settings tab
publishSettingsTab: () => cy.get(byTestId('publish-settings-tab')),
// Unpublish button
unpublishButton: () => cy.get(byTestId('unpublish-button')),
// Confirm unpublish button
confirmUnpublishButton: () => cy.get(byTestId('confirm-unpublish-button')),
// Publish confirm button (the main publish button)
publishConfirmButton: () => cy.get(byTestId('publish-confirm-button')),
// Visit Site button
visitSiteButton: () => cy.get(byTestId('visit-site-button')),
publishManageModal: () => cy.get(byTestId('publish-manage-modal')),
publishManagePanel: () => cy.get(byTestId('publish-manage-panel')),
};
/**
* Workspace-related selectors
*/
export const WorkspaceSelectors = {
// Workspace dropdown trigger
dropdownTrigger: () => cy.get(byTestId('workspace-dropdown-trigger')),
// Workspace dropdown content
dropdownContent: () => cy.get(byTestId('workspace-dropdown-content')),
// Workspace item
item: () => cy.get(byTestId('workspace-item')),
// Workspace item name
itemName: () => cy.get(byTestId('workspace-item-name')),
// Workspace member count
memberCount: () => cy.get(byTestId('workspace-member-count')),
};
/**
* Sidebar-related selectors
*/
export const SidebarSelectors = {
// Sidebar page header
pageHeader: () => cy.get(byTestId('sidebar-page-header')),
};
/**
* Trash view selectors
*/
export const TrashSelectors = {
sidebarTrashButton: () => cy.get(byTestId('sidebar-trash-button')),
table: () => cy.get(byTestId('trash-table')),
rows: () => cy.get(byTestId('trash-table-row')),
restoreButton: () => cy.get(byTestId('trash-restore-button')),
deleteButton: () => cy.get(byTestId('trash-delete-button')),
};
/**
* Chat Model Selector-related selectors
* Used for testing AI model selection in chat interface
*/
export const ModelSelectorSelectors = {
// Model selector button
button: () => cy.get(byTestId('model-selector-button')),
// Model search input
searchInput: () => cy.get(byTestId('model-search-input')),
// Get all model options
options: () => cy.get('[data-testid^="model-option-"]'),
// Get specific model option by name
optionByName: (modelName: string) => cy.get(byTestId(`model-option-${modelName}`)),
// Get selected model option (has the selected class)
selectedOption: () => cy.get('[data-testid^="model-option-"]').filter('.bg-fill-content-select'),
};
/**
* Chat UI selectors
*/
export const ChatSelectors = {
aiChatContainer: () => cy.get(byTestId('ai-chat-container')),
formatToggle: () => cy.get(byTestId('chat-input-format-toggle')),
formatGroup: () => cy.get(byTestId('chat-format-group')),
browsePromptsButton: () => cy.get(byTestId('chat-input-browse-prompts')),
relatedViewsButton: () => cy.get(byTestId('chat-input-related-views')),
relatedViewsPopover: () => cy.get(byTestId('chat-related-views-popover')),
sendButton: () => cy.get(byTestId('chat-input-send')),
};
/**
* Database Grid-related selectors
*/
export const DatabaseGridSelectors = {
// Main grid container
grid: () => cy.get(byTestId('database-grid')),
// Grid rows
rows: () => cy.get('[data-testid^="grid-row-"]'),
// Get specific row by row ID
rowById: (rowId: string) => cy.get(byTestId(`grid-row-${rowId}`)),
// Get first row
firstRow: () => cy.get('[data-testid^="grid-row-"]').first(),
// Grid cells
cells: () => cy.get('[data-testid^="grid-cell-"]'),
// Get specific cell by row ID and field ID
cellByIds: (rowId: string, fieldId: string) => cy.get(byTestId(`grid-cell-${rowId}-${fieldId}`)),
// Get all cells in a specific row
cellsInRow: (rowId: string) => cy.get(`[data-testid^="grid-cell-${rowId}-"]`),
// Get first cell
firstCell: () => cy.get('[data-testid^="grid-cell-"]').first(),
// Get new row button (if exists)
newRowButton: () => cy.get(byTestId('grid-new-row')),
};
/**
* Database View selectors
*/
export const DatabaseViewSelectors = {
// View tabs
viewTab: (viewId?: string) => viewId ? cy.get(byTestId(`view-tab-${viewId}`)) : cy.get('[data-testid^="view-tab-"]'),
// Active view tab
activeViewTab: () => cy.get('[data-testid^="view-tab-"][data-state="active"]'),
// View name input
viewNameInput: () => cy.get(byTestId('view-name-input')),
// Add view button (plus button)
addViewButton: () => cy.get(byTestId('add-view-button')), // Note: Check if this ID exists, otherwise might need to use the button containing "+" logic or add ID to code
// View type selection in dropdown
viewTypeOption: (type: string) => cy.contains(type), // Usually text based in dropdown
// Grid view container
gridView: () => cy.get(byTestId('grid-view')),
// Board view container
boardView: () => cy.get('[data-testid*="board"]'), // Using wildcard as specific ID might vary
// Calendar view container
calendarView: () => cy.get('[data-testid*="calendar"]'),
};
/**
* Database Filter & Sort selectors
*/
export const DatabaseFilterSelectors = {
// Filter button (opens filter menu)
filterButton: () => cy.get(byTestId('database-actions-filter')),
// Add filter button (plus button in DatabaseConditions area to add new filter condition)
addFilterButton: () => cy.get(byTestId('database-add-filter-button')),
// Sort button
sortButton: () => cy.get(byTestId('database-actions-sort')),
// Filter condition row
filterCondition: () => cy.get(byTestId('database-filter-condition')),
// Sort condition row
sortCondition: () => cy.get(byTestId('database-sort-condition')),
// Remove filter button (inside condition)
removeFilterButton: () => cy.get('button[aria-label*="remove"], button[aria-label*="delete"], button:contains("×"), svg[class*="close"], svg[class*="x"]').first(),
// Filter input
filterInput: () => cy.get(byTestId('text-filter-input')),
};
/**
* Slash Command selectors
*/
export const SlashCommandSelectors = {
// Slash panel
slashPanel: () => cy.get(byTestId('slash-panel')),
// Slash menu item
slashMenuItem: (name: string) => cy.get('[data-testid^="slash-menu-"]').filter(`:contains("${name}")`),
// Database selection modal (legacy - kept for backward compatibility)
promptModal: () => cy.get(byTestId('prompt-modal')),
// Search input in popover/modal
searchInput: () => cy.get('input[placeholder*="Search"]'),
// Select database from linked database picker
selectDatabase: (dbName?: string) => {
// Wait for the popover to appear
cy.contains('Link to an existing database', { timeout: 10000 }).should('be.visible');
// Wait for loading to complete if present
cy.get('body').then(($body) => {
if ($body.text().includes('Loading...')) {
cy.contains('Loading...', { timeout: 15000 }).should('not.exist');
}
});
// Find the MUI Popover paper element and interact with it
cy.get('.MuiPopover-paper').last().should('be.visible').within(() => {
if (dbName) {
// Try to search for specific database with retry logic
cy.get('input[placeholder*="Search"]').should('be.visible').clear().type(dbName);
// Retry mechanism: wait and check if database appears (up to 5 attempts)
let attempts = 0;
const maxAttempts = 5;
const checkDatabase = () => {
attempts++;
cy.task('log', `[selectDatabase] Attempt ${attempts}/${maxAttempts} to find database "${dbName}"`);
waitForReactUpdate(2000);
cy.get('[class*="appflowy-scrollbar"]').then(($area) => {
const areaText = $area.text();
// Check if we have "No databases found" message
if (areaText.includes('No databases found')) {
if (attempts < maxAttempts) {
cy.task('log', `[selectDatabase] No databases found, retrying... (attempt ${attempts})`);
waitForReactUpdate(3000); // Wait longer before retry
checkDatabase();
return;
} else {
cy.task('log', '[selectDatabase] No databases found after all retries');
throw new Error('No databases available to select');
}
}
if (areaText.includes(dbName)) {
// Database found by name, find the span and click its parent div
cy.task('log', `[selectDatabase] Database "${dbName}" found, selecting it`);
cy.contains('span', dbName).parent('div').click({ force: true });
} else {
// Database not found yet, retry if we haven't exceeded max attempts
if (attempts < maxAttempts) {
cy.task('log', `[selectDatabase] Database "${dbName}" not found yet, retrying... (attempt ${attempts})`);
waitForReactUpdate(3000); // Wait longer before retry
checkDatabase();
} else {
// After all retries, select first available database
cy.task('log', `[selectDatabase] Database "${dbName}" not found after ${maxAttempts} attempts, selecting first available`);
cy.get('[class*="appflowy-scrollbar"]').within(() => {
cy.get('span').then(($spans) => {
const $dbSpan = Array.from($spans).find((span) => {
const text = span.textContent?.trim() || '';
return /Grid|View|Database|Kanban|Calendar/i.test(text) &&
text.length > 0 &&
!text.includes('Link to an existing database');
});
if ($dbSpan) {
cy.wrap($dbSpan).parent('div').click({ force: true });
} else {
cy.get('div').first().click({ force: true });
}
});
});
}
}
});
};
checkDatabase();
} else {
// No name provided, select first available database
waitForReactUpdate(2000);
cy.get('[class*="appflowy-scrollbar"]').within(() => {
cy.get('span').then(($spans) => {
const $dbSpan = Array.from($spans).find((span) => {
const text = span.textContent?.trim() || '';
return /Grid|View|Database|Kanban|Calendar/i.test(text) &&
text.length > 0 &&
!text.includes('Link to an existing database');
});
if ($dbSpan) {
cy.wrap($dbSpan).parent('div').click({ force: true });
} else {
cy.get('div').first().click({ force: true });
}
});
});
}
});
},
};
/**
* Single Select Column selectors
*/
export const SingleSelectSelectors = {
// Select option cell by row and field ID
selectOptionCell: (rowId: string, fieldId: string) => cy.get(byTestId(`select-option-cell-${rowId}-${fieldId}`)),
// All select option cells
allSelectOptionCells: () => cy.get('[data-testid^="select-option-cell-"]'),
// Select option in dropdown by option ID
selectOption: (optionId: string) => cy.get(byTestId(`select-option-${optionId}`)),
// Select option menu popover
selectOptionMenu: () => cy.get(byTestId('select-option-menu')),
};
/**
* Grid Field/Column Header selectors
*/
export const GridFieldSelectors = {
// Field header by field ID
fieldHeader: (fieldId: string) => cy.get(byTestId(`grid-field-header-${fieldId}`)),
// All field headers
allFieldHeaders: () => cy.get('[data-testid^="grid-field-header-"]'),
// Add select option button
addSelectOptionButton: () => cy.get(byTestId('add-select-option')),
};
/**
* Add Page Actions selectors
*/
export const AddPageSelectors = {
// Inline add page button
inlineAddButton: () => cy.get(byTestId('inline-add-page')),
// Add grid button in dropdown
addGridButton: () => cy.get(byTestId('add-grid-button')),
// Add AI chat button in dropdown
addAIChatButton: () => cy.get(byTestId('add-ai-chat-button')),
};
/**
* Checkbox Column selectors
*/
export const CheckboxSelectors = {
// Checkbox cell by row and field ID
checkboxCell: (rowId: string, fieldId: string) => cy.get(byTestId(`checkbox-cell-${rowId}-${fieldId}`)),
// All checkbox cells
allCheckboxCells: () => cy.get('[data-testid^="checkbox-cell-"]'),
// Checked icon
checkedIcon: () => cy.get(byTestId('checkbox-checked-icon')),
// Unchecked icon
uncheckedIcon: () => cy.get(byTestId('checkbox-unchecked-icon')),
// Get checkbox cell by checked state
checkedCells: () => cy.get('[data-checked="true"]'),
uncheckedCells: () => cy.get('[data-checked="false"]'),
};
/**
* Editor-related selectors
*/
export const EditorSelectors = {
// Main Slate editor
slateEditor: () => cy.get('[data-slate-editor="true"]'),
// Get first editor
firstEditor: () => cy.get('[data-slate-editor="true"]').first(),
// Get editor with specific content
editorWithText: (text: string) => cy.get('[data-slate-editor="true"]').contains(text),
// Selection toolbar
selectionToolbar: () => cy.get('[data-testid="selection-toolbar"]'),
// Formatting buttons in toolbar
boldButton: () => cy.get(byTestId('toolbar-bold-button')),
italicButton: () => cy.get(byTestId('toolbar-italic-button')),
underlineButton: () => cy.get(byTestId('toolbar-underline-button')),
strikethroughButton: () => cy.get(byTestId('toolbar-strikethrough-button')),
codeButton: () => cy.get(byTestId('toolbar-code-button')),
};
/**
* Helper function to wait for React to re-render after state changes
*/
/**
* DateTime Column selectors
*/
export const DateTimeSelectors = {
// DateTime cell by row and field ID
dateTimeCell: (rowId: string, fieldId: string) => cy.get(byTestId(`datetime-cell-${rowId}-${fieldId}`)),
// All datetime cells
allDateTimeCells: () => cy.get('[data-testid^="datetime-cell-"]'),
// DateTime picker popover
dateTimePickerPopover: () => cy.get(byTestId('datetime-picker-popover')),
// DateTime date input field
dateTimeDateInput: () => cy.get(byTestId('datetime-date-input')),
// DateTime time input field
dateTimeTimeInput: () => cy.get(byTestId('datetime-time-input')),
};
/**
* Property Menu selectors
*/
export const PropertyMenuSelectors = {
// Property type trigger button
propertyTypeTrigger: () => cy.get(byTestId('property-type-trigger')),
// Property type option by field type number
propertyTypeOption: (fieldType: number) => cy.get(byTestId(`property-type-option-${fieldType}`)),
// Grid new property button
newPropertyButton: () => cy.get(byTestId('grid-new-property-button')),
// Edit property menu item
editPropertyMenuItem: () => cy.get(byTestId('grid-field-edit-property')),
};
/**
* Field Types enum for database columns
*/
export const FieldType = {
RichText: 0,
Number: 1,
DateTime: 2,
SingleSelect: 3,
MultiSelect: 4,
Checkbox: 5,
URL: 6,
Checklist: 7,
LastEditedTime: 8,
CreatedTime: 9,
Relation: 10,
AISummaries: 11,
AITranslations: 12,
FileMedia: 14
};
/**
* Database Row Controls selectors
*/
export const RowControlsSelectors = {
// Row accessory button (appears on hover)
rowAccessoryButton: () => cy.get(byTestId('row-accessory-button')),
// Row menu items
rowMenuDuplicate: () => cy.get(byTestId('row-menu-duplicate')),
rowMenuInsertAbove: () => cy.get(byTestId('row-menu-insert-above')),
rowMenuInsertBelow: () => cy.get(byTestId('row-menu-insert-below')),
rowMenuDelete: () => cy.get(byTestId('row-menu-delete')),
// Delete confirmation
deleteRowConfirmButton: () => cy.get(byTestId('delete-row-confirm-button')),
};
/**
* Authentication-related selectors
* Used for login/logout flow testing
*/
export const AuthSelectors = {
// Login page elements
emailInput: () => cy.get(byTestId('login-email-input')),
magicLinkButton: () => cy.get(byTestId('login-magic-link-button')),
enterCodeManuallyButton: () => cy.get(byTestId('enter-code-manually-button')),
otpCodeInput: () => cy.get(byTestId('otp-code-input')),
otpSubmitButton: () => cy.get(byTestId('otp-submit-button')),
// Password sign-in button
passwordSignInButton: () => cy.get(byTestId('login-password-button')),
// Password page elements
passwordInput: () => cy.get(byTestId('password-input')),
passwordSubmitButton: () => cy.get(byTestId('password-submit-button')),
// Logout elements
logoutMenuItem: () => cy.get(byTestId('logout-menu-item')),
logoutConfirmButton: () => cy.get(byTestId('logout-confirm-button')),
};
/**
* Account settings selectors
*/
export const AccountSelectors = {
settingsButton: () => cy.get(byTestId('account-settings-button')),
settingsDialog: () => cy.get(byTestId('account-settings-dialog')),
dateFormatDropdown: () => cy.get(byTestId('date-format-dropdown')),
dateFormatOptionYearMonthDay: () => cy.get(byTestId('date-format-1')),
timeFormatDropdown: () => cy.get(byTestId('time-format-dropdown')),
timeFormatOption24: () => cy.get(byTestId('time-format-1')),
startWeekDropdown: () => cy.get(byTestId('start-week-on-dropdown')),
startWeekMonday: () => cy.get(byTestId('start-week-1')),
};
/**
* Avatar display selectors
*/
export const AvatarUiSelectors = {
image: () => cy.get(byTestId('avatar-image')),
};
export function waitForReactUpdate(ms: number = 500) {
return cy.wait(ms);
}