summaryrefslogtreecommitdiff
path: root/apps/web/src/__tests__/excessive-projects.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/__tests__/excessive-projects.test.ts')
-rw-r--r--apps/web/src/__tests__/excessive-projects.test.ts316
1 files changed, 0 insertions, 316 deletions
diff --git a/apps/web/src/__tests__/excessive-projects.test.ts b/apps/web/src/__tests__/excessive-projects.test.ts
deleted file mode 100644
index 54dfb6d6..00000000
--- a/apps/web/src/__tests__/excessive-projects.test.ts
+++ /dev/null
@@ -1,316 +0,0 @@
-/**
- * Test Suite: Excessive Project Creation Bug
- *
- * This test suite reproduces and validates the bug where navigating to invalid
- * project IDs creates excessive "Untitled Project" entries due to race conditions
- * in the useEffect hook dependencies.
- *
- * Bug Report: https://github.com/OpenCut-app/OpenCut/issues/182
- */
-
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
-import { render, waitFor } from '@testing-library/react'
-import { act } from 'react'
-
-// Mock the project store
-const mockProjects: any[] = []
-const mockActiveProject = { id: null }
-
-const mockProjectStore = {
- savedProjects: mockProjects,
- activeProject: mockActiveProject,
- loadProject: vi.fn(),
- createNewProject: vi.fn(),
- loadAllProjects: vi.fn(),
-}
-
-const mockLoadProject = vi.fn()
-const mockCreateNewProject = vi.fn()
-
-// Mock Zustand store
-vi.mock('@/stores/project-store', () => ({
- useProjectStore: (selector: any) => {
- if (typeof selector === 'function') {
- return selector(mockProjectStore)
- }
- return mockProjectStore
- },
-}))
-
-// Mock the FIXED editor page component logic (extracted for testing)
-class EditorPageLogic {
- private handledProjectIds = new Set<string>()
- private isInitializing = false
- private projectStore: any
- private router: any
-
- constructor(projectStore: any, router: any) {
- this.projectStore = projectStore
- this.router = router
- }
-
- async initProject(projectId: string, activeProject: any) {
- if (!projectId) {
- return
- }
-
- // Prevent duplicate initialization (FIX for race condition)
- if (this.isInitializing) {
- return
- }
-
- // Check if project is already loaded
- if (activeProject?.id === projectId) {
- return
- }
-
- // Check if we've already handled this project ID
- if (this.handledProjectIds.has(projectId)) {
- return
- }
-
- // Mark as initializing to prevent race conditions
- this.isInitializing = true
- this.handledProjectIds.add(projectId)
-
- try {
- await this.projectStore.loadProject(projectId)
- // Project loaded successfully
- this.isInitializing = false
- } catch (error) {
- // More specific error handling - only create new project for actual "not found" errors
- const isProjectNotFound = error instanceof Error &&
- (error.message.includes('not found') ||
- error.message.includes('does not exist') ||
- error.message.includes('Project not found'))
-
- if (isProjectNotFound) {
- try {
- const newProjectId = await this.projectStore.createNewProject("Untitled Project")
- this.router.replace(`/editor/${newProjectId}`)
- } catch (createError) {
- console.error("Failed to create new project:", createError)
- }
- } else {
- // For other errors (storage issues, corruption, etc.), don't create new project
- console.error("Project loading failed with recoverable error:", error)
- // Remove from handled set so user can retry
- this.handledProjectIds.delete(projectId)
- }
-
- this.isInitializing = false
- }
- }
-
- // Simulate the useEffect behavior with dependencies (FIXED - no activeProject dependency)
- async simulateUseEffect(projectId: string, activeProject: any) {
- // This simulates how React useEffect would trigger with FIXED dependencies
- await this.initProject(projectId, activeProject)
- }
-
- reset() {
- this.handledProjectIds.clear()
- this.isInitializing = false
- }
-}
-
-describe('Excessive Project Creation Bug', () => {
- let editorLogic: EditorPageLogic
- let projectCreationCount: number
-
- beforeEach(() => {
- // Reset state
- mockProjects.length = 0
- mockActiveProject.id = null
- projectCreationCount = 0
-
- // Setup mocks
- mockLoadProject.mockRejectedValue(new Error('Project not found'))
- mockCreateNewProject.mockImplementation(async (name: string) => {
- projectCreationCount++
- const newProject = {
- id: `generated-id-${projectCreationCount}`,
- name,
- createdAt: new Date(),
- }
- mockProjects.push(newProject)
- mockActiveProject.id = newProject.id
- return newProject.id
- })
-
- mockProjectStore.loadProject = mockLoadProject
- mockProjectStore.createNewProject = mockCreateNewProject
-
- editorLogic = new EditorPageLogic(mockProjectStore, global.mockRouter)
- })
-
- afterEach(() => {
- editorLogic.reset()
- vi.clearAllMocks()
- })
-
- describe('Single Invalid Project ID', () => {
- it('should create exactly one project for a single invalid ID', async () => {
- const invalidProjectId = 'invalid-project-123'
-
- await editorLogic.simulateUseEffect(invalidProjectId, mockActiveProject)
-
- expect(projectCreationCount).toBe(1)
- expect(mockCreateNewProject).toHaveBeenCalledTimes(1)
- expect(mockCreateNewProject).toHaveBeenCalledWith('Untitled Project')
- expect(global.mockRouter.replace).toHaveBeenCalledWith('/editor/generated-id-1')
- })
-
- it('should not create duplicate projects for the same invalid ID', async () => {
- const invalidProjectId = 'invalid-project-123'
-
- // First call
- await editorLogic.simulateUseEffect(invalidProjectId, mockActiveProject)
-
- // Second call with same ID (should be prevented by handledProjectIds)
- await editorLogic.simulateUseEffect(invalidProjectId, mockActiveProject)
-
- expect(projectCreationCount).toBe(1)
- expect(mockCreateNewProject).toHaveBeenCalledTimes(1)
- })
- })
-
- describe('Race Condition Scenarios', () => {
- it('should reproduce the race condition when activeProject changes', async () => {
- const invalidProjectId = 'invalid-project-race'
-
- // Simulate the race condition sequence:
- // 1. useEffect triggers with invalid project ID
- await editorLogic.simulateUseEffect(invalidProjectId, { id: null })
-
- // 2. activeProject changes (this would trigger useEffect again in React)
- const newActiveProject = { id: 'generated-id-1' }
-
- // 3. useEffect triggers again with same projectId but different activeProject
- await editorLogic.simulateUseEffect(invalidProjectId, newActiveProject)
-
- // Should still only create one project due to handledProjectIds
- expect(projectCreationCount).toBe(1)
- })
-
- it('should demonstrate the bug with rapid navigation', async () => {
- const invalidIds = [
- 'rapid-test-1',
- 'rapid-test-2',
- 'rapid-test-3',
- 'rapid-test-4',
- 'rapid-test-5'
- ]
-
- // Simulate rapid navigation to multiple invalid IDs
- for (const invalidId of invalidIds) {
- await editorLogic.simulateUseEffect(invalidId, mockActiveProject)
- }
-
- // Each invalid ID should create exactly one project
- expect(projectCreationCount).toBe(invalidIds.length)
- expect(mockCreateNewProject).toHaveBeenCalledTimes(invalidIds.length)
- })
-
- it('should simulate React Strict Mode double execution', async () => {
- const invalidProjectId = 'strict-mode-test'
-
- // Simulate React Strict Mode double execution
- const promises = [
- editorLogic.simulateUseEffect(invalidProjectId, mockActiveProject),
- editorLogic.simulateUseEffect(invalidProjectId, mockActiveProject)
- ]
-
- await Promise.all(promises)
-
- // Should only create one project despite double execution
- // (This test validates that handledProjectIds prevents duplicates)
- expect(projectCreationCount).toBe(1)
- })
- })
-
- describe('Error Handling Scenarios', () => {
- it('should only create projects for "not found" errors, not other failures', async () => {
- const scenarios = [
- { id: 'storage-error', error: new Error('Storage quota exceeded'), shouldCreateProject: false },
- { id: 'network-error', error: new Error('Network timeout'), shouldCreateProject: false },
- { id: 'corruption-error', error: new Error('File corrupted'), shouldCreateProject: false },
- { id: 'permission-error', error: new Error('Permission denied'), shouldCreateProject: false },
- { id: 'not-found-error', error: new Error('Project not found'), shouldCreateProject: true },
- { id: 'does-not-exist-error', error: new Error('Project does not exist'), shouldCreateProject: true }
- ]
-
- let expectedProjectCount = 0
- for (const scenario of scenarios) {
- mockLoadProject.mockRejectedValueOnce(scenario.error)
- await editorLogic.simulateUseEffect(scenario.id, mockActiveProject)
- if (scenario.shouldCreateProject) {
- expectedProjectCount++
- }
- }
-
- // Only "not found" errors should create new projects
- expect(projectCreationCount).toBe(expectedProjectCount)
- expect(expectedProjectCount).toBe(2) // Only the 2 "not found" scenarios
- })
- })
-
- describe('Stress Test - Excessive Project Creation', () => {
- it('should demonstrate how 885 projects could be created', async () => {
- const numberOfInvalidRequests = 50 // Simulate many invalid requests over time
-
- // Simulate accumulated invalid project requests over development sessions
- for (let i = 0; i < numberOfInvalidRequests; i++) {
- const invalidId = `stress-test-${i}-${Date.now()}`
- await editorLogic.simulateUseEffect(invalidId, mockActiveProject)
-
- // Reset handledProjectIds occasionally to simulate component remounting
- if (i % 10 === 0) {
- editorLogic.reset()
- }
- }
-
- expect(projectCreationCount).toBe(numberOfInvalidRequests)
- expect(mockProjects).toHaveLength(numberOfInvalidRequests)
-
- // Verify all projects are "Untitled Project"
- const untitledProjects = mockProjects.filter(p => p.name === 'Untitled Project')
- expect(untitledProjects).toHaveLength(numberOfInvalidRequests)
- })
- })
-
- describe('Bug Validation', () => {
- it('should confirm the exact bug reproduction steps', async () => {
- // Step 1: Navigate to invalid project ID (like browser back button)
- const invalidId = 'back-button-navigation-test'
-
- // Step 2: Project load fails, creates new project
- await editorLogic.simulateUseEffect(invalidId, mockActiveProject)
-
- expect(projectCreationCount).toBe(1)
- expect(mockProjects[0].name).toBe('Untitled Project')
- expect(global.mockRouter.replace).toHaveBeenCalledWith('/editor/generated-id-1')
-
- // Step 3: Verify the project was added to storage
- expect(mockProjects).toHaveLength(1)
- })
-
- it('should measure the impact of the bug', async () => {
- const startTime = Date.now()
- const testIterations = 100
-
- for (let i = 0; i < testIterations; i++) {
- await editorLogic.simulateUseEffect(`test-${i}`, mockActiveProject)
- }
-
- const endTime = Date.now()
- const duration = endTime - startTime
-
- expect(projectCreationCount).toBe(testIterations)
-
- // Log performance impact
- console.log(`Created ${projectCreationCount} projects in ${duration}ms`)
- console.log(`Average: ${duration / testIterations}ms per project`)
- })
- })
-})