teableio_teable/apps/nestjs-backend/test/base-node-folder.e2e-spec.ts
Uno 8deafdad3c
feat: base node (#2168)
* feat: add BaseNode and BaseNodeFolder models with migration

* feat: add tree component in ui-lib

* feat: implement BaseNode and BaseNodeFolder functionality with CRUD operations and event handling

* feat: enhance migration script

* feat: add support for user last visit tracking and resource deletion events

* feat: implement permission management for BaseNode with role-based access control

* refactor:  PinService to optimize resource fetching and enhance code readability

* fix: router

* feat: base import/export/duplicae support base node

* test: add unit tests for BaseNodeService methods including SQL generation and edge cases

* feat: implement folder depth validation and enhance node movement logic in BaseNodeService

* feat: integrate performance caching for base node list

* refactor: remove unused routes from BasePageRouter

* feat: enhance dashboard renaming functionality with improved state management and keyboard shortcuts

* refactor: simplify BaseNodeTree component by removing unnecessary separator and enhancing drop logic

* feat: enhance QuickAction search

* fix: sorting for nodes in BaseImportService to ensure proper parent-child relationships

* fix: delete folder and pin list

* feat: add permanent delete functionality for base nodes and enhance delete logic in BaseNodeService

* feat: enhance error handling in BaseNodeService and BaseNodeFolderService with localized messages

* refactor: rename hooks and reorganize imports in base node feature

* refactor: remove console log and clean up imports in PinItem component

* fix: pin sql

* fix: e2e

* fix: sharedb presence handling

* fix: e2e

* refactor: optimize database transactions in BaseNodeService

* fix: improve URL generation in BaseNode components

* refactor: remove unnecessary permission decorator and adjust layout in BaseNodeTree component

* feat: add validation for folder depth when moving nodes

* fix: refine anchorId logic in BaseNodeTree component for improved node positioning

* fix: adjust emoji picker size in BaseNodeTree component for better UI consistency

* fix: enhance expanded when create

* feat: implement auto-scroll functionality during drag in BaseNodeTree component

* fix: update TreeItemLabel and TreeDragLine styles for improved visual consistency

* fix: enhance canDrop logic in BaseNodeTree for improved item drop validation

* refactor: add resourceMeta in baseNodeSchema

* fix: e2e

* refactor: update folder creation and update endpoints to return structured response objects

* fix: e2e

* feat: add disallowDashboard setting and deprecation banner in dashboard components

* fix: type check

* feat: add loading state to BaseNodeContext and integrate skeleton loading in BaseNodeTree

* feat: enhance BaseNode service and UI to include defaultViewId in resourceMeta

* refactor: simplify URL construction in getNodeUrl and streamline table navigation in BaseNodeTree

* refactor: improve styling and structure in BaseNodeTree for better responsiveness and accessibility

* feat: add workflow state render

* fix: sync dataLoader returned undefined error

* refactor: update styling in BaseNodeTree for improved layout and consistency

* refactor: remove setEditingNodeId  when create and duplicate

* refactor: extract table creation logic  for improved readability

* refactor: update dropdown menu width and enhance delete confirmation title with resource type

* fix: common noun i18n

* feat: introduce useBaseNodeContext hook for improved context management in BaseNode components

* refactor: update  useBaseNode for enhanced context management

* refactor: enhance BaseNodeTree component with edit mode support and improved local storage handling

* feat: add onUpdateError callback to useBaseNodeCrud and BaseNodeTree for improved error handling

* refactor:  improved UI consistency

* refactor: improve menu invalidation

* refactor: remove permanent delete functionality from UI components

* refactor: permission handling by consolidating base node actions

* feat: implement base node event handling with create, update, and delete events

* refactor: base node event

* refactor: remove table iist in ssr

* feat: enhance BaseNodeTree with  highlight

* feat: update BaseNodeTree to expand parent nodes on selection

* refactor: replace nativeEnum with enum

* fix: enhance router visits check

* refactor: remove unused user last visit mutation from BaseNodeTree
2025-12-09 22:07:15 +08:00

261 lines
9.1 KiB
TypeScript

/* eslint-disable sonarjs/no-duplicate-string */
import type { INestApplication } from '@nestjs/common';
import { getRandomString } from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import {
createBaseNodeFolder,
updateBaseNodeFolder,
deleteBaseNodeFolder,
createBaseNode,
BaseNodeResourceType,
deleteBaseNode,
} from '@teable/openapi';
import { getError } from './utils/get-error';
import { initApp } from './utils/init-app';
describe('BaseNodeFolderController (e2e) /api/base/:baseId/node/folder', () => {
let app: INestApplication;
const baseId = globalThis.testConfig.baseId;
const folderNameToDelete = 'Folder To Delete';
const whitespaceOnlyName = ' ';
const originalFolderName = 'Original Folder';
let prisma: PrismaService;
beforeAll(async () => {
const appCtx = await initApp();
app = appCtx.app;
prisma = app.get(PrismaService);
});
afterAll(async () => {
await app.close();
});
describe('POST /api/base/:baseId/node/folder - Create folder', () => {
it('should create a folder successfully', async () => {
const ro = { name: 'Test Folder' };
const response = await createBaseNodeFolder(baseId, ro);
expect(response.data).toBeDefined();
expect(response.data.name).toContain('Test Folder');
expect(response.data.id).toBeDefined();
// Cleanup
await deleteBaseNodeFolder(baseId, response.data.id);
});
it('should create multiple folders with same name (auto unique)', async () => {
const ro = { name: 'Duplicate Folder' };
const response1 = await createBaseNodeFolder(baseId, ro);
const response2 = await createBaseNodeFolder(baseId, ro);
expect(response1.data.name).toContain('Duplicate Folder');
expect(response2.data.name).toContain('Duplicate Folder');
expect(response1.data.name).not.toBe(response2.data.name);
expect(response1.data.id).not.toBe(response2.data.id);
// Cleanup
await deleteBaseNodeFolder(baseId, response1.data.id);
await deleteBaseNodeFolder(baseId, response2.data.id);
});
it('should trim folder name', async () => {
const ro = { name: ' Trimmed Folder ' };
const response = await createBaseNodeFolder(baseId, ro);
expect(response.data.name).toContain('Trimmed Folder');
// Cleanup
await deleteBaseNodeFolder(baseId, response.data.id);
});
it('should fail with empty name', async () => {
const ro = { name: '' };
const error = await getError(() => createBaseNodeFolder(baseId, ro));
expect(error?.status).toBe(400);
});
it('should fail with whitespace only name', async () => {
const ro = { name: whitespaceOnlyName };
const error = await getError(() => createBaseNodeFolder(baseId, ro));
expect(error?.status).toBe(400);
});
});
describe('PATCH /api/base/:baseId/node/folder/:folderId - Update folder', () => {
let folderId: string;
beforeEach(async () => {
const response = await createBaseNodeFolder(baseId, { name: originalFolderName });
folderId = response.data.id;
});
afterEach(async () => {
try {
await deleteBaseNodeFolder(baseId, folderId);
} catch (e) {
// Folder might already be deleted in some tests
}
});
it('should rename folder successfully', async () => {
const updateRo = { name: 'Renamed Folder' };
const response = await updateBaseNodeFolder(baseId, folderId, updateRo);
expect(response.data).toBeDefined();
expect(response.data.name).toBe('Renamed Folder');
expect(response.data.id).toBe(folderId);
});
it('should trim folder name when renaming', async () => {
const updateRo = { name: ' Trimmed Renamed ' };
const response = await updateBaseNodeFolder(baseId, folderId, updateRo);
expect(response.data.name).toBe('Trimmed Renamed');
});
it('should fail when renaming to existing folder name', async () => {
// Create another folder
const anotherFolder = await createBaseNodeFolder(baseId, { name: 'Existing Folder' });
// Try to rename original folder to existing name
const updateRo = { name: 'Existing Folder' };
const error = await getError(() => updateBaseNodeFolder(baseId, folderId, updateRo));
expect(error?.status).toBe(400);
expect(error?.message).toContain('Folder name already exists');
// Cleanup
await deleteBaseNodeFolder(baseId, anotherFolder.data.id);
});
it('should allow renaming folder to same name', async () => {
const updateRo = { name: originalFolderName };
const response = await updateBaseNodeFolder(baseId, folderId, updateRo);
expect(response.data.name).toBe(originalFolderName);
});
it('should fail with empty name', async () => {
const updateRo = { name: '' };
const error = await getError(() => updateBaseNodeFolder(baseId, folderId, updateRo));
expect(error?.status).toBe(400);
});
it('should fail with whitespace only name', async () => {
const updateRo = { name: whitespaceOnlyName };
const error = await getError(() => updateBaseNodeFolder(baseId, folderId, updateRo));
expect(error?.status).toBe(400);
});
it('should fail when updating non-existent folder', async () => {
const nonExistentId = 'non-existent-folder-id';
const updateRo = { name: 'New Name' };
const error = await getError(() => updateBaseNodeFolder(baseId, nonExistentId, updateRo));
expect(error?.status).toBeGreaterThanOrEqual(400);
});
});
describe('DELETE /api/base/:baseId/node/folder/:folderId - Delete folder', () => {
it('should delete empty folder successfully', async () => {
// Create a folder
const folder = await createBaseNodeFolder(baseId, { name: folderNameToDelete });
const folderId = folder.data.id;
const findFolder = await prisma.baseNodeFolder.findFirst({
where: { id: folderId },
});
expect(findFolder).toBeDefined();
// Delete the folder
await deleteBaseNodeFolder(baseId, folderId);
const findFolderAfterDelete = await prisma.baseNodeFolder.findFirst({
where: { id: folderId },
});
expect(findFolderAfterDelete).toBeNull();
// Verify folder is deleted
const error = await getError(() => deleteBaseNodeFolder(baseId, folderId));
expect(error?.status).toBeGreaterThanOrEqual(400);
});
it('should fail when deleting folder with children', async () => {
// Create a parent folder
const parentFolder = await createBaseNode(baseId, {
resourceType: BaseNodeResourceType.Folder,
name: 'Parent Folder',
}).then((res) => res.data);
// Create a child folder inside the parent folder using createBaseNode
const childFolder = await createBaseNode(baseId, {
resourceType: BaseNodeResourceType.Folder,
parentId: parentFolder.id,
name: 'Child Folder',
}).then((res) => res.data);
// Try to delete the parent folder
const error = await getError(() => deleteBaseNode(baseId, parentFolder.id));
expect(error?.status).toBe(400);
expect(error?.message).toContain('Cannot delete folder because it is not empty');
// Cleanup - need to delete the folder manually after removing children
await deleteBaseNode(baseId, childFolder.id);
await deleteBaseNode(baseId, parentFolder.id);
const findFolderAfterDelete = await prisma.baseNodeFolder.findFirst({
where: { id: { in: [parentFolder.id, childFolder.id] } },
});
expect(findFolderAfterDelete).toBeNull();
});
it('should fail when deleting non-existent folder', async () => {
const nonExistentId = 'non-existent-folder-id';
const error = await getError(() => deleteBaseNodeFolder(baseId, nonExistentId));
expect(error?.status).toBeGreaterThanOrEqual(400);
});
it('should handle deletion of already deleted folder', async () => {
// Create and delete a folder
const folder = await createBaseNodeFolder(baseId, { name: 'Temp Folder' });
const folderId = folder.data.id;
await deleteBaseNodeFolder(baseId, folderId);
// Try to delete again
const error = await getError(() => deleteBaseNodeFolder(baseId, folderId));
expect(error?.status).toBeGreaterThanOrEqual(400);
});
});
describe('Integration tests', () => {
it('should create, update and delete folder in sequence', async () => {
// Create
const createResponse = await createBaseNodeFolder(baseId, { name: 'Integration Folder' });
expect(createResponse.data.name).toContain('Integration Folder');
const folderId = createResponse.data.id;
// Update
const newName = getRandomString(10);
const updateResponse = await updateBaseNodeFolder(baseId, folderId, {
name: newName,
});
expect(updateResponse.data.name).toContain(newName);
// Delete
await deleteBaseNodeFolder(baseId, folderId);
const findFolderAfterDelete = await prisma.baseNodeFolder.findFirst({
where: { id: folderId },
});
expect(findFolderAfterDelete).toBeNull();
});
});
});