Skip to content

Conversation

@kah-seng
Copy link

@kah-seng kah-seng commented Mar 4, 2025

Description

  • Added exam mode switch and exam mode preview to admin panel
  • Hides remote execution tab, Google Drive, GitHub, sessions, link sharing buttons when exam mode enabled
  • Prevent course switching/creation when exam mode enabled
  • Added dev tools detection/blocking
  • Added pausing of Source Academy and resume code functionality
  • Added tab switching detection
  • Added documentation tab for assessments

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Code quality improvements

How to test

Checklist

  • I have tested this code

@kah-seng kah-seng requested a review from RichDom2185 March 4, 2025 14:37
@kah-seng kah-seng self-assigned this Mar 4, 2025
Copy link
Member

@RichDom2185 RichDom2185 left a comment

Choose a reason for hiding this comment

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

Just some minor nits :), thanks!

@coveralls
Copy link

coveralls commented Mar 6, 2025

Pull Request Test Coverage Report for Build 18873593165

Details

  • 240 of 1070 (22.43%) changed or added relevant lines in 21 files are covered.
  • 2 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.4%) to 42.067%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/commons/application/actions/SessionActions.ts 5 8 62.5%
src/commons/assessmentWorkspace/AssessmentWorkspace.tsx 3 10 30.0%
src/pages/academy/Academy.tsx 0 8 0.0%
src/commons/pauseAcademyOverlay/PauseAcademyOverlay.tsx 4 17 23.53%
src/commons/documentation/SicpIndexPage.tsx 65 79 82.28%
src/pages/academy/adminPanel/AdminPanel.tsx 0 17 0.0%
src/commons/documentation/SicpToc.tsx 8 28 28.57%
src/commons/sagas/BackendSaga.ts 5 27 18.52%
src/commons/sagas/RequestsSaga.ts 6 29 20.69%
src/pages/academy/adminPanel/subcomponents/CourseConfigPanel.tsx 3 53 5.66%
Files with Coverage Reduction New Missed Lines %
src/pages/academy/adminPanel/AdminPanel.tsx 1 0.0%
src/pages/academy/adminPanel/subcomponents/CourseConfigPanel.tsx 1 3.96%
Totals Coverage Status
Change from base Build 18873229785: -0.4%
Covered Lines: 21321
Relevant Lines: 53091

💛 - Coveralls

@RichDom2185 RichDom2185 self-requested a review March 16, 2025 07:44
@iZUMi-kyouka iZUMi-kyouka self-assigned this Apr 8, 2025
@iZUMi-kyouka iZUMi-kyouka changed the title Added exam mode Add exam mode Jun 10, 2025
@RichDom2185 RichDom2185 requested a review from Copilot October 28, 2025 11:38
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds comprehensive exam mode functionality to Source Academy, allowing instructors to enable a restricted environment for students during assessments. The implementation includes admin controls for enabling/previewing exam mode, UI restrictions (hiding remote execution, file sharing, and course switching), security features (dev tools detection, tab switching monitoring), and a pause/resume mechanism with authentication.

Key Changes

  • Added exam mode toggle and preview functionality in admin panel with resume code authentication
  • Implemented client-side security features: dev tools blocking, context menu prevention, and tab focus monitoring
  • Added documentation tab with embedded SICP content for assessments
  • Created pause overlay system with resume code validation

Reviewed Changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/commons/application/Application.tsx Implements dev tools detection, tab switching monitoring, and pause overlay logic
src/pages/academy/adminPanel/subcomponents/CourseConfigPanel.tsx Adds exam mode toggle, resume code input, and preview mode button
src/pages/academy/adminPanel/AdminPanel.tsx Validates resume code on submit and updates course config
src/commons/sideContent/content/SideContentDocumentation.tsx New documentation tab component with iframe-based content
src/commons/application/types/SessionTypes.ts Extends types with exam mode fields (enableExamMode, resumeCode, isPaused)
src/commons/sagas/RequestsSaga.ts Adds API endpoints for resume code validation and user pause/focus tracking
src/commons/sagas/BackendSaga.ts Implements saga handlers for exam mode API calls
src/pages/playground/Playground.tsx Conditionally hides UI elements when exam mode is active
src/pages/academy/Academy.tsx Prevents course switching when exam mode is enabled
src/commons/dropdown/Dropdown*.tsx Disables course creation/switching dropdowns in exam mode
src/commons/pauseAcademyOverlay/PauseAcademyOverlay.tsx New pause overlay component with resume code input
src/styles/*.scss Styling for documentation tabs and pause overlay
Test files Updates mock data with new exam mode fields

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@RichDom2185
Copy link
Member

@sentry review

Comment on lines +213 to +223
// Detect when Source Academy tab's content are hidden (e.g., user changes tab while Source Academy is active)
document.addEventListener('visibilitychange', _ => {
if (document.visibilityState === 'hidden') {
dispatch(SessionActions.reportFocusLost());
} else {
showDangerMessage('Source Academy was out of focus.', 5000);
dispatch(SessionActions.reportFocusRegain());
}
});
}
}, [dispatch, enableExamMode, isPaused, hasSentPauseUserRequest, role, isPreviewExamMode]);
Copy link

Choose a reason for hiding this comment

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

Bug: The useEffect hook adds event listeners without cleanup, causing accumulation and memory growth on dependency changes.
Severity: CRITICAL | Confidence: 1.00

🔍 Detailed Analysis

The useEffect hook at src/commons/application/Application.tsx:171~223 adds contextmenu, keydown, and visibilitychange event listeners to the document. The effect's dependency array includes dispatch, enableExamMode, isPaused, hasSentPauseUserRequest, role, and isPreviewExamMode. When any of these dependencies change, the effect re-runs, adding new listeners without removing previously added ones. This leads to an unbounded accumulation of event handlers, causing performance degradation and increased memory usage over time.

💡 Suggested Fix

Add a cleanup function to the useEffect hook that removes the contextmenu, keydown, and visibilitychange event listeners when the component unmounts or dependencies change.

🤖 Prompt for AI Agent
Fix this bug. In src/commons/application/Application.tsx at lines 171-223: The
`useEffect` hook at `src/commons/application/Application.tsx:171~223` adds
`contextmenu`, `keydown`, and `visibilitychange` event listeners to the document. The
effect's dependency array includes `dispatch`, `enableExamMode`, `isPaused`,
`hasSentPauseUserRequest`, `role`, and `isPreviewExamMode`. When any of these
dependencies change, the effect re-runs, adding new listeners without removing
previously added ones. This leads to an unbounded accumulation of event handlers,
causing performance degradation and increased memory usage over time.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines 169 to +221

// Effect for dev tools blocking/detection when exam mode enabled
React.useEffect(() => {
if (role !== Role.Student && !isPreviewExamMode) {
return;
}

const showPauseAcademyOverlay = (reason: string) => {
setPauseAcademy(true);
setPauseAcademyReason(reason);
if (hasSentPauseUserRequest.current === false) {
dispatch(SessionActions.pauseUser());
hasSentPauseUserRequest.current = true;
}
};

if (enableExamMode || isPreviewExamMode) {
dispatch(SessionActions.reportFocusRegain());

if (isPaused !== undefined && isPaused) {
showPauseAcademyOverlay('Browser was refreshed when Source Academy was paused');
} else {
hasSentPauseUserRequest.current = false;
}

// Disable/Detect dev tools
disableDevtool({
ondevtoolopen: () => {
showPauseAcademyOverlay('Developer tools detected');
}
});

document.addEventListener('contextmenu', event => event.preventDefault());
document.addEventListener('keydown', event => {
if (
event.key === 'F12' ||
((event.key === 'I' || event.key === 'J' || event.key === 'C') &&
event.ctrlKey &&
event.shiftKey)
) {
event.preventDefault();
}
});

// Detect when Source Academy tab's content are hidden (e.g., user changes tab while Source Academy is active)
document.addEventListener('visibilitychange', _ => {
if (document.visibilityState === 'hidden') {
dispatch(SessionActions.reportFocusLost());
} else {
showDangerMessage('Source Academy was out of focus.', 5000);
dispatch(SessionActions.reportFocusRegain());
}
});
Copy link

Choose a reason for hiding this comment

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

The dev tools detection effect (lines 169-221) adds event listeners to the document without ever removing them. This causes memory leaks when the component unmounts or when the effect re-runs. Each time the effect runs, new listeners are added without removing the old ones, potentially accumulating dozens of event listeners. Add a cleanup function that removes all listeners, or better yet, use AbortController or WeakMaps to track and manage listeners.
Severity: HIGH

🤖 Prompt for AI Agent

Fix this code. In src/commons/application/Application.tsx#L169-L221: The dev tools
detection effect (lines 169-221) adds event listeners to the document without ever
removing them. This causes memory leaks when the component unmounts or when the effect
re-runs. Each time the effect runs, new listeners are added without removing the old
ones, potentially accumulating dozens of event listeners. Add a cleanup function that
removes all listeners, or better yet, use AbortController or WeakMaps to track and
manage listeners.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +89 to +104

type SearchData = {
indexTrie: TrieNode;
textTrie: TrieNode;
idToContentMap: Record<string, string>;
};

const fetchSearchData = () => {
const xhr = new XMLHttpRequest();
const url = Constants.sicpBackendUrl + 'json/rewritedSearchData.json';
xhr.open('GET', url, false); //sync download
xhr.send();
if (xhr.status !== 200) {
alert('Unable to get rewrited search data. Error code = ' + xhr.status + ' url is ' + url);
throw new Error('Unable to get search data. Error code = ' + xhr.status + ' url is ' + url);
} else {
Copy link

Choose a reason for hiding this comment

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

The fetchSearchData() function uses synchronous XMLHttpRequest (line 93: xhr.open('GET', url, false)). This blocks the entire UI thread while the request completes, which can cause the application to become unresponsive. This is a significant performance anti-pattern. Replace this with an asynchronous fetch API and move the data fetching to a useEffect hook, or use a proper data loading library. The synchronous XHR also prevents proper error handling and timeout mechanisms.
Severity: HIGH

🤖 Prompt for AI Agent

Fix this code. In src/commons/documentation/SicpNavigationBar.tsx#L89-L104: The
`fetchSearchData()` function uses synchronous XMLHttpRequest (line 93: `xhr.open('GET',
url, false)`). This blocks the entire UI thread while the request completes, which can
cause the application to become unresponsive. This is a significant performance
anti-pattern. Replace this with an asynchronous fetch API and move the data fetching to
a useEffect hook, or use a proper data loading library. The synchronous XHR also
prevents proper error handling and timeout mechanisms.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +62 to +76
setActivePage(pages[index]);
};

const handleDocsHome = useCallback(() => {
if (sicpHomeCallbackFn !== null && activePage.src === 'https://sicp.sourceacademy.org') {
sicpHomeCallbackFn();
}

if (activeIframeRef.current !== null) {
activeIframeRef.current.src = activePage.src;
}
}, [activePage.src]);

const sicpHomeCallbackSetter = (fn: () => void) => {
sicpHomeCallbackFn = fn;
Copy link

Choose a reason for hiding this comment

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

The pages array is being mutated by pushing a new SICP JS page (line 76) outside of state management. This violates React's principles - the pages array is created fresh on each render, so the SICP JS page will be added repeatedly. Move the SICP JS page into a proper useState hook or create the pages array within useMemo to ensure it's handled correctly during re-renders.
Severity: MEDIUM

🤖 Prompt for AI Agent

Fix this code. In src/commons/sideContent/content/SideContentDocumentation.tsx#L62-L76:
The `pages` array is being mutated by pushing a new SICP JS page (line 76) outside of
state management. This violates React's principles - the `pages` array is created fresh
on each render, so the SICP JS page will be added repeatedly. Move the SICP JS page into
a proper useState hook or create the pages array within useMemo to ensure it's handled
correctly during re-renders.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +23 to +27
onChange={e => setResumeCode((e.target as HTMLInputElement).value)}
/>
</FormGroup>
<Button text="Submit" onClick={() => props.onSubmit(resumeCode)} />
</div>
Copy link

Choose a reason for hiding this comment

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

The InputGroup component receives an onChange handler that casts the target to HTMLInputElement, but defaultValue should be value with the state variable. Using defaultValue creates an uncontrolled component, which means the input won't properly sync with the state. Change defaultValue="" to value={resumeCode} to make this a controlled component. Additionally, consider adding a maxLength attribute to prevent excessively long resume codes.
Severity: MEDIUM

🤖 Prompt for AI Agent

Fix this code. In src/commons/pauseAcademyOverlay/PauseAcademyOverlay.tsx#L23-L27: The
`InputGroup` component receives an `onChange` handler that casts the target to
`HTMLInputElement`, but `defaultValue` should be `value` with the state variable. Using
`defaultValue` creates an uncontrolled component, which means the input won't properly
sync with the state. Change `defaultValue=""` to `value={resumeCode}` to make this a
controlled component. Additionally, consider adding a `maxLength` attribute to prevent
excessively long resume codes.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +19 to +24
Tree.nodeFromPath(path, newState).isExpanded = true;
setSidebarContent(newState);
};

const handleNodeCollapse = (_node: TreeNodeInfo, path: integer[]) => {
const newState = cloneDeep(sidebarContent);
Copy link

Choose a reason for hiding this comment

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

The type annotation integer[] on lines 19 and 24 is not a valid TypeScript type. It should be number[]. This will cause TypeScript compilation errors. Replace integer with number.
Severity: CRITICAL

💡 Suggested Fix

Suggested change
Tree.nodeFromPath(path, newState).isExpanded = true;
setSidebarContent(newState);
};
const handleNodeCollapse = (_node: TreeNodeInfo, path: integer[]) => {
const newState = cloneDeep(sidebarContent);
const handleNodeExpand = (_node: TreeNodeInfo, path: number[]) => {

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +170 to +172
// Effect for dev tools blocking/detection when exam mode enabled
React.useEffect(() => {
if (role !== Role.Student && !isPreviewExamMode) {
Copy link

Choose a reason for hiding this comment

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

The exam mode check on line 170 uses role !== Role.Student && !isPreviewExamMode which returns early. This means non-students won't set up dev tools detection unless in preview mode. The logic seems backwards - you probably want to trigger detection for students OR when in preview mode. The condition should likely be if (role === Role.Student || isPreviewExamMode) before setting up the detection. Please verify the intended behavior.
Severity: MEDIUM

🤖 Prompt for AI Agent

Fix this code. In src/commons/application/Application.tsx#L170-L172: The exam mode check
on line 170 uses `role !== Role.Student && !isPreviewExamMode` which returns early. This
means non-students won't set up dev tools detection unless in preview mode. The logic
seems backwards - you probably want to trigger detection for students OR when in preview
mode. The condition should likely be `if (role === Role.Student || isPreviewExamMode)`
before setting up the detection. Please verify the intended behavior.

Did we get this right? 👍 / 👎 to inform future reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants