-
Notifications
You must be signed in to change notification settings - Fork 0
Async test result #549
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Async test result #549
Changes from all commits
3937ba8
4faf254
17bfa54
0b6d129
522ce3c
d6855b4
eb4050a
5a3e583
80f2dab
dfac775
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| import { UserDropDownMenu } from "@cb/components/navigator/menu/UserDropDownMenu"; | ||
| import CreateRoomLoadingPanel from "@cb/components/panel/editor/CreateRoomLoadingPanel"; | ||
| import { CodeTab, TestTab } from "@cb/components/panel/editor/tab"; | ||
| import { | ||
| CodeTab, | ||
| TestResultTab, | ||
| TestTab, | ||
| } from "@cb/components/panel/editor/tab"; | ||
| import { Tooltip } from "@cb/components/tooltip"; | ||
| import { SkeletonWrapper } from "@cb/components/ui/SkeletonWrapper"; | ||
| import { | ||
|
|
@@ -26,7 +30,8 @@ const EditorPanel = () => { | |
| const { selectedPeer, peers } = usePeers(); | ||
| const { self } = useRoomData(); | ||
| const roomStatus = useRoomStatus(); | ||
| const { selectTest, toggleCodeVisibility } = usePeerActions(); | ||
| const { selectTest, selectTestResult, toggleCodeVisibility } = | ||
| usePeerActions(); | ||
| const { getLanguageExtension } = useLeetCodeActions(); | ||
|
|
||
| const url = self?.url ?? ""; | ||
|
|
@@ -38,6 +43,11 @@ const EditorPanel = () => { | |
| const upperTabConfigs = React.useMemo(() => { | ||
| const extension = | ||
| getLanguageExtension(selectedPeer?.questions[url]?.code?.language) ?? ""; | ||
|
|
||
| const activeTestResult = selectedPeer?.questions[url]?.testResults.find( | ||
| (testResult) => testResult.selected | ||
| ); | ||
|
|
||
| return [ | ||
| { | ||
| value: "code", | ||
|
|
@@ -57,8 +67,27 @@ const EditorPanel = () => { | |
| /> | ||
| ), | ||
| }, | ||
| { | ||
| value: "testResult", | ||
| label: "Test Result", | ||
| Icon: FlaskConical, | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we change the icon to https://lucide.dev/icons/terminal |
||
| Content: ( | ||
| <TestResultTab | ||
| activePeer={selectedPeer} | ||
| activeTestResult={activeTestResult} | ||
| selectTestResult={selectTestResult} | ||
| /> | ||
| ), | ||
| }, | ||
| ]; | ||
| }, [selectedPeer, activeTest, selectTest, getLanguageExtension, url]); | ||
| }, [ | ||
| selectedPeer, | ||
| activeTest, | ||
| selectTest, | ||
| selectTestResult, | ||
| getLanguageExtension, | ||
| url, | ||
| ]); | ||
|
|
||
| const hideCode = !selectedPeer?.questions[self?.url ?? ""]?.viewable; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||
| import checkIcon from "@cb/assets/accepted_icon.png"; | ||||||
| import xIcon from "@cb/assets/wrong_answer_icon.png"; | ||||||
| import { SkeletonWrapper } from "@cb/components/ui/SkeletonWrapper"; | ||||||
| import { useRoomData } from "@cb/hooks/store"; | ||||||
| import { | ||||||
| Identifiable, | ||||||
| PeerState, | ||||||
| ResultAssignment, | ||||||
| SelectableTestResult, | ||||||
| } from "@cb/types"; | ||||||
| import React from "react"; | ||||||
| import { | ||||||
| CompileErrorResult, | ||||||
| InvalidTestCaseResult, | ||||||
| MemoryLimitExceededResult, | ||||||
| RuntimeErrorResult, | ||||||
| TestResultDisplay, | ||||||
| TestResultStatus, | ||||||
| TimeLimitExceededResult, | ||||||
| } from "./testResultComponents"; | ||||||
|
|
||||||
| const STATUS_CONFIG: Record< | ||||||
| TestResultStatus, | ||||||
| { label: string; className: string } | ||||||
| > = { | ||||||
| Accepted: { label: "Accepted", className: "text-green-500" }, | ||||||
| "Wrong Answer": { label: "Wrong Answer", className: "text-red-500" }, | ||||||
| "Time Limit Exceeded": { | ||||||
| label: "Time Limit Exceeded", | ||||||
| className: "text-red-500", | ||||||
| }, | ||||||
| "Invalid Test Case": { | ||||||
| label: "Invalid Test Case", | ||||||
| className: "text-red-500", | ||||||
| }, | ||||||
| "Runtime Error": { label: "Runtime Error", className: "text-red-500" }, | ||||||
| "Compile Error": { label: "Compile Error", className: "text-red-500" }, | ||||||
| "Memory Limit Exceeded": { | ||||||
| label: "Memory Limit Exceeded", | ||||||
| className: "text-red-500", | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
| const renderTestResultContent = (activeTestResult: SelectableTestResult) => { | ||||||
| const status = activeTestResult.testResultStatus as TestResultStatus; | ||||||
|
|
||||||
| switch (status) { | ||||||
| case "Accepted": | ||||||
| return <TestResultDisplay activeTestResult={activeTestResult} />; | ||||||
| case "Wrong Answer": | ||||||
| return <TestResultDisplay activeTestResult={activeTestResult} />; | ||||||
| case "Compile Error": | ||||||
| return <CompileErrorResult activeTestResult={activeTestResult} />; | ||||||
| case "Runtime Error": | ||||||
| return <RuntimeErrorResult activeTestResult={activeTestResult} />; | ||||||
| case "Time Limit Exceeded": | ||||||
| return <TimeLimitExceededResult activeTestResult={activeTestResult} />; | ||||||
| case "Memory Limit Exceeded": | ||||||
| return <MemoryLimitExceededResult activeTestResult={activeTestResult} />; | ||||||
| case "Invalid Test Case": | ||||||
| return <InvalidTestCaseResult activeTestResult={activeTestResult} />; | ||||||
| default: | ||||||
| return null; | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| interface TestResultTabProps { | ||||||
| activePeer: Identifiable<PeerState> | undefined; | ||||||
| activeTestResult: SelectableTestResult | undefined; | ||||||
| selectTestResult: (index: number) => void; | ||||||
| } | ||||||
|
|
||||||
| export const TestResultTab: React.FC<TestResultTabProps> = ({ | ||||||
| activePeer, | ||||||
| activeTestResult, | ||||||
| selectTestResult, | ||||||
| }) => { | ||||||
| const { self } = useRoomData(); | ||||||
| const testResults = activePeer?.questions[self?.url ?? ""]?.testResults ?? []; | ||||||
|
|
||||||
| const getStatusBadge = (status: TestResultStatus) => { | ||||||
| const config = STATUS_CONFIG[status] ?? { | ||||||
| label: status, | ||||||
| className: "text-label-1 dark:text-dark-label-1", | ||||||
| }; | ||||||
| return <span className={config.className}>{config.label}</span>; | ||||||
| }; | ||||||
|
|
||||||
| return ( | ||||||
| <SkeletonWrapper loading={false} className="relative"> | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we wrap this in error boundary? https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary. Reasoning is that this feature is quite complex with a lot of array indexing and handling, so we don't want to break other working components |
||||||
| <div className="p-5 flex flex-col space-y-4 h-full w-full overflow-scroll hide-scrollbar"> | ||||||
| <div className="flex w-full flex-col items-start justify-between gap-4"> | ||||||
| <div className="text-label-1 dark:text-dark-label-1 text-xl"> | ||||||
| {activeTestResult && | ||||||
| getStatusBadge( | ||||||
| activeTestResult.testResultStatus as TestResultStatus | ||||||
| )} | ||||||
| </div> | ||||||
| <div className="hide-scrollbar flex flex-nowrap items-center gap-x-2 gap-y-4 overflow-x-scroll"> | ||||||
| {(activeTestResult?.testResultStatus === "Accepted" || | ||||||
| activeTestResult?.testResultStatus === "Wrong Answer") && | ||||||
| testResults.map((test: SelectableTestResult, idx: number) => { | ||||||
| const passed = (test.testResult ?? []).every( | ||||||
| (r: ResultAssignment) => r.output === r.expected | ||||||
| ); | ||||||
| const selected = !!test.selected; | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not necessary right? |
||||||
| const baseClasses = | ||||||
| "relative inline-flex items-center whitespace-nowrap rounded-lg px-4 py-1 text-sm font-semibold focus:outline-none"; | ||||||
| const selectedClasses = | ||||||
| "bg-fill-3 dark:bg-dark-fill-3 hover:bg-fill-2 dark:hover:bg-dark-fill-2 hover:text-label-1 dark:hover:text-dark-label-1 text-label-1 dark:text-dark-label-1"; | ||||||
| const unselectedClasses = | ||||||
| "hover:bg-fill-2 dark:hover:bg-dark-fill-2 text-label-2 dark:text-dark-label-2 hover:text-label-1 dark:hover:text-dark-label-1 dark:bg-dark-transparent bg-transparent"; | ||||||
| return ( | ||||||
| <div key={idx} onClick={() => selectTestResult(idx)}> | ||||||
| <button | ||||||
| className={`${baseClasses} ${selected ? selectedClasses : unselectedClasses} gap-2`} | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| > | ||||||
| <img | ||||||
| src={passed ? checkIcon : xIcon} | ||||||
| alt={passed ? "passed" : "failed"} | ||||||
| className="w-3 h-3 rounded-sm" | ||||||
| /> | ||||||
| Case {idx + 1} | ||||||
| </button> | ||||||
| </div> | ||||||
| ); | ||||||
| })} | ||||||
| </div> | ||||||
| </div> | ||||||
| {activeTestResult && renderTestResultContent(activeTestResult)} | ||||||
| </div> | ||||||
| </SkeletonWrapper> | ||||||
| ); | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { CodeTab } from "@cb/components/panel/editor/tab/CodeTab"; | ||
| export { TestResultTab } from "@cb/components/panel/editor/tab/TestResultTab"; | ||
| export { TestTab } from "@cb/components/panel/editor/tab/TestTab"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import React from "react"; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rename |
||
| import { ErrorMessage } from "./sharedComponents"; | ||
| import { TestResultContentProps } from "./types"; | ||
|
|
||
| export const CompileErrorResult: React.FC<TestResultContentProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <ErrorMessage message={activeTestResult.errorMessage} /> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import React from "react"; | ||
| import { ErrorMessage } from "./sharedComponents"; | ||
| import { TestResultContentProps } from "./types"; | ||
|
|
||
| export const InvalidTestCaseResult: React.FC<TestResultContentProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <div className="text-label-3 dark:text-dark-label-3 text-xs font-medium"> | ||
| Case {activeTestResult.invalidTestCaseIdx} | ||
| </div> | ||
| <ErrorMessage message={activeTestResult.errorMessage} /> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import React from "react"; | ||
| import { LastExecutedInput } from "./sharedComponents"; | ||
| import { TestResultContentProps } from "./types"; | ||
|
|
||
| export const MemoryLimitExceededResult: React.FC<TestResultContentProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <LastExecutedInput activeTestResult={activeTestResult} /> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import React from "react"; | ||
| import { ErrorMessage, LastExecutedInput } from "./sharedComponents"; | ||
| import { TestResultContentProps } from "./types"; | ||
|
|
||
| export const RuntimeErrorResult: React.FC<TestResultContentProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <ErrorMessage message={activeTestResult.errorMessage} /> | ||
| <LastExecutedInput activeTestResult={activeTestResult} /> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { ResultAssignment, SelectableTestResult } from "@cb/types"; | ||
| import React from "react"; | ||
| import { | ||
| ExpectedDisplay, | ||
| InputDisplay, | ||
| OutputDisplay, | ||
| } from "./sharedComponents"; | ||
|
|
||
| interface TestResultDisplayProps { | ||
| activeTestResult: SelectableTestResult; | ||
| } | ||
|
|
||
| export const TestResultDisplay: React.FC<TestResultDisplayProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <div> | ||
| <div className="flex h-full w-full flex-col space-y-2"> | ||
| {activeTestResult.testResult?.map( | ||
| (testResult: ResultAssignment, testIdx: number) => ( | ||
| <React.Fragment key={testIdx}> | ||
| {Array.isArray(testResult.input) && ( | ||
| <InputDisplay inputs={testResult.input} /> | ||
| )} | ||
| <OutputDisplay output={testResult.output} /> | ||
| <ExpectedDisplay expected={testResult.expected} /> | ||
| </React.Fragment> | ||
| ) | ||
| ) ?? null} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import React from "react"; | ||
| import { LastExecutedInput } from "./sharedComponents"; | ||
| import { TestResultContentProps } from "./types"; | ||
|
|
||
| export const TimeLimitExceededResult: React.FC<TestResultContentProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <LastExecutedInput activeTestResult={activeTestResult} /> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { ResultAssignment } from "@cb/types"; | ||
| import React from "react"; | ||
| import { TestResultContentProps } from "./types"; | ||
|
|
||
| export const WrongAnswerResult: React.FC<TestResultContentProps> = ({ | ||
| activeTestResult, | ||
| }) => { | ||
| return ( | ||
| <div className="space-y-4 pb-12"> | ||
| <div> | ||
| <div className="flex h-full w-full flex-col space-y-2"> | ||
| {activeTestResult.testResult?.map( | ||
| (testResult: ResultAssignment, testIdx: number) => ( | ||
| <React.Fragment key={testIdx}> | ||
| <div className="text-label-3 dark:text-dark-label-3 text-xs font-medium"> | ||
| Input | ||
| </div> | ||
| {Array.isArray(testResult.input) && | ||
| testResult.input.map((input, idx) => ( | ||
| <React.Fragment key={idx}> | ||
| <div className="font-menlo bg-fill-3 dark:bg-dark-fill-3 w-full cursor-text rounded-lg border border-transparent px-3 py-[10px]"> | ||
| <div className="font-menlo placeholder:text-label-4 dark:placeholder:text-dark-label-4 sentry-unmask w-full resize-none whitespace-pre-wrap break-words outline-none"> | ||
| {input.variable | ||
| ? `${input.variable}=${input.value}` | ||
| : input.value} | ||
| </div> | ||
| </div> | ||
| </React.Fragment> | ||
| ))} | ||
|
|
||
| {/* Output */} | ||
| <div className="text-label-3 dark:text-dark-label-3 text-xs font-medium"> | ||
| Output | ||
| </div> | ||
| <div className="font-menlo bg-fill-3 dark:bg-dark-fill-3 w-full cursor-text rounded-lg border border-transparent px-3 py-[10px]"> | ||
| <div className="font-menlo placeholder:text-label-4 dark:placeholder:text-dark-label-4 sentry-unmask w-full resize-none whitespace-pre-wrap break-words outline-none"> | ||
| {testResult.output ?? "-"} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Expected */} | ||
| <div className="text-label-3 dark:text-dark-label-3 text-xs font-medium"> | ||
| Expected | ||
| </div> | ||
| <div className="font-menlo bg-fill-3 dark:bg-dark-fill-3 w-full cursor-text rounded-lg border border-transparent px-3 py-[10px]"> | ||
| <div className="font-menlo placeholder:text-label-4 dark:placeholder:text-dark-label-4 sentry-unmask w-full resize-none whitespace-pre-wrap break-words outline-none"> | ||
| {testResult.expected ?? "-"} | ||
| </div> | ||
| </div> | ||
| </React.Fragment> | ||
| ) | ||
| ) ?? null} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| export { CompileErrorResult } from "./CompileErrorResult"; | ||
| export { InvalidTestCaseResult } from "./InvalidTestCaseResult"; | ||
| export { MemoryLimitExceededResult } from "./MemoryLimitExceededResult"; | ||
| export { RuntimeErrorResult } from "./RuntimeErrorResult"; | ||
| export * from "./sharedComponents"; | ||
| export { TestResultDisplay } from "./TestResultDisplay"; | ||
| export { TimeLimitExceededResult } from "./TimeLimitExceededResult"; | ||
| export * from "./types"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use
luicide-reactfor the icons?