Compare commits

..

11 Commits

Author SHA1 Message Date
dfe24c83ab fix: use synchronous getByText assertion while in fake timer mode before restoring real timers
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 16:47:38 +00:00
569deaf0a4 fix: use findByText instead of getByText for async modal appearance in fake timer tests
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 15:40:40 +00:00
8c10513d6d fix: add Promise.resolve() after advancing fake timers to flush state updates
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 14:40:37 +00:00
e4aaabb079 fix: restore real timers before waitFor in fake timer tests and fix remaining tests
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 14:09:29 +00:00
d48d8488a6 fix: wrap all remaining tests with act() and increase timeouts for stability
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 14:02:10 +00:00
c257721f10 fix: correct flushFileReader helper to avoid infinite recursion
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 13:25:25 +00:00
34d7f8eceb refactor: extract flushFileReader helper function for better maintainability
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 13:00:06 +00:00
94f691a066 fix: apply act() wrapping to additional failing tests
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 12:52:27 +00:00
af325812e8 fix: wrap file upload and button clicks in act() for async state updates
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 12:50:30 +00:00
bb47a4732a fix: ensure vi.useRealTimers() is always called in afterEach for update-dsl-modal tests
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
2026-02-04 12:40:39 +00:00
039ae14251 Initial plan 2026-02-04 12:08:36 +00:00

View File

@ -137,6 +137,7 @@ vi.mock('@/app/components/workflow/constants', () => ({
afterEach(() => {
cleanup()
vi.clearAllMocks()
vi.useRealTimers()
})
describe('UpdateDSLModal', () => {
@ -150,6 +151,11 @@ describe('UpdateDSLModal', () => {
onImport: mockOnImport,
}
// Helper function to flush FileReader microtasks
const flushFileReader = async () => {
await new Promise<void>(resolve => queueMicrotask(resolve))
}
beforeEach(() => {
vi.clearAllMocks()
mockImportDSL.mockResolvedValue({
@ -359,7 +365,11 @@ describe('UpdateDSLModal', () => {
// Select a file
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
// Wait for FileReader to process
await waitFor(() => {
@ -369,12 +379,15 @@ describe('UpdateDSLModal', () => {
// Click import button
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
// Wait for import to be called
await waitFor(() => {
expect(mockImportDSL).toHaveBeenCalled()
})
}, { timeout: 5000 })
})
it('should show success notification on completed import', async () => {
@ -389,7 +402,12 @@ describe('UpdateDSLModal', () => {
// Select a file and click import
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
// Wait for FileReader to process
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -397,13 +415,16 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'success',
}))
})
}, { timeout: 5000 })
})
it('should call onCancel after successful import', async () => {
@ -417,7 +438,12 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
// Wait for FileReader to process
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -425,7 +451,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockOnCancel).toHaveBeenCalled()
@ -443,7 +472,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -451,7 +484,10 @@ describe('UpdateDSLModal', () => {
}, { timeout: 1000 })
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockOnImport).toHaveBeenCalled()
@ -469,7 +505,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -477,7 +517,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
@ -497,7 +540,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -505,7 +552,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
@ -525,7 +575,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
// Wait for FileReader to process and button to be enabled
await waitFor(() => {
@ -534,7 +588,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
@ -550,7 +607,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
// Wait for FileReader to complete and button to be enabled
await waitFor(() => {
@ -559,7 +620,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
@ -588,7 +652,7 @@ describe('UpdateDSLModal', () => {
// Flush the FileReader microtask to ensure fileContent is set
await act(async () => {
await new Promise<void>(resolve => queueMicrotask(resolve))
await flushFileReader()
})
const importButton = screen.getByText('common.overwriteAndImport')
@ -644,7 +708,7 @@ describe('UpdateDSLModal', () => {
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
// Flush microtasks scheduled by the FileReader mock (which uses queueMicrotask)
await new Promise<void>(resolve => queueMicrotask(resolve))
await flushFileReader()
})
const importButton = screen.getByText('common.overwriteAndImport')
@ -656,12 +720,13 @@ describe('UpdateDSLModal', () => {
await Promise.resolve()
// Advance past the 300ms setTimeout in the component
await vi.advanceTimersByTimeAsync(350)
// Flush any pending state updates
await Promise.resolve()
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
})
// Element should be visible immediately after advancing timers
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
vi.useRealTimers()
})
@ -678,7 +743,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -686,7 +755,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
// Wait for error modal with version info
await waitFor(() => {
@ -708,7 +780,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -716,7 +792,10 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
// Wait for error modal
await waitFor(() => {
@ -762,7 +841,7 @@ describe('UpdateDSLModal', () => {
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
// Flush microtasks scheduled by the FileReader mock (which uses queueMicrotask)
await new Promise<void>(resolve => queueMicrotask(resolve))
await flushFileReader()
})
const importButton = screen.getByText('common.overwriteAndImport')
@ -774,21 +853,25 @@ describe('UpdateDSLModal', () => {
await Promise.resolve()
// Advance past the 300ms setTimeout in the component
await vi.advanceTimersByTimeAsync(350)
// Flush any pending state updates
await Promise.resolve()
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
// Element should be visible immediately after advancing timers
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
vi.useRealTimers()
// Click confirm button
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
await act(async () => {
fireEvent.click(confirmButton)
})
await waitFor(() => {
expect(mockImportDSLConfirm).toHaveBeenCalledWith('import-id')
})
vi.useRealTimers()
}, { timeout: 5000 })
})
it('should show success notification after confirm completes', async () => {
@ -851,7 +934,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -859,20 +946,26 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
}, { timeout: 5000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
await act(async () => {
fireEvent.click(confirmButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
}))
})
}, { timeout: 5000 })
})
it('should show error notification when confirm throws exception', async () => {
@ -890,7 +983,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -898,20 +995,26 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
}, { timeout: 5000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
await act(async () => {
fireEvent.click(confirmButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
}))
})
}, { timeout: 5000 })
})
it('should show error when confirm completes but pipeline_id is missing', async () => {
@ -932,7 +1035,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -940,20 +1047,26 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
}, { timeout: 5000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
await act(async () => {
fireEvent.click(confirmButton)
})
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
type: 'error',
}))
})
}, { timeout: 5000 })
})
it('should call onImport after confirm completes successfully', async () => {
@ -974,7 +1087,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -982,18 +1099,24 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
}, { timeout: 5000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
await act(async () => {
fireEvent.click(confirmButton)
})
await waitFor(() => {
expect(mockOnImport).toHaveBeenCalled()
})
}, { timeout: 5000 })
})
it('should call handleCheckPluginDependencies after confirm', async () => {
@ -1020,7 +1143,7 @@ describe('UpdateDSLModal', () => {
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
// Flush microtasks scheduled by the FileReader mock (which uses queueMicrotask)
await new Promise<void>(resolve => queueMicrotask(resolve))
await flushFileReader()
})
const importButton = screen.getByText('common.overwriteAndImport')
@ -1032,20 +1155,24 @@ describe('UpdateDSLModal', () => {
await Promise.resolve()
// Advance past the 300ms setTimeout in the component
await vi.advanceTimersByTimeAsync(350)
// Flush any pending state updates
await Promise.resolve()
})
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
// Element should be visible immediately after advancing timers
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
vi.useRealTimers()
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
await act(async () => {
fireEvent.click(confirmButton)
})
await waitFor(() => {
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('test-pipeline-id', true)
})
vi.useRealTimers()
}, { timeout: 5000 })
})
it('should handle undefined imported_dsl_version and current_dsl_version', async () => {
@ -1061,7 +1188,11 @@ describe('UpdateDSLModal', () => {
const fileInput = screen.getByTestId('file-input')
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
await act(async () => {
fireEvent.change(fileInput, { target: { files: [file] } })
await flushFileReader()
})
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
@ -1069,12 +1200,15 @@ describe('UpdateDSLModal', () => {
})
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
await act(async () => {
fireEvent.click(importButton)
})
// Should show error modal even with undefined versions
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 1000 })
}, { timeout: 5000 })
})
it('should not call importDSLConfirm when importId is not set', async () => {