diff --git a/.travis.yml b/.travis.yml
index bc7874a1e..9905eb699 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,18 @@
-language: java
-jdk: openjdk8
-sudo: true
+jobs:
+ include:
+ - language: java
+ jdk: openjdk8
+ sudo: true
+ script:
+ - ./gradlew assemble
+ - ./gradlew check
+ - ./gradlew buildOfficial
-script:
- - ./gradlew assemble
- - ./gradlew check
- - ./gradlew buildOfficial
+ - language: node_js
+ node_js: 10
+ script:
+ - cd tnoodle-ui
+ - npm install
+ - npm test -- --coverage
+ after_script:
+ - COVERALLS_REPO_TOKEN=$coveralls_repo_token npm run coveralls
diff --git a/README.md b/README.md
index 2721f753a..5f99aca23 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
TNoodle is a software suite that contains the official WCA scramble program. It consists of the core scrambling code (primarily written in Java) as well as a UI and server to generate a fully autonomous JAR file
-[![Build Status](https://travis-ci.org/thewca/tnoodle.svg?branch=master)](https://travis-ci.org/thewca/tnoodle)
+[![Build Status](https://travis-ci.org/thewca/tnoodle.svg?branch=master)](https://travis-ci.org/thewca/tnoodle) [![Coverage Status](https://coveralls.io/repos/github/thewca/tnoodle/badge.svg?branch=master)](https://coveralls.io/github/thewca/tnoodle?branch=master)
## WCA Scramble Program
diff --git a/tnoodle-ui/package.json b/tnoodle-ui/package.json
index 204d6ad84..4616ec169 100644
--- a/tnoodle-ui/package.json
+++ b/tnoodle-ui/package.json
@@ -1,46 +1,48 @@
{
- "name": "tnoodle-ui",
- "version": "0.1.0",
- "private": true,
- "homepage": "http://localhost:2014/scramble",
- "proxy": "http://localhost:2014",
- "dependencies": {
- "@testing-library/jest-dom": "^4.2.4",
- "@testing-library/react": "^9.3.2",
- "@testing-library/user-event": "^7.1.2",
- "bootstrap": "^4.4.1",
- "fetch-intercept": "^2.3.1",
- "node-sass": "^4.13.1",
- "react": "^16.12.0",
- "react-bootstrap": "^1.0.0-beta.16",
- "react-dom": "^16.12.0",
- "react-icons": "^3.10.0",
- "react-redux": "^7.1.3",
- "react-router-dom": "^5.1.2",
- "react-scripts": "3.4.3",
- "redux": "^4.0.5"
- },
- "eslintConfig": {
- "extends": "react-app"
- },
- "scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test --watchAll --watchAll=false",
- "eject": "react-scripts eject",
- "predeploy": "npm run build",
- "deploy": "gh-pages -d build"
- },
- "browserslist": {
- "production": [
- ">0.2%",
- "not dead",
- "not op_mini all"
- ],
- "development": [
- "last 1 chrome version",
- "last 1 firefox version",
- "last 1 safari version"
- ]
- }
+ "name": "tnoodle-ui",
+ "version": "0.1.0",
+ "private": true,
+ "homepage": "http://localhost:2014/scramble",
+ "proxy": "http://localhost:2014",
+ "dependencies": {
+ "@testing-library/jest-dom": "^4.2.4",
+ "@testing-library/react": "^9.3.2",
+ "@testing-library/user-event": "^7.1.2",
+ "bootstrap": "^4.4.1",
+ "fetch-intercept": "^2.3.1",
+ "node-sass": "^4.13.1",
+ "react": "^16.12.0",
+ "react-bootstrap": "^1.0.0-beta.16",
+ "react-dom": "^16.12.0",
+ "react-icons": "^3.10.0",
+ "react-redux": "^7.1.3",
+ "react-router-dom": "^5.1.2",
+ "react-scripts": "3.4.3",
+ "redux": "^4.0.5"
+ },
+ "eslintConfig": {
+ "extends": "react-app"
+ },
+ "scripts": {
+ "start": "react-scripts start",
+ "build": "react-scripts build",
+ "test": "react-scripts test --watchAll --watchAll=false",
+ "eject": "react-scripts eject",
+ "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "coveralls": "^3.1.0"
+ }
}
diff --git a/tnoodle-ui/src/api/wca.api.js b/tnoodle-ui/src/api/wca.api.js
index a0bb5d3ad..fb821b344 100644
--- a/tnoodle-ui/src/api/wca.api.js
+++ b/tnoodle-ui/src/api/wca.api.js
@@ -83,16 +83,16 @@ export function gotoPreLoginPath() {
export function fetchMe() {
return wcaApiFetch("/me")
- .then(response => response.json())
- .then(json => json.me);
+ .then((response) => response.json())
+ .then((json) => json.me);
}
export function fetchVersionInfo() {
- return wcaApiFetch("/scramble-program").then(response => response.json());
+ return wcaApiFetch("/scramble-program").then((response) => response.json());
}
export function getCompetitionJson(competitionId) {
- return wcaApiFetch(`/competitions/${competitionId}/wcif`).then(response =>
+ return wcaApiFetch(`/competitions/${competitionId}/wcif`).then((response) =>
response.json()
);
}
@@ -101,7 +101,7 @@ export function getUpcomingManageableCompetitions() {
let oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
return wcaApiFetch(
`/competitions?managed_by_me=true&start=${oneWeekAgo.toISOString()}`
- ).then(response => response.json());
+ ).then((response) => response.json());
}
function getHashParameter(name) {
@@ -141,11 +141,11 @@ function wcaApiFetch(path, fetchOptions) {
fetchOptions = Object.assign({}, fetchOptions, {
headers: new Headers({
Authorization: `Bearer ${wcaAccessToken}`,
- "Content-Type": "application/json"
- })
+ "Content-Type": "application/json",
+ }),
});
- return fetch(`${baseApiUrl}${path}`, fetchOptions).then(response => {
+ return fetch(`${baseApiUrl}${path}`, fetchOptions).then((response) => {
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`);
}
diff --git a/tnoodle-ui/src/components/EntryInterface.test.js b/tnoodle-ui/src/components/EntryInterface.test.js
new file mode 100644
index 000000000..de03ee500
--- /dev/null
+++ b/tnoodle-ui/src/components/EntryInterface.test.js
@@ -0,0 +1,67 @@
+import React from "react";
+import { act } from "react-dom/test-utils";
+
+import { render, unmountComponentAtNode } from "react-dom";
+import { fireEvent } from "@testing-library/react";
+
+import { Provider } from "react-redux";
+import store from "../redux/Store";
+
+import EntryInterface from "./EntryInterface";
+
+let container = null;
+beforeEach(() => {
+ // setup a DOM element as a render target
+ container = document.createElement("div");
+ document.body.appendChild(container);
+});
+
+afterEach(() => {
+ // cleanup on exiting
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+});
+
+it("Competition name should be already filled with current date", () => {
+ // Render component
+ act(() => {
+ render(
+
+
+ ,
+ container
+ );
+ });
+
+ const today = new Date().toISOString().split("T")[0];
+
+ const competitionNameInput = container.querySelector("#competition-name");
+ expect(competitionNameInput.value).toEqual("Scrambles for " + today);
+});
+
+it("Password should toggle", () => {
+ // Render component
+ act(() => {
+ render(
+
+
+ ,
+ container
+ );
+ });
+
+ const input = container.querySelector("#password");
+ const passwordToggler = container.querySelector(".input-group-prepend");
+
+ fireEvent.change(input, { target: { value: "123456" } });
+
+ // Type password at first
+ expect(input.type).toBe("password");
+
+ // After the click, it should be type text
+ fireEvent.click(passwordToggler);
+ expect(input.type).toBe("text");
+
+ expect(input.value).toBe("123456");
+});
diff --git a/tnoodle-ui/src/components/EventPickerTable.jsx b/tnoodle-ui/src/components/EventPickerTable.jsx
index c83bdc8b5..dda5fe089 100644
--- a/tnoodle-ui/src/components/EventPickerTable.jsx
+++ b/tnoodle-ui/src/components/EventPickerTable.jsx
@@ -36,11 +36,11 @@ const EventPickerTable = connect(
mapDispatchToProps
)(
class extends Component {
- componentDidMount = function () {
+ componentDidMount() {
this.getFormats();
this.getWcaEvents();
this.getFmcTranslations();
- };
+ }
getFormats = () => {
fetchFormats()
@@ -49,8 +49,8 @@ const EventPickerTable = connect(
return response.json();
}
})
- .then((formats) => {
- this.props.setWcaFormats(formats);
+ .then((response) => {
+ this.props.setWcaFormats(response);
});
};
@@ -61,8 +61,8 @@ const EventPickerTable = connect(
return response.json();
}
})
- .then((wcaEvents) => {
- this.props.setWcaEvents(wcaEvents);
+ .then((response) => {
+ this.props.setWcaEvents(response);
});
};
diff --git a/tnoodle-ui/src/components/FmcTranslationsDetail.jsx b/tnoodle-ui/src/components/FmcTranslationsDetail.jsx
index 8946b857d..fc43b5e2a 100644
--- a/tnoodle-ui/src/components/FmcTranslationsDetail.jsx
+++ b/tnoodle-ui/src/components/FmcTranslationsDetail.jsx
@@ -141,18 +141,23 @@ const FmcTranslationsDetail = connect(
checked={
translation.status
}
- onChange={(e) =>
+ onChange={(
+ e
+ ) =>
this.handleTranslation(
- translation.id, e.target.checked
+ translation.id,
+ e
+ .target
+ .checked
)
}
/>
{j <
TRANSLATIONS_PER_LINE -
- 1 && (
-
|
- )}
+ 1 && (
+ |
+ )}
);
}
diff --git a/tnoodle-ui/src/components/Main.test.js b/tnoodle-ui/src/components/Main.test.js
new file mode 100644
index 000000000..50a71b9ed
--- /dev/null
+++ b/tnoodle-ui/src/components/Main.test.js
@@ -0,0 +1,287 @@
+import React from "react";
+import { act } from "react-dom/test-utils";
+
+import { render, unmountComponentAtNode } from "react-dom";
+import { fireEvent } from "@testing-library/react";
+
+import { Provider } from "react-redux";
+import store from "../redux/Store";
+
+import Main from "./Main";
+
+const tnoodleApi = require("../api/tnoodle.api");
+
+let container = null;
+beforeEach(() => {
+ // setup a DOM element as a render target
+ container = document.createElement("div");
+ document.body.appendChild(container);
+});
+
+afterEach(() => {
+ // cleanup on exiting
+ unmountComponentAtNode(container);
+ container.remove();
+ container = null;
+});
+
+it("There should be only 1 button of type submit", async () => {
+ // Define mock objects
+ const events = [
+ {
+ id: "333",
+ name: "3x3x3",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "222",
+ name: "2x2x2",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "444",
+ name: "4x4x4",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "555",
+ name: "5x5x5",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "666",
+ name: "6x6x6",
+ format_ids: ["m", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "777",
+ name: "7x7x7",
+ format_ids: ["m", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "333bf",
+ name: "3x3x3 Blindfolded",
+ format_ids: ["3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "333fm",
+ name: "3x3x3 Fewest Moves",
+ format_ids: ["m", "2", "1"],
+ can_change_time_limit: false,
+ is_timed_event: false,
+ is_fewest_moves: true,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "333oh",
+ name: "3x3x3 One-Handed",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "clock",
+ name: "Clock",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "minx",
+ name: "Megaminx",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "pyram",
+ name: "Pyraminx",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "skewb",
+ name: "Skewb",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "sq1",
+ name: "Square-1",
+ format_ids: ["a", "3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "444bf",
+ name: "4x4x4 Blindfolded",
+ format_ids: ["3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "555bf",
+ name: "5x5x5 Blindfolded",
+ format_ids: ["3", "2", "1"],
+ can_change_time_limit: true,
+ is_timed_event: true,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: false,
+ },
+ {
+ id: "333mbf",
+ name: "3x3x3 Multiple Blindfolded",
+ format_ids: ["3", "2", "1"],
+ can_change_time_limit: false,
+ is_timed_event: false,
+ is_fewest_moves: false,
+ is_multiple_blindfolded: true,
+ },
+ ];
+
+ const formats = {
+ 1: { name: "Best of 1", shortName: "Bo1" },
+ 2: { name: "Best of 2", shortName: "Bo2" },
+ 3: { name: "Best of 3", shortName: "Bo3" },
+ a: { name: "Average of 5", shortName: "Ao5" },
+ m: { name: "Mean of 3", shortName: "Mo3" },
+ };
+
+ const languages = {
+ da: "Danish",
+ de: "German",
+ en: "English",
+ es: "Spanish",
+ et: "Estonian",
+ fi: "Finnish",
+ fr: "French",
+ hr: "Croatian",
+ hu: "Hungarian",
+ id: "Indonesian",
+ it: "Italian",
+ ja: "Japanese",
+ ko: "Korean",
+ pl: "Polish",
+ pt: "Portuguese",
+ "pt-BR": "Portuguese (Brazil)",
+ ro: "Romanian",
+ ru: "Russian",
+ sl: "Slovenian",
+ vi: "Vietnamese",
+ "zh-CN": "Chinese (China)",
+ "zh-TW": "Chinese (Taiwan)",
+ };
+
+ // Turn on mocking behavior
+ jest.spyOn(tnoodleApi, "fetchWcaEvents").mockImplementation(() =>
+ Promise.resolve(new Response(JSON.stringify(events)))
+ );
+
+ jest.spyOn(tnoodleApi, "fetchFormats").mockImplementation(() =>
+ Promise.resolve(new Response(JSON.stringify(formats)))
+ );
+
+ jest.spyOn(
+ tnoodleApi,
+ "fetchAvailableFmcTranslations"
+ ).mockImplementation(() =>
+ Promise.resolve(new Response(JSON.stringify(languages)))
+ );
+
+ // Render component
+ await act(async () => {
+ render(
+
+
+ ,
+ container
+ );
+ });
+
+ // Pick all