mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
256 lines
11 KiB
TypeScript
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');
|
|
});
|
|
});
|