Skip to content

Commit

Permalink
feat(fe): implement test in code editor (#2093)
Browse files Browse the repository at this point in the history
* feat(fe): add test button, implement poll request

* feat(fe): implement testcase panel with dummy data

* chore(fe): edit table row style

* feat(fe): store test result

* chore(fe): delete console.log

* feat(fe): show wrong testcase numbers, adjust table style

* feat(fe): implement testcase tabs

* feat(fe): edit tab style

* feat(fe): implement test result detail

* feat(fe): implement delete tab button

* fix(fe): fix testcase tab style

* refactor(fe): componentize TestcaseTab TestSummary TestResultDetail

* feat(fe): add output in testcase result

* chore(fe): specify type statement

Co-authored-by: Eunbi Kang <[email protected]>

* chore(fe): execute prettier

* chore(fe): add padding bottom to panel, add ellipsis to long input output

* chore(fe): add pre wrap for labeled field

* chore(fe): simplify long styles using truncate

---------

Co-authored-by: Jaehyeon Kim <[email protected]>
Co-authored-by: Eunbi Kang <[email protected]>
  • Loading branch information
3 people authored Oct 6, 2024
1 parent 9eb1887 commit 6ec6c5b
Show file tree
Hide file tree
Showing 8 changed files with 532 additions and 24 deletions.
98 changes: 89 additions & 9 deletions apps/frontend/components/EditorHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { fetcherWithAuth } from '@/lib/utils'
import submitIcon from '@/public/submit.svg'
import useAuthModalStore from '@/stores/authModal'
import {
TestResultsContext,
useLanguageStore,
getKey,
setItem,
Expand All @@ -35,7 +36,8 @@ import type {
Language,
ProblemDetail,
Submission,
Template
Template,
TestResult
} from '@/types/type'
import JSConfetti from 'js-confetti'
import { Save } from 'lucide-react'
Expand All @@ -44,7 +46,7 @@ import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { useContext, useEffect, useRef, useState } from 'react'
import { BsTrash3 } from 'react-icons/bs'
//import { IoPlayCircleOutline } from 'react-icons/io5'
import { IoPlayCircleOutline } from 'react-icons/io5'
import { useInterval } from 'react-use'
import { toast } from 'sonner'
import { useStore } from 'zustand'
Expand All @@ -61,9 +63,12 @@ export default function Editor({
templateString
}: ProblemEditorProps) {
const { language, setLanguage } = useLanguageStore()
const store = useContext(CodeContext)
if (!store) throw new Error('CodeContext is not provided')
const { code, setCode } = useStore(store)
const codeStore = useContext(CodeContext)
if (!codeStore) throw new Error('CodeContext is not provided')
const { code, setCode } = useStore(codeStore)
const testResultStore = useContext(TestResultsContext)
if (!testResultStore) throw new Error('TestResultsContext is not provided')
const { setTestResults } = useStore(testResultStore)
const [loading, setLoading] = useState(false)
const [submissionId, setSubmissionId] = useState<number | null>(null)
const [templateCode, setTemplateCode] = useState<string | null>(null)
Expand Down Expand Up @@ -165,6 +170,82 @@ export default function Editor({
}
}

const submitTest = async () => {
if (code === '') {
toast.error('Please write code before test')
return
}
setLoading(true)
const res = await fetcherWithAuth.post('submission/test', {
json: {
language,
code: [
{
id: 1,
text: code,
locked: false
}
]
},
searchParams: {
problemId: problem.id
},
next: {
revalidate: 0
}
})
if (res.ok) {
pollTestResult()
} else {
setLoading(false)
if (res.status === 401) {
showSignIn()
toast.error('Log in first to test your code')
} else toast.error('Please try again later.')
}
}

const pollTestResult = async () => {
let attempts = 0
const maxAttempts = 10
const pollingInterval = 2000

const poll = async () => {
const res = await fetcherWithAuth.get('submission/test', {
next: {
revalidate: 0
}
})

if (res.ok) {
const resultArray: TestResult[] = await res.json()

setTestResults(resultArray)

const allJudged = resultArray.every(
(submission: TestResult) => submission.result !== 'Judging'
)

if (!allJudged) {
if (attempts < maxAttempts) {
attempts += 1
setTimeout(poll, pollingInterval)
} else {
setLoading(false)
toast.error('Judging took too long. Please try again later.')
}
} else {
setLoading(false)
}
} else {
setLoading(false)
toast.error('Please try again later.')
}
}

poll()
}

const saveCode = async () => {
const session = await auth()
if (!session) {
Expand Down Expand Up @@ -242,16 +323,15 @@ export default function Editor({
<Save className="stroke-1" size={22} />
Save
</Button>
{/* TODO: Add Test function
<Button
variant={'secondary'}
variant="secondary"
className="h-8 shrink-0 gap-1 rounded-[4px] border-none bg-[#D7E5FE] px-2 font-normal text-[#484C4D] hover:bg-[#c6d3ea]"
onClick={submitTest}
disabled={loading}
>
<IoPlayCircleOutline size={22} />
Test
</Button>
*/}
<Button
className="h-8 shrink-0 gap-1 rounded-[4px] px-2 font-normal"
disabled={loading}
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/components/EditorLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import EditorResizablePanel from '@/components/EditorResizablePanel'
import HeaderAuthPanel from '@/components/auth/HeaderAuthPanel'
import { auth } from '@/lib/auth'
import { convertToLetter, fetcher, fetcherWithAuth } from '@/lib/utils'
Expand All @@ -9,6 +8,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { FaSortDown } from 'react-icons/fa'
import ContestStatusTimeDiff from './ContestStatusTimeDiff'
import EditorResizablePanelWithContext from './EditorResizablePanelWithContext'
import {
DropdownMenu,
DropdownMenuContent,
Expand Down Expand Up @@ -106,13 +106,13 @@ export default async function EditorLayout({
<HeaderAuthPanel session={session} group={'editor'} />
</div>
</header>
<EditorResizablePanel
<EditorResizablePanelWithContext
contest={contest}
problem={problem}
contestId={contestId}
enableCopyPaste={contest ? contest.enableCopyPaste : true}
>
{children}
</EditorResizablePanel>
</EditorResizablePanelWithContext>
</div>
)
}
61 changes: 51 additions & 10 deletions apps/frontend/components/EditorResizablePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import {
ResizablePanel,
ResizablePanelGroup
} from '@/components/ui/resizable'
import { ScrollArea } from '@/components/ui/scroll-area'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { CodeContext, createCodeStore, useLanguageStore } from '@/stores/editor'
import {
CodeContext,
TestResultsContext,
createCodeStore,
useLanguageStore
} from '@/stores/editor'
import type { Language, ProblemDetail } from '@/types/type'
import type { Route } from 'next'
import Link from 'next/link'
Expand All @@ -17,6 +22,7 @@ import { Suspense, useContext, useEffect } from 'react'
import { useStore } from 'zustand'
import Loading from '../app/problem/[problemId]/loading'
import EditorHeader from './EditorHeader'
import TestcasePanel from './TestcasePanel'

interface ProblemEditorProps {
problem: ProblemDetail
Expand All @@ -34,7 +40,21 @@ export default function EditorMainResizablePanel({
const pathname = usePathname()
const base = contestId ? `/contest/${contestId}` : ''
const { language, setLanguage } = useLanguageStore()
const store = createCodeStore()
const codeStore = createCodeStore()
const testResultStore = useContext(TestResultsContext)
if (!testResultStore) throw new Error('TestResultsContext is not provided')
const { testResults } = useStore(testResultStore)
const testcases = problem.problemTestcase
const testResultData =
testResults.length > 0
? testcases.map((testcase, index) => ({
id: testcase.id,
input: testcase.input,
expectedOutput: testcase.output,
output: testResults[index]?.output,
result: testResults[index]?.result
}))
: null
useEffect(() => {
if (!problem.languages.includes(language)) {
setLanguage(problem.languages[0])
Expand Down Expand Up @@ -91,16 +111,38 @@ export default function EditorMainResizablePanel({

<ResizablePanel defaultSize={65} className="bg-[#222939]">
<div className="grid-rows-editor grid h-full">
<CodeContext.Provider value={store}>
<CodeContext.Provider value={codeStore}>
<EditorHeader
problem={problem}
contestId={contestId}
templateString={problem.template[0]}
/>
<CodeEditorInEditorResizablePanel
templateString={problem.template[0]}
enableCopyPaste={enableCopyPaste}
/>

<ResizablePanelGroup direction="vertical" className="h-32">
<ResizablePanel
defaultSize={60}
className="!overflow-x-auto !overflow-y-auto"
>
<ScrollArea className="h-full bg-[#121728]">
<CodeEditorInEditorResizablePanel
enableCopyPaste={enableCopyPaste}
/>
<ScrollBar orientation="horizontal" />
<ScrollBar orientation="vertical" />
</ScrollArea>
</ResizablePanel>
{testResultData && (
<>
<ResizableHandle
withHandle
className="border-[0.5px] border-slate-700"
/>
<ResizablePanel defaultSize={40}>
<TestcasePanel testResult={testResultData} />
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</CodeContext.Provider>
</div>
</ResizablePanel>
Expand All @@ -109,7 +151,6 @@ export default function EditorMainResizablePanel({
}

interface CodeEditorInEditorResizablePanelProps {
templateString: string
enableCopyPaste: boolean
}

Expand All @@ -123,7 +164,7 @@ function CodeEditorInEditorResizablePanel({

return (
<CodeEditor
value={code}
value={code ?? ''}
language={language as Language}
onChange={setCode}
enableCopyPaste={enableCopyPaste}
Expand Down
31 changes: 31 additions & 0 deletions apps/frontend/components/EditorResizablePanelWithContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client'

import EditorResizablePanel from '@/components/EditorResizablePanel'
import { TestResultsContext, createTestResultsStore } from '@/stores/editor'
import type { Contest, ProblemDetail } from '@/types/type'

export default function EditorResizablePanelWithContext({
problem,
contestId,
contest,
children
}: {
problem: ProblemDetail
contestId?: number | undefined
contest?: Contest | undefined
children: React.ReactNode
}) {
const testResultsStore = createTestResultsStore(problem.id, contestId)

return (
<TestResultsContext.Provider value={testResultsStore}>
<EditorResizablePanel
problem={problem}
contestId={contestId}
enableCopyPaste={contest ? contest.enableCopyPaste : true}
>
{children}
</EditorResizablePanel>
</TestResultsContext.Provider>
)
}
Loading

0 comments on commit 6ec6c5b

Please sign in to comment.