Files
AppFlowy-Web/playwright/e2e/database/database-duplicate-cloud.spec.ts
2026-03-11 11:24:18 +08:00

256 lines
11 KiB
TypeScript

/**
* Cloud Database Duplication Tests
*
* Tests that duplicating a database creates an independent copy:
* - Row counts match
* - Edits in duplicate don't affect original
* - Row document content is independent
*
* Migrated from: cypress/e2e/database/database-duplicate-cloud.cy.ts
*
* NOTE: This test uses a specific pre-existing user (export_user@appflowy.io)
* with password-based login and requires the "Database 1" under
* General > Getting started to exist.
*/
import { test, expect } from '@playwright/test';
import {
AuthSelectors,
DatabaseGridSelectors,
PageSelectors,
ViewActionSelectors,
} from '../../support/selectors';
import { expandSpaceByName } from '../../support/page-utils';
import { testLog } from '../../support/test-helpers';
const _exportUserEmail = 'export_user@appflowy.io';
const _exportUserPassword = 'AppFlowy!@123';
const _testDatabaseName = 'Database 1';
const _spaceName = 'General';
const _gettingStartedPageName = 'Getting started';
/**
* Expand a page in the sidebar and wait for its children to become visible.
* With lazy loading, the outline may reload and clear children even while the
* page stays in the "expanded" state. This helper retries by collapsing and
* re-expanding until the child appears.
*/
async function expandPageAndWaitForChildren(
page: import('@playwright/test').Page,
pageName: string,
childNameContains: string,
maxAttempts = 15
) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const pageItem = PageSelectors.itemByName(page, pageName);
const expandToggle = pageItem.locator('[data-testid="outline-toggle-expand"]');
const collapseToggle = pageItem.locator('[data-testid="outline-toggle-collapse"]');
if ((await expandToggle.count()) > 0) {
// Page is collapsed - expand it
await expandToggle.first().click({ force: true });
await page.waitForTimeout(1000);
} else if ((await collapseToggle.count()) > 0 && attempt > 0) {
// Page is expanded but children may be stale from outline reload.
// Collapse and re-expand to trigger a fresh children fetch.
await collapseToggle.first().click({ force: true });
await page.waitForTimeout(500);
const expToggle = pageItem.locator('[data-testid="outline-toggle-expand"]');
if ((await expToggle.count()) > 0) {
await expToggle.first().click({ force: true });
await page.waitForTimeout(1000);
}
}
// Check if the target child is now visible
const childVisible = await PageSelectors.nameContaining(page, childNameContains).first().isVisible().catch(() => false);
if (childVisible) {
return;
}
await page.waitForTimeout(1000);
}
throw new Error(`Child "${childNameContains}" not found under "${pageName}" after ${maxAttempts} attempts`);
}
test.describe('Cloud Database Duplication', () => {
test.beforeEach(async ({ page }) => {
page.on('pageerror', (err) => {
if (
err.message.includes('Minified React error') ||
err.message.includes('View not found') ||
err.message.includes('No workspace or service found') ||
err.message.includes('ResizeObserver loop')
) {
return;
}
});
await page.setViewportSize({ width: 1280, height: 720 });
});
test('should duplicate Database 1 and verify data independence', async ({ page }) => {
testLog.info(`[TEST START] Testing cloud database duplication with: ${_exportUserEmail}`);
// Given: logged in as the export user with password
testLog.info('[STEP 1] Visiting login page');
await page.goto('/login', { waitUntil: 'load' });
await page.waitForTimeout(5000);
testLog.info('[STEP 2] Entering email address');
await expect(AuthSelectors.emailInput(page)).toBeVisible({ timeout: 30000 });
await AuthSelectors.emailInput(page).fill(_exportUserEmail);
await page.waitForTimeout(500);
testLog.info('[STEP 3] Clicking sign in with password button');
await expect(AuthSelectors.passwordSignInButton(page)).toBeVisible();
await AuthSelectors.passwordSignInButton(page).click();
await page.waitForTimeout(1000);
testLog.info('[STEP 4] Verifying password page loaded');
await expect(page).toHaveURL(/action=enterPassword/);
testLog.info('[STEP 5] Entering password');
await expect(AuthSelectors.passwordInput(page)).toBeVisible();
await AuthSelectors.passwordInput(page).fill(_exportUserPassword);
await page.waitForTimeout(500);
testLog.info('[STEP 6] Submitting password for authentication');
await AuthSelectors.passwordSubmitButton(page).click();
testLog.info('[STEP 7] Waiting for successful login');
await expect(page).toHaveURL(/\/app/, { timeout: 30000 });
testLog.info('[STEP 8] Waiting for app to fully load');
await page.waitForTimeout(5000);
testLog.info('[STEP 9] Waiting for data sync');
await expect(PageSelectors.names(page).first()).toBeAttached({ timeout: 60000 });
await page.waitForTimeout(5000);
// And: any existing duplicate databases are cleaned up
testLog.info('[STEP 10] Cleaning up existing duplicate databases');
const copySuffix = ' (Copy)';
const duplicatePrefix = `${_testDatabaseName}${copySuffix}`;
const existingDuplicates = page.getByTestId('page-name').filter({ hasText: duplicatePrefix });
const dupeCount = await existingDuplicates.count();
for (let i = 0; i < dupeCount; i++) {
const pageName = (await existingDuplicates.first().innerText()).trim();
if (pageName.startsWith(duplicatePrefix)) {
await PageSelectors.moreActionsButton(page, pageName).click({ force: true });
await page.waitForTimeout(500);
await ViewActionSelectors.deleteButton(page).click({ force: true });
await page.waitForTimeout(500);
const confirmBtn = page.getByTestId('confirm-delete-button');
if ((await confirmBtn.count()) > 0) {
await confirmBtn.click({ force: true });
}
await page.waitForTimeout(1000);
}
}
// And: navigated to the original Database 1 with rows loaded
testLog.info('[STEP 11] Expanding General space and Getting started page');
await expandSpaceByName(page, _spaceName);
await page.waitForTimeout(1000);
await expandPageAndWaitForChildren(page, _gettingStartedPageName, _testDatabaseName);
testLog.info('[STEP 11.1] Opening Database 1');
await expect(PageSelectors.nameContaining(page, _testDatabaseName).first()).toBeVisible({ timeout: 30000 });
await PageSelectors.nameContaining(page, _testDatabaseName).first().click({ force: true });
await page.waitForTimeout(3000);
testLog.info('[STEP 12] Waiting for database grid to load');
await expect(DatabaseGridSelectors.grid(page)).toBeVisible({ timeout: 30000 });
await page.waitForTimeout(2000);
testLog.info('[STEP 13] Counting original rows');
const originalRowCount = await DatabaseGridSelectors.dataRows(page).count();
testLog.info(`[STEP 13.1] Original database has ${originalRowCount} rows`);
expect(originalRowCount).toBeGreaterThan(0);
// When: duplicating the database via the context menu
testLog.info('[STEP 14] Duplicating the database');
await PageSelectors.moreActionsButton(page, _testDatabaseName).click({ force: true });
await page.waitForTimeout(500);
testLog.info('[STEP 14.1] Clicking duplicate button');
await ViewActionSelectors.duplicateButton(page).click({ force: true });
await page.waitForTimeout(3000);
// Then: the duplicate appears in the sidebar
testLog.info('[STEP 15] Waiting for duplicate to appear in sidebar');
await expect(PageSelectors.nameContaining(page, duplicatePrefix).first()).toBeVisible({ timeout: 90000 });
await page.waitForTimeout(2000);
// And: the duplicate has the same row count as the original
testLog.info('[STEP 16] Opening the duplicated database');
await PageSelectors.nameContaining(page, duplicatePrefix).first().click({ force: true });
await page.waitForTimeout(3000);
testLog.info('[STEP 17] Waiting for duplicated database grid to load');
await expect(DatabaseGridSelectors.grid(page)).toBeVisible({ timeout: 30000 });
await page.waitForTimeout(2000);
testLog.info('[STEP 18] Verifying duplicated row count');
const duplicatedRowCount = await DatabaseGridSelectors.dataRows(page).count();
testLog.info(`[STEP 18.1] Duplicated database has ${duplicatedRowCount} rows`);
expect(duplicatedRowCount).toBe(originalRowCount);
// When: editing a cell in the duplicated database
testLog.info('[STEP 19] Editing a cell in the duplicated database');
const marker = `db-duplicate-marker-${Date.now()}`;
await DatabaseGridSelectors.cells(page).first().click({ force: true });
await page.waitForTimeout(500);
await page.keyboard.press('Control+A');
await page.keyboard.type(marker);
await page.keyboard.press('Enter');
await page.waitForTimeout(1000);
testLog.info('[STEP 20] Verifying marker was added to duplicated database');
await expect(DatabaseGridSelectors.cells(page).first()).toContainText(marker);
// And: navigating back to the original database
testLog.info('[STEP 21] Opening the original database');
await expandSpaceByName(page, _spaceName);
await page.waitForTimeout(500);
await expandPageAndWaitForChildren(page, _gettingStartedPageName, _testDatabaseName);
const dbPages = PageSelectors.nameContaining(page, _testDatabaseName);
const dbCount = await dbPages.count();
for (let i = 0; i < dbCount; i++) {
const text = (await dbPages.nth(i).innerText()).trim();
if (!text.includes('(Copy)')) {
await dbPages.nth(i).click({ force: true });
break;
}
}
await page.waitForTimeout(3000);
testLog.info('[STEP 22] Waiting for original database grid to load');
await expect(DatabaseGridSelectors.grid(page)).toBeVisible({ timeout: 30000 });
await page.waitForTimeout(2000);
// Then: the marker does not appear in the original database
testLog.info('[STEP 23] Verifying cell marker is NOT in original database');
const allCellTexts = await DatabaseGridSelectors.cells(page).allInnerTexts();
const markerFound = allCellTexts.some((text) => text.includes(marker));
expect(markerFound).toBeFalsy();
// And: cleanup by deleting the duplicated database
testLog.info('[STEP 24] Cleaning up - deleting duplicated database');
const dupePageName = await PageSelectors.nameContaining(page, duplicatePrefix).first().innerText();
await PageSelectors.moreActionsButton(page, dupePageName.trim()).click({ force: true });
await page.waitForTimeout(500);
await ViewActionSelectors.deleteButton(page).click({ force: true });
await page.waitForTimeout(500);
const confirmDelete = page.getByTestId('confirm-delete-button');
if ((await confirmDelete.count()) > 0) {
await confirmDelete.click({ force: true });
}
testLog.info('[STEP 25] Cloud database duplication test completed successfully');
});
});