mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
995 lines
33 KiB
TypeScript
995 lines
33 KiB
TypeScript
/**
|
|
* Centralized selectors for Cypress E2E tests
|
|
* This file encapsulates all data-testid selectors to avoid hardcoding them in tests
|
|
*/
|
|
|
|
// Re-export FieldType from the source to avoid duplication
|
|
export { FieldType } from '../../src/application/database-yjs/database.type';
|
|
|
|
/**
|
|
* 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}"]`;
|
|
}
|
|
|
|
type CypressGetOptions = Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>;
|
|
|
|
/**
|
|
* Extracts a viewId from a sidebar page item test id (e.g. "page-<viewId>").
|
|
*/
|
|
export function viewIdFromPageTestId(testId: string | null | undefined): string {
|
|
if (!testId || !testId.startsWith('page-')) {
|
|
throw new Error(`Expected data-testid to start with "page-" but got: ${String(testId)}`);
|
|
}
|
|
|
|
return testId.slice('page-'.length);
|
|
}
|
|
|
|
/**
|
|
* Page-related selectors
|
|
*/
|
|
export const PageSelectors = {
|
|
// Get all page items
|
|
items: (options?: CypressGetOptions) => cy.get(byTestId('page-item'), options),
|
|
|
|
// Get all page names
|
|
names: (options?: CypressGetOptions) => cy.get(byTestId('page-name'), options),
|
|
|
|
// Get page row by view id (clickable container in the sidebar list)
|
|
pageByViewId: (viewId: string, options?: CypressGetOptions) => {
|
|
return cy.get(byTestId(`page-${viewId}`), options).first();
|
|
},
|
|
|
|
// Get page item by view id
|
|
itemByViewId: (viewId: string, options?: CypressGetOptions) => {
|
|
return PageSelectors.pageByViewId(viewId, options).closest(byTestId('page-item'));
|
|
},
|
|
|
|
// Get page name containing specific text
|
|
nameContaining: (text: string, options?: CypressGetOptions) => cy.get(byTestId('page-name'), options).contains(text),
|
|
|
|
// Get page item containing specific page name
|
|
itemByName: (pageName: string, options?: CypressGetOptions) => {
|
|
return cy.get(byTestId('page-name'), options).contains(pageName).first().closest(byTestId('page-item'));
|
|
},
|
|
|
|
// Get the first child viewId for a page (e.g. database container -> first database view)
|
|
firstChildViewIdByName: (pageName: string) =>
|
|
PageSelectors.itemByName(pageName)
|
|
.find(byTestId('page-item'))
|
|
.first()
|
|
.children()
|
|
.first()
|
|
.invoke('attr', 'data-testid')
|
|
.then((testId) => viewIdFromPageTestId(testId)),
|
|
|
|
// 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: (options?: CypressGetOptions) => cy.get(byTestId('new-page-button'), options),
|
|
|
|
// Get page title input
|
|
titleInput: (options?: CypressGetOptions) => cy.get(byTestId('page-title-input'), options),
|
|
};
|
|
|
|
/**
|
|
* Page Icon-related selectors
|
|
* Used for testing page icon upload and display
|
|
*/
|
|
export const PageIconSelectors = {
|
|
// Page icon wrapper in sidebar (clickable to open popover)
|
|
pageIcon: () => cy.get(byTestId('page-icon')),
|
|
|
|
// Page icon image (for URL type icons)
|
|
pageIconImage: () => cy.get(byTestId('page-icon-image')),
|
|
|
|
// View meta hover area (hover to show Add icon button)
|
|
viewMetaHoverArea: () => cy.get(byTestId('view-meta-hover-area')),
|
|
|
|
// Add icon button in document header
|
|
addIconButton: () => cy.get(byTestId('add-icon-button')),
|
|
|
|
// Icon popover tabs
|
|
iconPopoverTabEmoji: () => cy.get(byTestId('icon-popover-tab-emoji')),
|
|
iconPopoverTabIcon: () => cy.get(byTestId('icon-popover-tab-icon')),
|
|
iconPopoverTabUpload: () => cy.get(byTestId('icon-popover-tab-upload')),
|
|
|
|
// File dropzone for image upload
|
|
fileDropzone: () => cy.get(byTestId('file-dropzone')),
|
|
};
|
|
|
|
/**
|
|
* 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, options?: CypressGetOptions) => {
|
|
return cy.get(byTestId('space-name'), options).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')),
|
|
|
|
// Generic dialog selectors
|
|
dialogContainer: () => cy.get('.MuiDialog-container'),
|
|
dialogRole: () => cy.get('[role="dialog"]'),
|
|
addButton: () => cy.contains('button', 'Add'),
|
|
};
|
|
|
|
/**
|
|
* Dropdown/Menu selectors
|
|
*/
|
|
export const DropdownSelectors = {
|
|
content: (options?: any) => cy.get('[data-slot="dropdown-menu-content"]', options),
|
|
menu: (options?: any) => cy.get('[role="menu"]', options),
|
|
menuItem: (options?: any) => cy.get('[role="menuitem"]', options),
|
|
};
|
|
|
|
/**
|
|
* 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')),
|
|
|
|
// Share inputs
|
|
emailTagInput: () => cy.get('[data-slot="email-tag-input"]'),
|
|
inviteButton: () => cy.contains('button', /invite/i),
|
|
|
|
// 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')),
|
|
|
|
// Edit namespace button
|
|
editNamespaceButton: () => cy.get(byTestId('edit-namespace-button')),
|
|
|
|
// Homepage setting (only visible when namespace is not a UUID)
|
|
homePageSetting: () => cy.get(byTestId('homepage-setting')),
|
|
|
|
// Homepage upgrade button (visible for Free plan users on official hosts)
|
|
homePageUpgradeButton: () => cy.get(byTestId('homepage-upgrade-button')),
|
|
};
|
|
|
|
/**
|
|
* 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: (options?: CypressGetOptions) => cy.get(byTestId('sidebar-page-header'), options),
|
|
};
|
|
|
|
/**
|
|
* App Header selectors (top bar)
|
|
*/
|
|
export const HeaderSelectors = {
|
|
// Header container
|
|
container: (options?: CypressGetOptions) => cy.get('.appflowy-top-bar', options),
|
|
|
|
// More actions button in the header (not sidebar)
|
|
moreActionsButton: (options?: CypressGetOptions) =>
|
|
cy.get('.appflowy-top-bar').find(byTestId('page-more-actions'), options),
|
|
};
|
|
|
|
/**
|
|
* 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')),
|
|
cell: () => cy.get('td'),
|
|
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: (options?: CypressGetOptions) => cy.get(byTestId('ai-chat-container'), options),
|
|
formatToggle: (options?: CypressGetOptions) => cy.get(byTestId('chat-input-format-toggle'), options),
|
|
formatGroup: (options?: CypressGetOptions) => cy.get(byTestId('chat-format-group'), options),
|
|
browsePromptsButton: (options?: CypressGetOptions) => cy.get(byTestId('chat-input-browse-prompts'), options),
|
|
relatedViewsButton: (options?: CypressGetOptions) => cy.get(byTestId('chat-input-related-views'), options),
|
|
relatedViewsPopover: (options?: CypressGetOptions) => cy.get(byTestId('chat-related-views-popover'), options),
|
|
sendButton: (options?: CypressGetOptions) => cy.get(byTestId('chat-input-send'), options),
|
|
};
|
|
|
|
/**
|
|
* 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(),
|
|
|
|
// Get data rows only (excludes header, new-row, calculate-row with undefined IDs)
|
|
dataRows: () => cy.get('[data-testid^="grid-row-"]:not([data-testid="grid-row-undefined"])'),
|
|
|
|
// 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 all cells for a specific field (column) by field ID
|
|
cellsForField: (fieldId: string) => cy.get(`[data-testid$="-${fieldId}"][data-testid^="grid-cell-"]`),
|
|
|
|
// Get clickable row cell wrappers for a field (DATA ROWS ONLY)
|
|
// These have data-column-id={fieldId} and contain the onClick handler
|
|
dataRowCellsForField: (fieldId: string, options?: CypressGetOptions) =>
|
|
cy.get(
|
|
`[data-testid^="grid-row-"]:not([data-testid="grid-row-undefined"]) .grid-row-cell[data-column-id="${fieldId}"]`,
|
|
options
|
|
),
|
|
|
|
// 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"]'),
|
|
|
|
// Tab context menu actions
|
|
tabActionRename: () => cy.get(byTestId('database-view-action-rename')),
|
|
tabActionDelete: () => cy.get(byTestId('database-view-action-delete')),
|
|
|
|
// Delete view confirm dialog button
|
|
deleteViewConfirmButton: () => cy.get(byTestId('database-view-delete-confirm')),
|
|
|
|
// 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/chip
|
|
filterCondition: () => cy.get(byTestId('database-filter-condition')),
|
|
|
|
// Sort condition row
|
|
sortCondition: () => cy.get(byTestId('database-sort-condition')),
|
|
|
|
// Delete filter button (inside filter menu)
|
|
deleteFilterButton: () => cy.get(byTestId('delete-filter-button')),
|
|
|
|
// Filter input (text/number)
|
|
filterInput: () => cy.get(byTestId('text-filter-input')),
|
|
|
|
// Text filter container
|
|
textFilter: () => cy.get(byTestId('text-filter')),
|
|
|
|
// Filter condition option by value (0-7 for text, 0-7 for number, etc.)
|
|
filterConditionOption: (conditionValue: number) => cy.get(byTestId(`filter-condition-${conditionValue}`)),
|
|
|
|
// Property/field item in filter selection popover
|
|
propertyItem: (fieldId: string) => cy.get(`[data-item-id="${fieldId}"]`),
|
|
|
|
// Property/field item by name (searches within popover)
|
|
propertyItemByName: (name: string) => cy.contains('[data-item-id]', name),
|
|
};
|
|
|
|
/**
|
|
* 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}")`),
|
|
|
|
heading1: () => cy.get(byTestId('slash-menu-heading1')),
|
|
bulletedList: () => cy.get(byTestId('slash-menu-bulletedList')),
|
|
|
|
// 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')),
|
|
};
|
|
|
|
/**
|
|
* Person Column selectors
|
|
*/
|
|
export const PersonSelectors = {
|
|
// Person cell by row and field ID
|
|
personCell: (rowId: string, fieldId: string) => cy.get(byTestId(`person-cell-${rowId}-${fieldId}`)),
|
|
|
|
// All person cells
|
|
allPersonCells: () => cy.get('[data-testid^="person-cell-"]'),
|
|
|
|
// Person cell menu popover
|
|
personCellMenu: () => cy.get(byTestId('person-cell-menu')),
|
|
|
|
// Notify assignee toggle row
|
|
notifyAssigneeToggle: () => cy.get(byTestId('person-cell-menu')).find('[role="switch"]'),
|
|
|
|
// Person option in dropdown
|
|
personOption: (personId: string) => cy.get(byTestId(`person-option-${personId}`)),
|
|
};
|
|
|
|
/**
|
|
* 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 calendar button in dropdown
|
|
addCalendarButton: () => cy.get(byTestId('add-calendar-button')),
|
|
|
|
// Add board button in dropdown
|
|
addBoardButton: () => cy.get(byTestId('add-board-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')),
|
|
linkButton: () => cy.get(byTestId('link-button')),
|
|
textColorButton: () => cy.get(byTestId('text-color-button')),
|
|
bgColorButton: () => cy.get(byTestId('bg-color-button')),
|
|
headingButton: () => cy.get(byTestId('heading-button')),
|
|
heading1Button: () => cy.get(byTestId('heading-1-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')),
|
|
};
|
|
|
|
|
|
/**
|
|
* 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')),
|
|
|
|
// Create account link on login page
|
|
createAccountButton: () => cy.get(byTestId('login-create-account-button')),
|
|
|
|
// Logout elements
|
|
logoutMenuItem: () => cy.get(byTestId('logout-menu-item')),
|
|
logoutConfirmButton: () => cy.get(byTestId('logout-confirm-button')),
|
|
};
|
|
|
|
/**
|
|
* Sign Up page selectors
|
|
* Used for sign-up flow testing
|
|
*/
|
|
export const SignUpSelectors = {
|
|
// Sign-up form elements
|
|
emailInput: () => cy.get(byTestId('signup-email-input')),
|
|
passwordInput: () => cy.get(byTestId('signup-password-input')),
|
|
confirmPasswordInput: () => cy.get(byTestId('signup-confirm-password-input')),
|
|
submitButton: () => cy.get(byTestId('signup-submit-button')),
|
|
|
|
// Navigation
|
|
backToLoginButton: () => cy.get(byTestId('signup-back-to-login-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')),
|
|
};
|
|
|
|
/**
|
|
* Block-related selectors
|
|
*/
|
|
export const BlockSelectors = {
|
|
dragHandle: () => cy.get(byTestId('drag-block')),
|
|
hoverControls: () => cy.get(byTestId('hover-controls')),
|
|
slashMenuGrid: () => cy.get(byTestId('slash-menu-grid')),
|
|
blockByType: (type: string) => cy.get(`[data-block-type="${type}"]`),
|
|
blockSelector: (type: string) => `[data-block-type="${type}"]`,
|
|
allBlocks: () => cy.get('[data-block-type]'),
|
|
};
|
|
|
|
/**
|
|
* Sort selectors
|
|
*/
|
|
export const SortSelectors = {
|
|
// Sort button in database actions toolbar
|
|
sortButton: () => cy.get(byTestId('database-actions-sort')),
|
|
|
|
// Sort condition chip (shown when sorts are active)
|
|
sortCondition: () => cy.get(byTestId('database-sort-condition')),
|
|
|
|
// Individual sort item in the sort menu
|
|
sortItem: () => cy.get(byTestId('sort-condition')),
|
|
|
|
// Add sort button in sort menu
|
|
addSortButton: () => cy.contains('button', /add.*sort/i),
|
|
|
|
// Delete all sorts button
|
|
deleteAllSortsButton: () => cy.contains('button', /delete.*all.*sort/i),
|
|
};
|
|
|
|
/**
|
|
* Calendar selectors using FullCalendar's class names
|
|
*/
|
|
export const CalendarSelectors = {
|
|
// Main calendar container (FullCalendar wrapper)
|
|
calendarContainer: () => cy.get('.fc'),
|
|
|
|
// Calendar toolbar
|
|
toolbar: () => cy.get('.fc-toolbar'),
|
|
|
|
// Navigation buttons
|
|
prevButton: () => cy.get('.fc-prev-button'),
|
|
nextButton: () => cy.get('.fc-next-button'),
|
|
todayButton: () => cy.get('.fc-today-button'),
|
|
|
|
// View buttons
|
|
monthViewButton: () => cy.get('.fc-dayGridMonth-button'),
|
|
weekViewButton: () => cy.get('.fc-timeGridWeek-button'),
|
|
dayViewButton: () => cy.get('.fc-timeGridDay-button'),
|
|
|
|
// Calendar title (current month/week)
|
|
title: () => cy.get('.fc-toolbar-title'),
|
|
|
|
// Day cells
|
|
dayCell: () => cy.get('.fc-daygrid-day'),
|
|
dayCellByDate: (dateStr: string) => cy.get(`[data-date="${dateStr}"]`),
|
|
todayCell: () => cy.get('.fc-day-today'),
|
|
|
|
// Events
|
|
event: () => cy.get('.fc-event'),
|
|
eventTitle: () => cy.get('.fc-event-title'),
|
|
|
|
// No date / unscheduled events button
|
|
noDateButton: () => cy.get('.no-date-button'),
|
|
|
|
// More link (when events overflow)
|
|
moreLink: () => cy.get('.fc-more-link, .fc-daygrid-more-link'),
|
|
};
|
|
|
|
/**
|
|
* Board View selectors
|
|
*/
|
|
export const BoardSelectors = {
|
|
// Board container
|
|
boardContainer: () => cy.get('.database-board'),
|
|
|
|
// Board columns (groups)
|
|
columns: () => cy.get('[class*="board-column"], [data-testid*="board-column"]'),
|
|
|
|
// Board cards
|
|
cards: () => cy.get('.board-card'),
|
|
|
|
// Card by row ID
|
|
cardByRowId: (rowId: string) => cy.get(`[data-card-id*="${rowId}"]`),
|
|
|
|
// Card field content (text inside cards)
|
|
cardFields: () => cy.get('.board-card [class*="CardField"]'),
|
|
|
|
// Card content area
|
|
cardContent: () => cy.get('.board-card .truncate'),
|
|
|
|
// Column headers
|
|
columnHeaders: () => cy.get('[class*="column-header"], [data-testid*="column-header"]'),
|
|
|
|
// New card button
|
|
newCardButton: () => cy.contains('+ New'),
|
|
};
|
|
|
|
/**
|
|
* Row Detail Modal selectors
|
|
*/
|
|
export const RowDetailSelectors = {
|
|
// Row detail modal (MUI Dialog)
|
|
modal: () => cy.get('.MuiDialog-paper'),
|
|
|
|
// Modal content
|
|
modalContent: () => cy.get('.MuiDialogContent-root'),
|
|
|
|
// Modal title/header area
|
|
modalTitle: () => cy.get('.MuiDialogTitle-root'),
|
|
|
|
// Close button
|
|
closeButton: () => cy.get('.MuiDialogTitle-root button').first(),
|
|
|
|
// More actions button
|
|
moreActionsButton: () => cy.get('.MuiDialogTitle-root button').last(),
|
|
|
|
// Document area
|
|
documentArea: () => cy.get('.appflowy-scroll-container'),
|
|
|
|
// Dropdown menu items
|
|
duplicateMenuItem: () => cy.get('[role="menuitem"]').contains(/duplicate/i),
|
|
deleteMenuItem: () => cy.get('[role="menuitem"]').contains(/delete/i),
|
|
};
|
|
|
|
export function waitForReactUpdate(ms: number = 500) {
|
|
return cy.wait(ms);
|
|
}
|