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