diff --git a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.test.tsx b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.test.tsx new file mode 100644 index 0000000000..2c05bec7f4 --- /dev/null +++ b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.test.tsx @@ -0,0 +1,329 @@ +import { assignmentProblemQueries } from '@/app/(client)/_libs/queries/assignmentProblem' +import { assignmentSubmissionQueries } from '@/app/(client)/_libs/queries/assignmentSubmission' +import { contestProblemQueries } from '@/app/(client)/_libs/queries/contestProblem' +import { contestSubmissionQueries } from '@/app/(client)/_libs/queries/contestSubmission' +import { problemSubmissionQueries } from '@/app/(client)/_libs/queries/problemSubmission' +import type * as Utils from '@/libs/utils' +import type { ProblemDetail } from '@/types/type' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import type { ComponentProps } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { SubmissionProgressPanel } from '../TestcasePanel/SubmissionProgressPanel' +import { + TestPollingStoreProvider, + useTestPollingStore +} from '../context/TestPollingStoreProvider' +import { EditorHeader } from './EditorHeader' + +const mocks = vi.hoisted(() => ({ + post: vi.fn(), + get: vi.fn(), + invalidate: vi.fn(), + session: { user: { username: 'student' } }, + router: { push: vi.fn() }, + confetti: vi.fn(), + destroyCanvas: vi.fn(), + onSubmissionStart: vi.fn(), + startRunner: vi.fn() +})) + +vi.mock('@/libs/utils', async (importOriginal) => ({ + ...(await importOriginal()), + fetcherWithAuth: Object.assign(mocks.get, { post: mocks.post }) +})) +vi.mock('@/libs/hooks/useSession', () => ({ useSession: () => mocks.session })) +vi.mock('next/navigation', () => ({ + useRouter: () => mocks.router, + usePathname: () => '/problem/1' +})) +vi.mock('@tanstack/react-query', () => ({ + useQueryClient: () => ({ invalidateQueries: mocks.invalidate }) +})) +vi.mock('js-confetti', () => ({ + default: class { + addConfetti = mocks.confetti + destroyCanvas = mocks.destroyCanvas + } +})) +vi.mock('@/stores/editor', () => ({ + useCodeStore: (selector: (state: unknown) => unknown) => + selector({ getCode: () => 'int main() {}', setCode: vi.fn() }), + useLanguageStore: () => () => ({ language: 'C', setLanguage: vi.fn() }), + getStorageKey: () => undefined, + getCodeFromLocalStorage: () => undefined +})) +vi.mock('../TestcasePanel/useRunner', () => ({ + useRunner: () => ({ startRunner: mocks.startRunner }) +})) +vi.mock('@/components/AlertModal', () => ({ AlertModal: () => null })) +vi.mock('./RunTestButton', () => ({ RunTestButton: () => null })) +vi.mock('./BackCautionDialog', () => ({ BackCautionDialog: () => null })) +vi.mock('@/public/icons/submit.svg', () => ({ default: () => })) + +function ProgressView() { + const progress = useTestPollingStore((state) => state.submissionProgress) + return progress ? : null +} + +const problem = { + id: 1, + languages: ['C'], + problemTestcase: [] +} as unknown as ProblemDetail +const response = (result: string) => ({ + ok: true, + json: () => + Promise.resolve({ + result, + testcaseResult: [ + { + id: 1, + problemTestcaseId: 1, + result, + cpuTime: '35', + memoryUsage: 1024 + } + ] + }) +}) + +const mount = (props: Partial> = {}) => + render( + + + + + ) + +beforeEach(() => { + vi.resetAllMocks() + vi.useFakeTimers() + mocks.post.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ id: 42 }) + }) +}) +afterEach(() => { + cleanup() + vi.useRealTimers() +}) + +describe('submission workflow', () => { + it('clears the previous submission result when running code', async () => { + mocks.get.mockResolvedValue(response('Accepted')) + mount() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + await act(async () => { + await vi.advanceTimersByTimeAsync(500) + }) + expect(screen.getByText('맞았습니다!')).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: /^Run$/ })) + expect(screen.queryByText('맞았습니다!')).toBeNull() + expect(mocks.startRunner).toHaveBeenCalledWith('int main() {}', 'C') + }) + + it.each([ + { + props: {}, + searchParams: { problemId: 1 }, + queryKeys: [problemSubmissionQueries.lists(1)] + }, + { + props: { contestId: 10 }, + searchParams: { problemId: 1, contestId: 10 }, + queryKeys: [ + contestProblemQueries.lists(10), + contestSubmissionQueries.lists({ contestId: 10, problemId: 1 }) + ] + }, + { + props: { assignmentId: 20 }, + searchParams: { problemId: 1, assignmentId: 20 }, + queryKeys: [ + assignmentProblemQueries.lists(20), + assignmentSubmissionQueries.lists({ assignmentId: 20, problemId: 1 }) + ] + }, + { + props: { exerciseId: 30 }, + searchParams: { problemId: 1, assignmentId: 30 }, + queryKeys: [ + assignmentProblemQueries.lists(30), + assignmentSubmissionQueries.lists({ assignmentId: 30, problemId: 1 }) + ] + } + ])( + 'refreshes lists on submission and completion for $props', + async ({ props, searchParams, queryKeys }) => { + mocks.get.mockResolvedValue(response('WrongAnswer')) + mount(props) + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + expect(mocks.post).toHaveBeenCalledWith( + 'submission', + expect.objectContaining({ searchParams }) + ) + expect( + mocks.invalidate.mock.calls.map(([options]) => options.queryKey) + ).toEqual(queryKeys) + mocks.invalidate.mockClear() + await act(async () => { + await vi.advanceTimersByTimeAsync(500) + }) + expect(screen.getByText('틀렸습니다!')).toBeTruthy() + expect(mocks.get).toHaveBeenCalledWith( + 'submission/42', + expect.objectContaining({ + searchParams: { ...searchParams, pollingTime: expect.any(Number) } + }) + ) + expect( + mocks.invalidate.mock.calls.map(([options]) => options.queryKey) + ).toEqual(queryKeys) + await act(async () => { + await vi.advanceTimersByTimeAsync(5000) + }) + expect(mocks.get).toHaveBeenCalledTimes(1) + expect(mocks.invalidate).toHaveBeenCalledTimes(queryKeys.length) + } + ) + + it('ignores a polling response after the editor unmounts', async () => { + let finishPoll: (value: ReturnType) => void = () => { + throw new Error('Polling has not started') + } + mocks.get.mockReturnValueOnce( + new Promise((resolve) => { + finishPoll = resolve + }) + ) + const view = mount() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + await act(async () => { + await vi.advanceTimersByTimeAsync(500) + }) + expect(mocks.get).toHaveBeenCalledTimes(1) + mocks.invalidate.mockClear() + view.unmount() + await act(async () => { + finishPoll(response('Accepted')) + await Promise.resolve() + }) + expect(mocks.confetti).not.toHaveBeenCalled() + expect(mocks.invalidate).not.toHaveBeenCalled() + }) + + it('releases the submit button and shows an error after a rejected POST', async () => { + mocks.post.mockRejectedValueOnce(new Error('offline')) + mount() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + expect(screen.getByRole('alert').textContent).toContain( + '제출 요청에 실패했습니다' + ) + expect( + screen.getByRole('button', { name: /Submit/ }).hasAttribute('disabled') + ).toBe(false) + expect(mocks.get).not.toHaveBeenCalled() + }) + + it('does not overlap slow polls and transitions to the final result once', async () => { + let finishPoll: (value: ReturnType) => void = () => { + throw new Error('Polling has not started') + } + mocks.get + .mockReturnValueOnce( + new Promise((resolve) => { + finishPoll = resolve + }) + ) + .mockResolvedValue(response('Accepted')) + mount() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + expect(screen.getByText('채점 대기 중...')).toBeTruthy() + expect(mocks.onSubmissionStart).toHaveBeenCalledTimes(1) + await act(async () => { + await vi.advanceTimersByTimeAsync(2500) + }) + expect(mocks.get).toHaveBeenCalledTimes(1) + await act(async () => { + finishPoll(response('Judging')) + await Promise.resolve() + }) + expect(screen.getByText('채점 대기 중...')).toBeTruthy() + await act(async () => { + await vi.advanceTimersByTimeAsync(500) + }) + expect(screen.getByText('맞았습니다!')).toBeTruthy() + expect(mocks.confetti).toHaveBeenCalledTimes(1) + expect(mocks.invalidate).toHaveBeenCalledTimes(2) + await act(async () => { + await vi.advanceTimersByTimeAsync(3000) + }) + expect(mocks.get).toHaveBeenCalledTimes(2) + }) + + it('ignores a POST that resolves after the editor unmounts', async () => { + let finishSubmit: (value: { + ok: boolean + json: () => Promise<{ id: number }> + }) => void = () => { + throw new Error('Submission has not started') + } + mocks.post.mockReturnValueOnce( + new Promise((resolve) => { + finishSubmit = resolve + }) + ) + const view = mount() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + view.unmount() + await act(async () => { + finishSubmit({ ok: true, json: () => Promise.resolve({ id: 42 }) }) + await vi.advanceTimersByTimeAsync(1000) + }) + expect(mocks.invalidate).not.toHaveBeenCalled() + expect(mocks.get).not.toHaveBeenCalled() + expect(mocks.destroyCanvas).toHaveBeenCalledTimes(1) + }) + + it('stops repeated polling failures and releases the button', async () => { + mocks.get.mockRejectedValue(new Error('offline')) + mount() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Submit/ })) + await Promise.resolve() + }) + await act(async () => { + await vi.advanceTimersByTimeAsync(20000) + }) + expect(mocks.get).toHaveBeenCalledTimes(5) + expect(screen.getByRole('alert').textContent).toContain( + '제출 내역에서 결과를 확인' + ) + expect( + screen.getByRole('button', { name: /Submit/ }).hasAttribute('disabled') + ).toBe(false) + }) +}) diff --git a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx index d9c10790fc..13255cf67e 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx +++ b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/EditorHeader.tsx @@ -1,6 +1,5 @@ 'use client' -import { useSubmissionPolling } from '@/app/(client)/(code-editor)/_libs/hooks/useSubmissionPolling' import { assignmentProblemQueries } from '@/app/(client)/_libs/queries/assignmentProblem' import { assignmentSubmissionQueries } from '@/app/(client)/_libs/queries/assignmentSubmission' import { contestProblemQueries } from '@/app/(client)/_libs/queries/contestProblem' @@ -35,12 +34,14 @@ import { import { RUN_CODE_TAB, useSidePanelTabStore, - useTestcaseTabStore + useTestcaseTabStore, + TESTCASE_RESULT_TAB } from '@/stores/editorTabs' import type { Language, ProblemDetail, Submission, + SubmissionDetail, Template } from '@/types/type' import { useQueryClient } from '@tanstack/react-query' @@ -53,6 +54,7 @@ import { BsTrash3 } from 'react-icons/bs' import { IoPlayCircleOutline } from 'react-icons/io5' import { useInterval, useKey } from 'react-use' import { toast } from 'sonner' +import { getSubmissionProgress } from '../../_libs/submissionProgress' import { useRunner } from '../TestcasePanel/useRunner' import { useTestPollingStore } from '../context/TestPollingStoreProvider' import { BackCautionDialog } from './BackCautionDialog' @@ -65,6 +67,7 @@ interface ProblemEditorProps { exerciseId?: number courseId?: number templateString: string + onSubmissionStart?: () => void } export function EditorHeader({ @@ -73,7 +76,8 @@ export function EditorHeader({ assignmentId, exerciseId, courseId, - templateString + templateString, + onSubmissionStart }: ProblemEditorProps) { const { language, setLanguage } = useLanguageStore( problem.id, @@ -86,17 +90,25 @@ export function EditorHeader({ const getCode = useCodeStore((state) => state.getCode) const isTesting = useTestPollingStore((state) => state.isTesting) + const setSubmissionProgress = useTestPollingStore( + (state) => state.setSubmissionProgress + ) const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false) const [selectedLanguage, setSelectedLanguage] = useState(language) const [isSubmitting, setIsSubmitting] = useState(false) const loading = isTesting || isSubmitting const [submissionId, setSubmissionId] = useState(null) + const pollingRequestRef = useRef(false) + const pollingGenerationRef = useRef(0) + const finishedSubmissionRef = useRef(null) + const pollingFailuresRef = useRef(0) + const nextPollAtRef = useRef(0) const [templateCode, setTemplateCode] = useState('') const [userName, setUserName] = useState('') const router = useRouter() const pathname = usePathname() - const confetti = typeof window !== 'undefined' ? new JSConfetti() : null + const confettiRef = useRef(null) const storageKey = useRef( getStorageKey( language, @@ -111,7 +123,6 @@ export function EditorHeader({ const session = useSession() const showSignIn = useAuthModalStore((state) => state.showSignIn) const [showModal, setShowModal] = useState(false) - //const pushed = useRef(false) const whereToPush = useRef('') const isModalConfrimed = useRef(false) @@ -123,51 +134,126 @@ export function EditorHeader({ const { isSidePanelHidden, toggleSidePanelVisibility } = useSidePanelTabStore() - const [isSubmitted, setIsSubmitted] = useState(false) - useSubmissionPolling({ - contestId, + const submissionSearchParams = { problemId: problem.id, - enabled: isSubmitted - }) + ...(contestId && { contestId }), + ...((assignmentId || exerciseId) && { + assignmentId: assignmentId || exerciseId + }) + } + + const invalidateSubmissionQueries = () => { + const targetAssignmentId = assignmentId || exerciseId + if (contestId) { + queryClient.invalidateQueries({ + queryKey: contestProblemQueries.lists(contestId) + }) + queryClient.invalidateQueries({ + queryKey: contestSubmissionQueries.lists({ + contestId, + problemId: problem.id + }) + }) + } else if (targetAssignmentId) { + queryClient.invalidateQueries({ + queryKey: assignmentProblemQueries.lists(targetAssignmentId) + }) + queryClient.invalidateQueries({ + queryKey: assignmentSubmissionQueries.lists({ + assignmentId: targetAssignmentId, + problemId: problem.id + }) + }) + } else { + queryClient.invalidateQueries({ + queryKey: problemSubmissionQueries.lists(problem.id) + }) + } + } + + useEffect(() => { + const generationRef = pollingGenerationRef + confettiRef.current = new JSConfetti() + return () => { + confettiRef.current?.destroyCanvas() + generationRef.current++ + } + }, []) useInterval( async () => { - // TODO: Implement assignment submission - const res = await fetcherWithAuth(`submission/${submissionId}`, { - searchParams: { - problemId: problem.id, - ...(contestId && { contestId }), - ...(assignmentId && { assignmentId }), - ...(exerciseId && { assignmentId: exerciseId }) - } - }) - if (res.ok) { - const submission: Submission = await res.json() - if (submission.result !== 'Judging') { - setIsSubmitting(false) + if ( + submissionId === null || + pollingRequestRef.current || + finishedSubmissionRef.current === submissionId || + Date.now() < nextPollAtRef.current + ) { + return + } - let href = '' - if (contestId) { - href = `/contest/${contestId}/problem/${problem.id}/submission/${submissionId}?cellProblemId=${problem.id}` - } else if (assignmentId) { - href = `/course/${courseId}/assignment/${assignmentId}/problem/${problem.id}/submission/${submissionId}` - } else if (exerciseId) { - href = `/course/${courseId}/exercise/${exerciseId}/problem/${problem.id}/submission/${submissionId}` - } else { - href = `/problem/${problem.id}/submission/${submissionId}` + const generation = pollingGenerationRef.current + pollingRequestRef.current = true + try { + const res = await fetcherWithAuth(`submission/${submissionId}`, { + cache: 'no-store', + searchParams: { + ...submissionSearchParams, + pollingTime: Date.now() } + }) + if (!res.ok) { + throw new Error(`Submission status request failed: ${res.status}`) + } + + const submission: SubmissionDetail = await res.json() + if (generation !== pollingGenerationRef.current) { + return + } - !contestId && router.replace(href as Route) - //window.history.pushState(null, '', window.location.href) + pollingFailuresRef.current = 0 + nextPollAtRef.current = 0 + const progress = getSubmissionProgress(submission) + setSubmissionProgress(progress) + + if (progress.stage === 'finished') { + finishedSubmissionRef.current = submissionId + setIsSubmitting(false) + invalidateSubmissionQueries() if (submission.result === 'Accepted') { - confetti?.addConfetti() + confettiRef.current?.addConfetti() } if (isSidePanelHidden) { toggleSidePanelVisibility() } } - } else { - setIsSubmitting(false) - toast.error('Please try again later.') + } catch { + if (generation !== pollingGenerationRef.current) { + return + } + + pollingFailuresRef.current++ + nextPollAtRef.current = + Date.now() + + Math.min( + 1000 * 2 ** Math.min(pollingFailuresRef.current - 1, 4), + 10000 + ) + if (pollingFailuresRef.current >= 5) { + setIsSubmitting(false) + setSubmissionProgress({ + stage: 'error', + completed: 0, + total: 0, + message: + '채점 상태를 확인하지 못했습니다. 제출 내역에서 결과를 확인해 주세요.' + }) + } + if (pollingFailuresRef.current === 1) { + toast.error('Unable to check submission status. Retrying...') + } + } finally { + if (generation === pollingGenerationRef.current) { + pollingRequestRef.current = false + } } }, isSubmitting && submissionId ? 500 : null @@ -237,11 +323,15 @@ export function EditorHeader({ } setActiveTestcaseTab(RUN_CODE_TAB) + setSubmissionProgress(null) storeCodeToLocalStorage(code) startRunner(code, language) } const submit = async () => { + if (loading) { + return + } const code = getCode() if (session === null) { @@ -255,81 +345,71 @@ export function EditorHeader({ return } + const generation = ++pollingGenerationRef.current + pollingRequestRef.current = false + finishedSubmissionRef.current = null + pollingFailuresRef.current = 0 + nextPollAtRef.current = 0 setSubmissionId(null) setIsSubmitting(true) - const res = await fetcherWithAuth.post('submission', { - json: { - language, - code: [ - { - id: 1, - text: code, - locked: false - } - ] - }, - searchParams: { - problemId: problem.id, - ...(contestId && { contestId }), - ...(assignmentId && { assignmentId }), - ...(exerciseId && { assignmentId: exerciseId }) - }, - next: { - revalidate: 0 + onSubmissionStart?.() + setActiveTestcaseTab(TESTCASE_RESULT_TAB) + setSubmissionProgress({ stage: 'waiting', completed: 0, total: 0 }) + try { + const res = await fetcherWithAuth.post('submission', { + json: { + language, + code: [ + { + id: 1, + text: code, + locked: false + } + ] + }, + searchParams: submissionSearchParams, + next: { + revalidate: 0 + } + }) + if (generation !== pollingGenerationRef.current) { + return } - }) - if (res.ok) { - toast.success('Successfully submitted the code') - storeCodeToLocalStorage(code) - const submission: Submission = await res.json() - - setSubmissionId(submission.id) - if (contestId) { - queryClient.invalidateQueries({ - queryKey: contestProblemQueries.lists(contestId) - }) - queryClient.invalidateQueries({ - queryKey: contestSubmissionQueries.lists({ - contestId, - problemId: problem.id - }) - }) - setIsSubmitted(true) - } else if (assignmentId) { - queryClient.invalidateQueries({ - queryKey: assignmentProblemQueries.lists(assignmentId) - }) - queryClient.invalidateQueries({ - queryKey: assignmentSubmissionQueries.lists({ - assignmentId, - problemId: problem.id - }) - }) - } else if (exerciseId) { - queryClient.invalidateQueries({ - queryKey: assignmentProblemQueries.lists(exerciseId) - }) - queryClient.invalidateQueries({ - queryKey: assignmentSubmissionQueries.lists({ - assignmentId: exerciseId, - problemId: problem.id - }) - }) + if (res.ok) { + toast.success('Successfully submitted the code') + storeCodeToLocalStorage(code) + const submission: Submission = await res.json() + + if (generation !== pollingGenerationRef.current) { + return + } + setSubmissionId(submission.id) + invalidateSubmissionQueries() } else { - queryClient.invalidateQueries({ - queryKey: problemSubmissionQueries.lists(problem.id) - }) + setIsSubmitting(false) + setSubmissionProgress(null) + if (res.status === 401) { + showSignIn() + toast.error('Log in first to submit your code') + } else if (res.status === 404) { + toast.error('Submission period has ended.') + } else { + toast.error('Please try again later.') + } } - } else { - setIsSubmitting(false) - if (res.status === 401) { - showSignIn() - toast.error('Log in first to submit your code') - } else if (res.status === 404) { - toast.error('Submission period has ended.') - } else { - toast.error('Please try again later.') + } catch { + if (generation !== pollingGenerationRef.current) { + return } + setIsSubmitting(false) + setSubmissionProgress({ + stage: 'error', + completed: 0, + total: 0, + message: + '제출 요청에 실패했습니다. 제출 내역을 확인한 뒤 다시 시도해 주세요.' + }) + toast.error('Please try again later.') } } @@ -391,24 +471,9 @@ export function EditorHeader({ // TODO: 배포 후 뒤로 가기 로직 재구현 - // const handlePopState = () => { - // if (!checkSaved()) { - // whereToPush.current = contestId - // ? `/contest/${contestId}/problem` - // : '/problem' - // setShowModal(true) - // } else window.history.back() - // } - // if (!pushed.current) { - // window.history.pushState(null, '', window.location.href) - // pushed.current = true - // } window.addEventListener('beforeunload', handleBeforeUnload) - //window.addEventListener('popstate', handlePopState) - return () => { window.removeEventListener('beforeunload', handleBeforeUnload) - //window.removeEventListener('popstate', handlePopState) } }, []) @@ -551,7 +616,7 @@ export function EditorHeader({ {contestId === undefined && ( - + +

{progress ? 'Previous submission result' : 'Test results'}

+ + ) +} + +it('clears the previous submission when the user starts a testcase test', async () => { + render( + + + + + + + + + ) + fireEvent.click(screen.getByRole('button', { name: 'Seed submission' })) + expect(screen.getByText('Previous submission result')).toBeTruthy() + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /^Test$/ })) + await Promise.resolve() + }) + expect(screen.queryByText('Previous submission result')).toBeNull() + expect(screen.getByText('Test results')).toBeTruthy() +}) diff --git a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/RunTestButton.tsx b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/RunTestButton.tsx index 6dfbbcabf7..ee11867a48 100644 --- a/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/RunTestButton.tsx +++ b/apps/frontend/app/(client)/(code-editor)/_components/EditorHeader/RunTestButton.tsx @@ -34,6 +34,9 @@ export function RunTestButton({ const session = useSession() const setIsTesting = useTestPollingStore((state) => state.setIsTesting) const startPolling = useTestPollingStore((state) => state.startPolling) + const setSubmissionProgress = useTestPollingStore( + (state) => state.setSubmissionProgress + ) const showSignIn = useAuthModalStore((state) => state.showSignIn) const getCode = useCodeStore((state) => state.getCode) const getUserTestcases = useTestcaseStore((state) => state.getUserTestcases) @@ -126,12 +129,13 @@ export function RunTestButton({ } setIsTesting(true) + setSubmissionProgress(null) mutate({ code, testcases }) } return ( - +