Skip to content

Commit

Permalink
Render quizzes HTML in the instructions panel (#800)
Browse files Browse the repository at this point in the history
closes #777

- Initial steps to render quizzes within `InstructionsPanel`
- `InstructionsPanel` checks if it has a quiz and, if so, renders it as
the step content
- `ProgressBar` is removed for the quiz

**Notes:**
- This PR renders quizzes but styling is not yet applied
- To check locally, the `target-practice` project has a quiz at step 5:
(http://localhost:3001/en/projects/target-practice/editor)
- Ensure your Projects branch is up to date with
[changes](RaspberryPiFoundation/projects-ui#2425)
made to pass `currentQuestion` data to the editor

---------

Co-authored-by: PetarSimonovic <PetarSimonovic@users.noreply.github.com>
Co-authored-by: Pete Simonovic <69108995+PetarSimonovic@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 4, 2023
1 parent f5167e2 commit 165d3c5
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- Quiz rendering in InstructionsPanel (#777)
- Styling for the task section of the instructions (#781)
- Styling for the instructions callouts (#788)
- Output styles for Instructions (#790)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useMemo } from "react";
import SidebarPanel from "../SidebarPanel";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
Expand All @@ -7,13 +7,19 @@ import "../../../../assets/stylesheets/Instructions.scss";
import "prismjs/plugins/highlight-keywords/prism-highlight-keywords.js";

const InstructionsPanel = () => {
const steps = useSelector((state) => state.instructions.project.steps);
const steps = useSelector((state) => state.instructions.project?.steps);
const quiz = useSelector((state) => state.instructions?.quiz);

const currentStepPosition = useSelector(
(state) => state.instructions.currentStepPosition,
);
const { t } = useTranslation();
const stepContent = useRef();

const isQuiz = useMemo(() => {
return !!quiz?.questionCount;
}, [quiz]);

const applySyntaxHighlighting = (container) => {
const codeElements = container.querySelectorAll(".language-python");

Expand All @@ -23,17 +29,23 @@ const InstructionsPanel = () => {
};

useEffect(() => {
if (steps[currentStepPosition]) {
const setStepContent = (content) => {
stepContent.current.parentElement.scrollTo({ top: 0 });
stepContent.current.innerHTML = steps[currentStepPosition].content;
stepContent.current.innerHTML = content;
applySyntaxHighlighting(stepContent.current);
};

if (isQuiz) {
setStepContent(quiz.questions[quiz.currentQuestion]);
} else if (steps[currentStepPosition]) {
setStepContent(steps[currentStepPosition].content);
}
}, [steps, currentStepPosition]);
}, [steps, currentStepPosition, quiz, isQuiz]);

return (
<SidebarPanel
heading={t("instructionsPanel.projectSteps")}
Footer={ProgressBar}
Footer={!isQuiz && ProgressBar}
>
<div className="project-instructions" ref={stepContent}></div>
</SidebarPanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,94 @@ window.Prism = {
highlightElement: jest.fn(),
};

beforeEach(() => {
const mockStore = configureStore([]);
const initialState = {
instructions: {
project: {
steps: [
{ content: "<p>step 0</p>" },
{
content:
"<p>step 1</p><code class='language-python'>print('hello')</code>",
},
],
describe("It renders project steps when there is no quiz", () => {
beforeEach(() => {
const mockStore = configureStore([]);
const initialState = {
instructions: {
project: {
steps: [
{ content: "<p>step 0</p>" },
{
content:
"<p>step 1</p><code class='language-python'>print('hello')</code>",
},
],
},
quiz: {},
currentStepPosition: 1,
},
currentStepPosition: 1,
},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<InstructionsPanel />
</Provider>,
);
});
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<InstructionsPanel />
</Provider>,
);
});

test("Renders with correct instruction step content", () => {
expect(screen.queryByText("step 1")).toBeInTheDocument();
});
test("Renders with correct instruction step content", () => {
expect(screen.queryByText("step 1")).toBeInTheDocument();
});

test("Scrolls instructions to the top", () => {
expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({
top: 0,
test("Scrolls instructions to the top", () => {
expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({
top: 0,
});
});

test("Renders the progress bar", () => {
expect(screen.queryByRole("progressbar")).toBeInTheDocument();
});

test("Applies syntax highlighting", () => {
const codeElement = document.getElementsByClassName("language-python")[0];
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
});
});

test("Applies syntax highlighting", () => {
const codeElement = document.getElementsByClassName("language-python")[0];
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
describe("It renders a quiz when it has one", () => {
beforeEach(() => {
const mockStore = configureStore([]);
const initialState = {
instructions: {
project: {
steps: [{ content: "<p>step 0</p>" }, { content: "<p>step 1</p>" }],
},
quiz: {
questions: [
"<h2>Test quiz</h2><p>step 1</p><code class='language-python'>print('hello')</code>",
],
questionCount: 1,
currentQuestion: 0,
},
currentStepPosition: 1,
},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<InstructionsPanel />
</Provider>,
);
});

test("Renders the quiz content", () => {
expect(screen.queryByText("Test quiz")).toBeInTheDocument();
});

test("Scrolls instructions to the top", () => {
expect(window.HTMLElement.prototype.scrollTo).toHaveBeenCalledWith({
top: 0,
});
});

test("Removes the progress bar", () => {
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
});

test("Applies syntax highlighting", () => {
const codeElement = document.getElementsByClassName("language-python")[0];
expect(window.Prism.highlightElement).toHaveBeenCalledWith(codeElement);
});
});
5 changes: 4 additions & 1 deletion src/redux/reducers/instructionsReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export const setCurrentStepPosition = (state, action) => {
state.currentStepPosition = action.payload;
};

export const reducers = { setInstructions, setCurrentStepPosition };
export const reducers = {
setInstructions,
setCurrentStepPosition,
};

0 comments on commit 165d3c5

Please sign in to comment.