diff --git a/services/gui/app/views/public/app.js b/services/gui/app/views/public/app.js index 45ce4d9..93e566a 100644 --- a/services/gui/app/views/public/app.js +++ b/services/gui/app/views/public/app.js @@ -1,165 +1,5 @@ -//prepare modal to delete a title -function prepareDeleteModal(name, id) { - //set name of modal - $("#deleteModalName").text(name); - //set action to id of title - $("#deleteModalForm").attr("action", "/title/" + id + "/?_method=DELETE"); - //show delete modal - $("#deleteModal").modal("show"); -} - -//clicking the magnifier activates the search bar -function activateSearchBar() { - $("#searchNewTitle").focus(); -} - //trigger fadeout of flash messages window.onload = function() { $("#successAlert").fadeOut(5000); $("#errorAlert").fadeOut(5000); }; - -//update a title as seen or unseen -function toggleSeenStatus(id, name, seen) { - let form = document.createElement("form"); - form.action = "/title/" + id + "/?_method=PUT"; - form.method = "POST"; - if (seen) { - form.innerHTML = - ' '; - } else { - form.innerHTML = ''; - } - - //the form must be in the document to submit it, but should be invisible - form.hidden = true; - document.body.append(form); - - form.submit(); -} - -//suggestions from IMDB (via omdb api) for adding a new title -//needs to be globally available -var omdbRequest = null; -function suggestTitle() { - var searchString = $("#searchNewTitle").val(); - if (searchString.length > 2) { - //ask omdb to find a title based on the input provided - omdbRequest = jQuery.ajax({ - type: "GET", - url: `https://www.omdbapi.com/?s=${searchString}&apikey=b50af808`, - beforeSend: function() { - //if there is an unfinished request to omdb, abort it, to avoid overlapping responses - if (omdbRequest != null) { - omdbRequest.abort(); - } - }, - success: function(response) { - if (response.Response === "True" && response.Search.length > 0) { - const results = $("#results"); - //clear suggestion list - results.html(""); - for (let suggestion of response.Search) { - //some titles have no cover and some covers are hosted at imdb, seems like they don't allow external usage of those, we replace those with a default one - if ( - suggestion.Poster === "N/A" || - suggestion.Poster.includes("media-imdb.com") || - suggestion.Poster.includes("images-na") - ) { - suggestion.Poster = "/nocover.png"; - } - //build string for suggestion - let suggestionHtml = ` -
-
- - -
-
-

${suggestion.Title}

-

(${suggestion.Year})

-
-
- `; - results.append(suggestionHtml); - results - .children() - .last() - .click(() => - addTitle( - suggestion.Title, - suggestion.imdbID, - suggestion.Year, - suggestion.Poster - ) - ); - } - results.show(); - } - }, - error: function(err, errText) { - //aborting a request also calls the error function - crazy people - if (errText != "abort") { - console.log(`ERR: oMDB failed us, here is the reason: ${errText}`); - $("#results").html( - "

😰ooops we can't get results from iMDB, please notify us!

" - ); - $("#results").show(); - } - } - }); - } else { - $("#results").hide(0); - } -} - -//add a new title -function addTitle(name, imdbID, year, poster, genres) { - let form = document.createElement("form"); - form.action = "/title"; - form.method = "POST"; - - var ratingRequest = new XMLHttpRequest(); - ratingRequest.onreadystatechange = function() { - if (ratingRequest.readyState == 4 && ratingRequest.status == 200) { - let genreArray = JSON.parse(ratingRequest.responseText).Genre.split(","); - let genreInput = ""; - for (let i = 0; i < genreArray.length; i++) { - genreInput += ``; - } - - form.innerHTML = ` - - - - - - - ${genreInput} - `; - //the form must be in the document to submit it, but should be invisible - form.hidden = true; - document.body.append(form); - form.submit(); - } - }; - - ratingRequest.open( - "GET", - "https://www.omdbapi.com/?i=" + imdbID + "&apikey=b50af808&tomatoes=true" - ); - ratingRequest.send(); -} - -//hide suggestions when search field loses focus -function hideSuggestions() { - setTimeout(function() { - $("#results").hide(0); - }, 200); -} diff --git a/services/gui/app/views/public/moveez.js b/services/gui/app/views/public/moveez.js new file mode 100644 index 0000000..4b36f0b --- /dev/null +++ b/services/gui/app/views/public/moveez.js @@ -0,0 +1,15 @@ +import { + prepareDeleteModal, + activateSearchBar, + toggleSeenStatus, + suggestTitle, + hideSuggestions +} from "/title.js"; + +export const moveez = (window.moveez = { + prepareDeleteModal, + activateSearchBar, + toggleSeenStatus, + suggestTitle, + hideSuggestions +}); diff --git a/services/gui/app/views/public/title.js b/services/gui/app/views/public/title.js new file mode 100644 index 0000000..fce1292 --- /dev/null +++ b/services/gui/app/views/public/title.js @@ -0,0 +1,181 @@ +//prepare modal to delete a title +export function prepareDeleteModal(name, id) { + //set name of modal + $("#deleteModalName").text(name); + //set action to id of title + $("#deleteModalForm").attr("action", "/title/" + id + "/?_method=DELETE"); + //show delete modal + $("#deleteModal").modal("show"); +} + +//clicking the magnifier activates the search bar +export function activateSearchBar() { + $("#searchNewTitle").focus(); +} + +//update a title as seen or unseen +export function toggleSeenStatus(id, name, seen) { + let form = document.createElement("form"); + form.action = "/title/" + id + "/?_method=PUT"; + form.method = "POST"; + if (seen) { + form.innerHTML = + ' '; + } else { + form.innerHTML = ''; + } + + //the form must be in the document to submit it, but should be invisible + form.hidden = true; + document.body.append(form); + + form.submit(); +} + +//suggestions from IMDB (via omdb api) for adding a new title +//needs to be globally available +var omdbRequest = null; +export function suggestTitle() { + var searchString = $("#searchNewTitle").val(); + if (searchString.length > 2) { + //ask omdb to find a title based on the input provided + omdbRequest = jQuery.ajax({ + type: "GET", + url: `https://www.omdbapi.com/?s=${searchString}&apikey=b50af808`, + beforeSend: function() { + //if there is an unfinished request to omdb, abort it, to avoid overlapping responses + if (omdbRequest != null) { + omdbRequest.abort(); + } + }, + success: function(response) { + if (response.Response === "True" && response.Search.length > 0) { + const results = $("#results"); + //clear suggestion list + results.html(""); + for (let suggestion of response.Search) { + //some titles have no cover and some covers are hosted at imdb, seems like they don't allow external usage of those, we replace those with a default one + if ( + suggestion.Poster === "N/A" || + suggestion.Poster.includes("media-imdb.com") || + suggestion.Poster.includes("images-na") + ) { + suggestion.Poster = "/nocover.png"; + } + //build string for suggestion + let suggestionHtml = renderSuggestion(suggestion); + results.append(suggestionHtml); + results + .children() + .last() + .click(() => onSuggestionSelected(suggestion)); + } + results.show(); + } + }, + error: function(err, errText) { + //aborting a request also calls the error function - crazy people + if (errText != "abort") { + console.log(`ERR: oMDB failed us, here is the reason: ${errText}`); + $("#results").html( + "

😰ooops we can't get results from iMDB, please notify us!

" + ); + $("#results").show(); + } + } + }); + } else { + $("#results").hide(0); + } +} + +const renderSuggestion = suggestion => { + const bIsInWatchlist = isInWatchList(suggestion); + return ` +
+
+ + + + +
+
+

${suggestion.Title}

+

(${suggestion.Year})

+
+
+ `; +}; + +const onSuggestionSelected = suggestion => { + if (!isInWatchList(suggestion)) { + addTitle( + suggestion.Title, + suggestion.imdbID, + suggestion.Year, + suggestion.Poster + ); + } +}; + +export const isInWatchList = suggestion => + moveez.titles.some(title => title.imdbID === suggestion.imdbID); + +//add a new title +export function addTitle(name, imdbID, year, poster, genres) { + let form = document.createElement("form"); + form.action = "/title"; + form.method = "POST"; + + var ratingRequest = new XMLHttpRequest(); + ratingRequest.onreadystatechange = function() { + if (ratingRequest.readyState == 4 && ratingRequest.status == 200) { + let genreArray = JSON.parse(ratingRequest.responseText).Genre.split(","); + let genreInput = ""; + for (let i = 0; i < genreArray.length; i++) { + genreInput += ``; + } + + form.innerHTML = ` + + + + + + + ${genreInput} + `; + //the form must be in the document to submit it, but should be invisible + form.hidden = true; + document.body.append(form); + form.submit(); + } + }; + + ratingRequest.open( + "GET", + "https://www.omdbapi.com/?i=" + imdbID + "&apikey=b50af808&tomatoes=true" + ); + ratingRequest.send(); +} + +//hide suggestions when search field loses focus +export function hideSuggestions() { + setTimeout(function() { + $("#results").hide(0); + }, 200); +} diff --git a/services/gui/app/views/title/index.ejs b/services/gui/app/views/title/index.ejs index ed2c444..32787cd 100644 --- a/services/gui/app/views/title/index.ejs +++ b/services/gui/app/views/title/index.ejs @@ -6,9 +6,9 @@
- +
- +
@@ -21,7 +21,7 @@
<% titles.forEach(function(title){ %> <% if(!title.seen) { %> -
+
cover @@ -67,8 +67,8 @@ <% } %>
  • - - + +
  • @@ -116,8 +116,8 @@
    <%= title.name %>
  • - - + +
  • @@ -164,6 +164,18 @@
    -
    + + + <% include ../partials/footer %> \ No newline at end of file diff --git a/services/gui/karma.conf.js b/services/gui/karma.conf.js new file mode 100644 index 0000000..56ef4a1 --- /dev/null +++ b/services/gui/karma.conf.js @@ -0,0 +1,56 @@ +// Karma configuration +// Generated on Thu Oct 31 2019 11:42:57 GMT+0100 (GMT+01:00) + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha", "chai"], + + // list of files / patterns to load in the browser + files: [ + { pattern: "test/frontend/**/*Test.js", type: "module" }, + { pattern: "app/views/public/**/*.js", type: "js", included: false } + ], + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: {}, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["progress"], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ["Chrome"], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: Infinity + }); +}; diff --git a/services/gui/package.json b/services/gui/package.json index 786e690..5a5a77c 100644 --- a/services/gui/package.json +++ b/services/gui/package.json @@ -5,6 +5,7 @@ "main": "app.js", "scripts": { "test": "cd ./app && NODE_ENV='test' PORT='8082' NODE_CONFIG_DIR='../config' RELEASE='mocha test' nyc --reporter=lcov --reporter=text --reporter=cobertura mocha --timeout 20000 --full-trace ../test/integration/*Test.js", + "frontend-test": "karma start", "start": "cd app && NODE_CONFIG_DIR='../config' node .", "watch": "nodemon -e js,ejs,css --watch ./app --exec npm run dev", "dev": "cd ./app && KETCHUP_ENDPOINT='localhost:8083' PORT='8443' TLS_CRT_PATH='../tlscert/server.crt' TLS_KEY_PATH='../tlscert/server.key' FACEBOOK_APP_SECRET_PATH='../facebook/app_secret' NODE_CONFIG_DIR='../config' node .", @@ -49,14 +50,19 @@ "chai": "^3.5.0", "chai-http": "^2.0.1", "file-loader": "^2.0.0", + "karma": "^4.4.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^3.1.0", + "karma-mocha": "^1.3.0", "mocha": "^2.4.5", "nodemon": "^1.18.4", "nyc": "^11.4.1", + "prettier": "^1.18.2", "react": "^16.6.3", "react-dom": "^16.6.3", "sinon": "^7.2.7" }, "optionalDependencies": { - "cypress": "^3.2.0" + "cypress": "^3.5.0" } } diff --git a/services/gui/test/frontend/app/appTest.js b/services/gui/test/frontend/app/appTest.js new file mode 100644 index 0000000..29745b9 --- /dev/null +++ b/services/gui/test/frontend/app/appTest.js @@ -0,0 +1,37 @@ +import { isInWatchList } from "/base/app/views/public/title.js"; +chai.should(); + +describe("app", () => { + describe("isInWatchList", () => { + it("returns false if given movie is not in watchlist", () => { + // setup + const movie = {}; + window.moveez = { titles: [] }; + + // run + const bIsInWatchList = isInWatchList(movie); + + // check + bIsInWatchList.should.equal(false); + + // clean up + delete window.moveez; + }); + + it("returns true if given movie is contained in watchlist", () => { + // setup + const movie = {}; + const otherMovie = {}; + window.moveez = { titles: [otherMovie, movie] }; + + // run + const bIsInWatchList = isInWatchList(movie); + + // check + bIsInWatchList.should.equal(true); + + // clean up + delete window.moveez; + }); + }); +});