Skip to content

Commit

Permalink
Downloading the instructions (#1160)
Browse files Browse the repository at this point in the history
## What's Changed?

- Includes instructions in the project download if there are any
- Adds a list of reserved file names which includes `INSTRUCTIONS.md` to
prevent clashes

Closes
RaspberryPiFoundation/digital-editor-issues#374
  • Loading branch information
loiswells97 authored Jan 10, 2025
1 parent 268c6f6 commit b0a0a7f
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Support for the `outputPanels` attribute in the `PyodideRunner` (#1157)
- Downloading project instructions (#1160)

### Changed

- Made `INSTRUCTIONS.md` a reserved file name (#1160)

## [0.28.14] - 2025-01-06

Expand Down
4 changes: 4 additions & 0 deletions src/components/DownloadButton/DownloadButton.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const DownloadButton = (props) => {

const zip = new JSZip();

if (project.instructions) {
zip.file("INSTRUCTIONS.md", project.instructions);
}

project.components.forEach((file) => {
zip.file(`${file.name}.${file.extension}`, file.content);
});
Expand Down
58 changes: 58 additions & 0 deletions src/components/DownloadButton/DownloadButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("Downloading project with name set", () => {
project: {
name: "My epic project",
identifier: "hello-world-project",
instructions: "print hello world to the console",
components: [
{
name: "main",
Expand Down Expand Up @@ -53,6 +54,18 @@ describe("Downloading project with name set", () => {
expect(downloadButton).toBeInTheDocument();
});

test("Clicking download zips instructions", async () => {
fireEvent.click(downloadButton);
const JSZipInstance = JSZip.mock.instances[0];
const mockFile = JSZipInstance.file;
await waitFor(() =>
expect(mockFile).toHaveBeenCalledWith(
"INSTRUCTIONS.md",
"print hello world to the console",
),
);
});

test("Clicking download zips project file content", async () => {
fireEvent.click(downloadButton);
const JSZipInstance = JSZip.mock.instances[0];
Expand Down Expand Up @@ -123,3 +136,48 @@ describe("Downloading project with no name set", () => {
);
});
});

describe("Downloading project with no instructions set", () => {
let downloadButton;

beforeEach(() => {
JSZip.mockClear();
const middlewares = [];
const mockStore = configureStore(middlewares);
const initialState = {
editor: {
project: {
name: "My epic project",
identifier: "hello-world-project",
components: [
{
name: "main",
extension: "py",
content: "",
},
],
image_list: [],
},
},
};
const store = mockStore(initialState);
render(
<Provider store={store}>
<DownloadButton buttonText="Download" Icon={() => {}} />
</Provider>,
);
downloadButton = screen.queryByText("Download").parentElement;
});

test("Clicking download button does not zip instructions", async () => {
fireEvent.click(downloadButton);
const JSZipInstance = JSZip.mock.instances[0];
const mockFile = JSZipInstance.file;
await waitFor(() =>
expect(mockFile).not.toHaveBeenCalledWith(
"INSTRUCTIONS.md",
expect.anything(),
),
);
});
});
11 changes: 11 additions & 0 deletions src/components/Modals/NewFileModal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ describe("Testing the new file modal", () => {
expect(store.getActions()).toEqual(expectedActions);
});

test("Reserved file name throws error", () => {
fireEvent.change(inputBox, { target: { value: "INSTRUCTIONS.md" } });
fireEvent.click(saveButton);
const expectedActions = [
setNameError("filePanel.errors.reservedFileName", {
fileName: "INSTRUCTIONS.md",
}),
];
expect(store.getActions()).toEqual(expectedActions);
});

test("Unsupported extension throws error", () => {
fireEvent.change(inputBox, { target: { value: "file1.js" } });
fireEvent.click(saveButton);
Expand Down
11 changes: 11 additions & 0 deletions src/components/Modals/RenameFileModal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ describe("Testing the rename file modal", () => {
expect(store.getActions()).toEqual(expectedActions);
});

test("Reserved file name throws error", () => {
fireEvent.change(inputBox, { target: { value: "INSTRUCTIONS.md" } });
fireEvent.click(saveButton);
const expectedActions = [
setNameError("filePanel.errors.reservedFileName", {
fileName: "INSTRUCTIONS.md",
}),
];
expect(store.getActions()).toEqual(expectedActions);
});

test("Unchanged file name does not throw error", () => {
fireEvent.click(saveButton);
const expectedActions = [
Expand Down
7 changes: 7 additions & 0 deletions src/utils/componentNameValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const allowedExtensions = {
html: ["html", "css", "js"],
};

const reservedFileNames = ["INSTRUCTIONS.md"];

const allowedExtensionsString = (projectType, t) => {
const extensionsList = allowedExtensions[projectType];
if (extensionsList.length === 1) {
Expand All @@ -19,6 +21,7 @@ const allowedExtensionsString = (projectType, t) => {
const isValidFileName = (fileName, projectType, componentNames) => {
const extension = fileName.split(".").slice(1).join(".");
if (
!reservedFileNames.includes(fileName) &&
allowedExtensions[projectType].includes(extension) &&
!componentNames.includes(fileName) &&
fileName.split(" ").length === 1
Expand All @@ -44,6 +47,10 @@ export const validateFileName = (
(currentFileName && fileName === currentFileName)
) {
callback();
} else if (reservedFileNames.includes(fileName)) {
dispatch(
setNameError(t("filePanel.errors.reservedFileName", { fileName })),
);
} else if (componentNames.includes(fileName)) {
dispatch(setNameError(t("filePanel.errors.notUnique")));
} else if (fileName.split(" ").length > 1) {
Expand Down
2 changes: 2 additions & 0 deletions src/utils/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ i18n
},
filePanel: {
errors: {
reservedFileName:
"{{fileName}} is a reserved file name. Please choose a different name.",
containsSpaces: "File names must not contain spaces.",
generalError: "Error",
notUnique: "File names must be unique.",
Expand Down

0 comments on commit b0a0a7f

Please sign in to comment.