Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added extension/src/assets/accepted_icon.png
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use luicide-react for the icons?

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/src/assets/wrong_answer_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 32 additions & 3 deletions extension/src/components/panel/editor/EditorPanel.tsx
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 {
Expand All @@ -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 ?? "";
Expand All @@ -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",
Expand All @@ -57,8 +67,27 @@ const EditorPanel = () => {
/>
),
},
{
value: "testResult",
label: "Test Result",
Icon: FlaskConical,
Copy link
Owner

Choose a reason for hiding this comment

The 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;

Expand Down
134 changes: 134 additions & 0 deletions extension/src/components/panel/editor/tab/TestResultTab.tsx
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">
Copy link
Owner

Choose a reason for hiding this comment

The 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;
Copy link
Owner

Choose a reason for hiding this comment

The 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`}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
className={`${baseClasses} ${selected ? selectedClasses : unselectedClasses} gap-2`}
className={cn(baseClasses, { selectedClasses: selected, unselectedClasses: !selected )}

>
<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>
);
};
1 change: 1 addition & 0 deletions extension/src/components/panel/editor/tab/index.ts
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";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rename testResultComponents to TestResults

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";
Loading
Loading