From b18206c817d9cba8a63f0e7101690fc2b7ca5263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20M=C3=A4lman?= Date: Tue, 15 Sep 2026 20:20:28 +0200 Subject: [PATCH 1/2] Add Prettier configuration and update project files for code formatting --- .prettierignore | 10 ++++++++++ .prettierrc.json | 1 + .vscode/extensions.json | 3 ++- .vscode/launch.json | 26 +++++++++++++------------- .vscode/settings.json | 13 ++++++++----- CONTRIBUTING.md | 16 +++++++++++----- package-lock.json | 19 ++++++++++++++++++- package.json | 11 +++++++++-- 8 files changed, 72 insertions(+), 27 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e184b6a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +node_modules/ +coverage/ +dist/ +.github/dependabot.yml +.github/FUNDING.yml +.github/workflows/sonarcloud.yml +.vscode/extensions.json +index.html +CONTRIBUTING.md +README.md \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 46a3ead..8cf9606 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,6 @@ { "recommendations": [ + "esbenp.prettier-vscode", "SonarSource.sonarlint-vscode" ] -} +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 021fd2e..30a366c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,14 +1,14 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Website", - "file": "${workspaceFolder}/index.html" - } - ] -} + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Website", + "file": "${workspaceFolder}/index.html" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8018617..1a7f24e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ { - "sonarlint.connectedMode.project": { - "connectionId": "bellmano", - "projectKey": "bellmano_RandomBeckGenerator" - } -} + "git.autofetch": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "sonarlint.connectedMode.project": { + "connectionId": "bellmano", + "projectKey": "bellmano_RandomBeckGenerator" + } +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 921af26..4dac71a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,16 +3,22 @@ 1. Fork the repository 2. Create a feature branch: `git checkout -b feature/your-feature` 3. Make your changes and add or update tests -4. Run the tests: `npm test` or `npm run test:coverage` -5. Commit your changes: `git commit -m 'Add some feature'` -6. Push to the branch: `git push origin feature/your-feature` -7. Open a Pull Request +4. Format the code with [Prettier](https://prettier.io): `npm run format` +5. Run the tests: `npm test` or `npm run test:coverage` +6. Commit your changes: `git commit -m 'Add some feature'` +7. Push to the branch: `git push origin feature/your-feature` +8. Open a Pull Request ## Development Setup 1. Clone the repository: `git clone https://github.com/bellmano/RandomBeckGenerator.git` 2. Install dependencies: `npm install` +## Code Style + +- All code must be formatted with [Prettier](https://prettier.io) before it is committed or pushed. Run `npm run format` from the repository root. +- Write meaningful commit messages + ## Testing - Add or update unit tests for new features or bug fixes @@ -20,4 +26,4 @@ ## Issues -If you find a bug or have a feature request, please [open an issue](https://github.com/bellmano/RandomBeckGenerator/issues). +If you find a bug or have a feature request, please [open an issue](https://github.com/bellmano/RandomBeckGenerator/issues). \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 97e8979..943af43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "devDependencies": { "jest": "^30.5.1", - "jest-environment-jsdom": "^30.5.1" + "jest-environment-jsdom": "^30.5.1", + "prettier": "^3.9.6" } }, "node_modules/@asamuzakjp/css-color": { @@ -4293,6 +4294,22 @@ "node": ">=8" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", diff --git a/package.json b/package.json index 2585aa0..29ae878 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,14 @@ "scripts": { "test": "jest", "test:coverage": "jest --coverage", + "format": "prettier --write .", + "format:check": "prettier --check .", "fetch:ratings": "node src/fetchRatings.js" }, "devDependencies": { "jest": "^30.5.1", - "jest-environment-jsdom": "^30.5.1" + "jest-environment-jsdom": "^30.5.1", + "prettier": "^3.9.6" }, "jest": { "testEnvironment": "jsdom", @@ -18,7 +21,11 @@ "src/fetchRatings.js" ], "coverageDirectory": "coverage", - "coverageReporters": ["text", "lcov", "html"], + "coverageReporters": [ + "text", + "lcov", + "html" + ], "coveragePathIgnorePatterns": [ "/node_modules/", "/tests/" From 2a08e9206c7b406a7e8c592f4a8ccaea1af348c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20M=C3=A4lman?= Date: Tue, 15 Sep 2026 20:24:19 +0200 Subject: [PATCH 2/2] Format whole repo with Prettier --- .github/workflows/fetch-ratings.yml | 6 +- .prettierignore | 1 + .prettierrc.json | 2 +- .vscode/launch.json | 2 +- .vscode/settings.json | 2 +- src/fetchRatings.js | 296 ++++++----- src/script.js | 539 ++++++++++--------- src/style.css | 664 +++++++++++------------ tests/fetchRatings.test.js | 797 ++++++++++++++++------------ tests/script.test.js | 476 +++++++++-------- 10 files changed, 1495 insertions(+), 1290 deletions(-) diff --git a/.github/workflows/fetch-ratings.yml b/.github/workflows/fetch-ratings.yml index cc01c0d..b18b657 100644 --- a/.github/workflows/fetch-ratings.yml +++ b/.github/workflows/fetch-ratings.yml @@ -1,7 +1,7 @@ name: Fetch IMDB Ratings on: schedule: - - cron: '0 2 * * 1' # Runs every Monday at 02:00 UTC + - cron: "0 2 * * 1" # Runs every Monday at 02:00 UTC workflow_dispatch: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -22,8 +22,8 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '24' - cache: 'npm' + node-version: "24" + cache: "npm" - name: Install dependencies run: npm ci - name: Run fetchRatings script diff --git a/.prettierignore b/.prettierignore index e184b6a..cd173e4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,6 +5,7 @@ dist/ .github/FUNDING.yml .github/workflows/sonarcloud.yml .vscode/extensions.json +database/beckDB.js index.html CONTRIBUTING.md README.md \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 9e26dfe..0967ef4 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1 +1 @@ -{} \ No newline at end of file +{} diff --git a/.vscode/launch.json b/.vscode/launch.json index 30a366c..251050e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,4 +11,4 @@ "file": "${workspaceFolder}/index.html" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a7f24e..3a8977f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,4 +6,4 @@ "connectionId": "bellmano", "projectKey": "bellmano_RandomBeckGenerator" } -} \ No newline at end of file +} diff --git a/src/fetchRatings.js b/src/fetchRatings.js index eeabd44..b25cd01 100644 --- a/src/fetchRatings.js +++ b/src/fetchRatings.js @@ -1,211 +1,213 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const { gunzipSync } = require('node:zlib'); +const fs = require("node:fs"); +const path = require("node:path"); +const { gunzipSync } = require("node:zlib"); const backslash = String.fromCodePoint(92); const escapedBackslash = String.raw`\\`; const escapedQuote = String.raw`\"`; const dbFooter = [ - '', - "if (typeof module !== 'undefined' && module.exports) {", - ' module.exports = beckMovies;', - '}', - '' -].join('\n'); + "", + "if (typeof module !== 'undefined' && module.exports) {", + " module.exports = beckMovies;", + "}", + "", +].join("\n"); -const dbPath = path.join(__dirname, '../database/beckDB.js'); -const ratingsDatasetUrl = 'https://datasets.imdbws.com/title.ratings.tsv.gz'; +const dbPath = path.join(__dirname, "../database/beckDB.js"); +const ratingsDatasetUrl = "https://datasets.imdbws.com/title.ratings.tsv.gz"; function parseMovies(movies) { - if (!Array.isArray(movies)) { - throw new TypeError('beckDB.js must export an array of movies.'); - } + if (!Array.isArray(movies)) { + throw new TypeError("beckDB.js must export an array of movies."); + } - return movies.map((movie) => ({ ...movie })); + return movies.map((movie) => ({ ...movie })); } function loadMoviesFromDb(dbFilePath = dbPath) { - delete require.cache[require.resolve(dbFilePath)]; - const exportedMovies = require(dbFilePath); - return parseMovies(exportedMovies); + delete require.cache[require.resolve(dbFilePath)]; + const exportedMovies = require(dbFilePath); + return parseMovies(exportedMovies); } function extractTitleId(imdbUrl) { - const match = imdbUrl ? imdbUrl.match(/title\/(tt\d+)/) : null; - return match ? match[1] : null; + const match = imdbUrl ? imdbUrl.match(/title\/(tt\d+)/) : null; + return match ? match[1] : null; } function parseRatingsDataset(datasetContent, requiredTitleIds) { - const ratingsByTitleId = new Map(); - - for (const line of datasetContent.split('\n').slice(1)) { - if (!line) { - continue; - } - - const [titleId, averageRating] = line.split('\t'); - if (!requiredTitleIds.has(titleId)) { - continue; - } - - ratingsByTitleId.set(titleId, averageRating); - if (ratingsByTitleId.size === requiredTitleIds.size) { - break; - } + const ratingsByTitleId = new Map(); + + for (const line of datasetContent.split("\n").slice(1)) { + if (!line) { + continue; + } + + const [titleId, averageRating] = line.split("\t"); + if (!requiredTitleIds.has(titleId)) { + continue; } - return ratingsByTitleId; + ratingsByTitleId.set(titleId, averageRating); + if (ratingsByTitleId.size === requiredTitleIds.size) { + break; + } + } + + return ratingsByTitleId; } async function fetchRatingsMap(requiredTitleIds, fetchImpl = globalThis.fetch) { - if (typeof fetchImpl !== 'function') { - throw new TypeError('Fetch API is not available in this Node.js runtime.'); - } + if (typeof fetchImpl !== "function") { + throw new TypeError("Fetch API is not available in this Node.js runtime."); + } - const response = await fetchImpl(ratingsDatasetUrl); - if (!response.ok) { - throw new Error(`Failed to fetch IMDb ratings dataset: ${response.status} ${response.statusText}`.trim()); - } + const response = await fetchImpl(ratingsDatasetUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch IMDb ratings dataset: ${response.status} ${response.statusText}`.trim(), + ); + } - const compressedBuffer = Buffer.from(await response.arrayBuffer()); - const datasetContent = gunzipSync(compressedBuffer).toString('utf8'); + const compressedBuffer = Buffer.from(await response.arrayBuffer()); + const datasetContent = gunzipSync(compressedBuffer).toString("utf8"); - return parseRatingsDataset(datasetContent, requiredTitleIds); + return parseRatingsDataset(datasetContent, requiredTitleIds); } function escapeString(value) { - return String(value) + return String(value) .replaceAll(backslash, escapedBackslash) .replaceAll('"', escapedQuote); } function formatMovie(movie) { - let result = ' {\n'; - result += ` number: ${movie.number},\n`; - result += ` title: "${escapeString(movie.title)}",\n`; - result += ` year: ${movie.year},\n`; - if (movie.description) { - result += ` description: "${escapeString(movie.description)}",\n`; - } - result += ` imdbUrl: "${escapeString(movie.imdbUrl)}"`; - - if (movie.tv4playUrl || movie.runtime || movie.imdbRating) { - result += ','; - } - result += '\n'; - - if (movie.tv4playUrl) { - result += ` tv4playUrl: "${escapeString(movie.tv4playUrl)}"`; - if (movie.runtime || movie.imdbRating) { - result += ','; - } - result += '\n'; - } - if (movie.runtime) { - result += ` runtime: "${escapeString(movie.runtime)}"`; - if (movie.imdbRating) { - result += ','; - } - result += '\n'; + let result = " {\n"; + result += ` number: ${movie.number},\n`; + result += ` title: "${escapeString(movie.title)}",\n`; + result += ` year: ${movie.year},\n`; + if (movie.description) { + result += ` description: "${escapeString(movie.description)}",\n`; + } + result += ` imdbUrl: "${escapeString(movie.imdbUrl)}"`; + + if (movie.tv4playUrl || movie.runtime || movie.imdbRating) { + result += ","; + } + result += "\n"; + + if (movie.tv4playUrl) { + result += ` tv4playUrl: "${escapeString(movie.tv4playUrl)}"`; + if (movie.runtime || movie.imdbRating) { + result += ","; } + result += "\n"; + } + if (movie.runtime) { + result += ` runtime: "${escapeString(movie.runtime)}"`; if (movie.imdbRating) { - result += ` imdbRating: "${escapeString(movie.imdbRating)}"\n`; + result += ","; } - result += ' }'; - return result; + result += "\n"; + } + if (movie.imdbRating) { + result += ` imdbRating: "${escapeString(movie.imdbRating)}"\n`; + } + result += " }"; + return result; } function serializeMovies(movies) { - return `const beckMovies = [\n${movies.map(formatMovie).join(',\n')}\n];\n${dbFooter}`; + return `const beckMovies = [\n${movies.map(formatMovie).join(",\n")}\n];\n${dbFooter}`; } async function updateRatings(options = {}) { - const loadMovies = options.loadMovies || loadMoviesFromDb; - const writeFileSync = options.writeFileSync || fs.writeFileSync; - const fetchImpl = options.fetchImpl || globalThis.fetch; - - let movies; - try { - movies = loadMovies(dbPath); - } catch (error) { - console.error('Failed to parse beckDB.js:', error); - return null; + const loadMovies = options.loadMovies || loadMoviesFromDb; + const writeFileSync = options.writeFileSync || fs.writeFileSync; + const fetchImpl = options.fetchImpl || globalThis.fetch; + + let movies; + try { + movies = loadMovies(dbPath); + } catch (error) { + console.error("Failed to parse beckDB.js:", error); + return null; + } + + const requiredTitleIds = new Set(); + for (const movie of movies) { + const titleId = extractTitleId(movie.imdbUrl); + if (titleId) { + requiredTitleIds.add(titleId); } - - const requiredTitleIds = new Set(); - for (const movie of movies) { - const titleId = extractTitleId(movie.imdbUrl); - if (titleId) { - requiredTitleIds.add(titleId); - } + } + + let ratingsByTitleId; + try { + console.log("Fetching latest IMDb ratings dataset..."); + ratingsByTitleId = await fetchRatingsMap(requiredTitleIds, fetchImpl); + } catch (error) { + console.error("Failed to fetch IMDb ratings dataset:", error); + return null; + } + + for (const movie of movies) { + if (!movie.imdbUrl) { + console.log(`No imdbUrl for: ${movie.title}`); + continue; } - let ratingsByTitleId; - try { - console.log('Fetching latest IMDb ratings dataset...'); - ratingsByTitleId = await fetchRatingsMap(requiredTitleIds, fetchImpl); - } catch (error) { - console.error('Failed to fetch IMDb ratings dataset:', error); - return null; + const titleId = extractTitleId(movie.imdbUrl); + if (!titleId) { + console.log(`Invalid imdbUrl for: ${movie.title}`); + continue; } - for (const movie of movies) { - if (!movie.imdbUrl) { - console.log(`No imdbUrl for: ${movie.title}`); - continue; - } - - const titleId = extractTitleId(movie.imdbUrl); - if (!titleId) { - console.log(`Invalid imdbUrl for: ${movie.title}`); - continue; - } - - console.log(`Updating IMDb rating for: ${movie.title}`); - const rating = ratingsByTitleId.get(titleId); - if (rating) { - movie.imdbRating = rating; - console.log(` -> Rating: ${rating}`); - } else { - console.log(' -> Rating not found'); - } + console.log(`Updating IMDb rating for: ${movie.title}`); + const rating = ratingsByTitleId.get(titleId); + if (rating) { + movie.imdbRating = rating; + console.log(` -> Rating: ${rating}`); + } else { + console.log(" -> Rating not found"); } + } - writeFileSync(dbPath, serializeMovies(movies), 'utf8'); - console.log('beckDB.js updated with latest IMDb ratings.'); - return movies; + writeFileSync(dbPath, serializeMovies(movies), "utf8"); + console.log("beckDB.js updated with latest IMDb ratings."); + return movies; } async function main(options) { - await updateRatings(options); + await updateRatings(options); } async function runMain(options) { - try { - await main(options); - } catch (error) { - console.error('Unexpected error while updating ratings:', error); - process.exitCode = 1; - } + try { + await main(options); + } catch (error) { + console.error("Unexpected error while updating ratings:", error); + process.exitCode = 1; + } } /* istanbul ignore next */ if (require.main === module) { - void main().catch((error) => { - console.error('Unexpected error while updating ratings:', error); - process.exitCode = 1; - }); + void main().catch((error) => { + console.error("Unexpected error while updating ratings:", error); + process.exitCode = 1; + }); } module.exports = { - extractTitleId, - fetchRatingsMap, - formatMovie, - loadMoviesFromDb, - main, - parseMovies, - parseRatingsDataset, - runMain, - serializeMovies, - updateRatings + extractTitleId, + fetchRatingsMap, + formatMovie, + loadMoviesFromDb, + main, + parseMovies, + parseRatingsDataset, + runMain, + serializeMovies, + updateRatings, }; diff --git a/src/script.js b/src/script.js index 6642ce8..51bf1f8 100644 --- a/src/script.js +++ b/src/script.js @@ -1,315 +1,328 @@ class BeckMovieGenerator { - constructor() { - this.movies = beckMovies; - this.initializeEventListeners(); - this.initializeRangeValues(); - this.updateRangeSelector(); + constructor() { + this.movies = beckMovies; + this.initializeEventListeners(); + this.initializeRangeValues(); + this.updateRangeSelector(); + } + + initializeRangeValues() { + const maxRangeInput = document.getElementById("max-range"); + maxRangeInput.value = this.movies.length; + } + + initializeEventListeners() { + const generateBtn = document.getElementById("generate-btn"); + const minRangeInput = document.getElementById("min-range"); + const maxRangeInput = document.getElementById("max-range"); + const allMoviesCheckbox = document.getElementById("all-movies-checkbox"); + + generateBtn.addEventListener("click", () => this.generateRandomMovie()); + + // Update ranges and validate when inputs change + minRangeInput.addEventListener("change", () => this.updateRangeSelector()); + maxRangeInput.addEventListener("change", () => this.updateRangeSelector()); + minRangeInput.addEventListener("input", () => this.validateInputs()); + maxRangeInput.addEventListener("input", () => this.validateInputs()); + + // Handle "All movies" checkbox + allMoviesCheckbox.addEventListener("change", () => { + this.toggleAllMoviesMode(); + this.validateInputs(); + }); + } + + updateRangeSelector() { + const minRangeInput = document.getElementById("min-range"); + const maxRangeInput = document.getElementById("max-range"); + const maxMovies = this.movies.length; + const minValue = Number.parseInt(minRangeInput.value); + const maxValue = Number.parseInt(maxRangeInput.value); + + // Set min/max bounds + minRangeInput.min = 1; + minRangeInput.max = maxMovies; + maxRangeInput.min = 1; + maxRangeInput.max = maxMovies; + + // Ensure values are within bounds + if (minValue < 1 || minValue > maxMovies) { + minRangeInput.value = 1; } - - initializeRangeValues() { - const maxRangeInput = document.getElementById('max-range'); - maxRangeInput.value = this.movies.length; + if (maxValue < 1 || maxValue > maxMovies) { + maxRangeInput.value = maxMovies; } - initializeEventListeners() { - const generateBtn = document.getElementById('generate-btn'); - const minRangeInput = document.getElementById('min-range'); - const maxRangeInput = document.getElementById('max-range'); - const allMoviesCheckbox = document.getElementById('all-movies-checkbox'); - - generateBtn.addEventListener('click', () => this.generateRandomMovie()); - - // Update ranges and validate when inputs change - minRangeInput.addEventListener('change', () => this.updateRangeSelector()); - maxRangeInput.addEventListener('change', () => this.updateRangeSelector()); - minRangeInput.addEventListener('input', () => this.validateInputs()); - maxRangeInput.addEventListener('input', () => this.validateInputs()); - - // Handle "All movies" checkbox - allMoviesCheckbox.addEventListener('change', () => { - this.toggleAllMoviesMode(); - this.validateInputs(); - }); + // Ensure min doesn't exceed max and vice versa + if (minValue > maxValue) { + maxRangeInput.value = minValue; + } + if (maxValue < minValue) { + minRangeInput.value = maxValue; } - updateRangeSelector() { - const minRangeInput = document.getElementById('min-range'); - const maxRangeInput = document.getElementById('max-range'); - const maxMovies = this.movies.length; - const minValue = Number.parseInt(minRangeInput.value); - const maxValue = Number.parseInt(maxRangeInput.value); - - // Set min/max bounds - minRangeInput.min = 1; - minRangeInput.max = maxMovies; - maxRangeInput.min = 1; - maxRangeInput.max = maxMovies; - - // Ensure values are within bounds - if (minValue < 1 || minValue > maxMovies) { - minRangeInput.value = 1; - } - if (maxValue < 1 || maxValue > maxMovies) { - maxRangeInput.value = maxMovies; - } + this.validateInputs(); + } - // Ensure min doesn't exceed max and vice versa - if (minValue > maxValue) { - maxRangeInput.value = minValue; - } - if (maxValue < minValue) { - minRangeInput.value = maxValue; - } + toggleAllMoviesMode() { + const allMoviesCheckbox = document.getElementById("all-movies-checkbox"); + const rangeSelector = document.getElementById("range-selector"); - this.validateInputs(); + if (allMoviesCheckbox.checked) { + // Dim the range selector when "All movies" is checked + rangeSelector.classList.add("dimmed"); + } else { + // Enable range selector when unchecked + rangeSelector.classList.remove("dimmed"); } - - toggleAllMoviesMode() { - const allMoviesCheckbox = document.getElementById('all-movies-checkbox'); - const rangeSelector = document.getElementById('range-selector'); - - if (allMoviesCheckbox.checked) { - // Dim the range selector when "All movies" is checked - rangeSelector.classList.add('dimmed'); - } else { - // Enable range selector when unchecked - rangeSelector.classList.remove('dimmed'); - } + } + + validateInputs() { + const allMoviesCheckbox = document.getElementById("all-movies-checkbox"); + const generateBtn = document.getElementById("generate-btn"); + + // If "All movies" is checked, always allow generation + if (allMoviesCheckbox.checked) { + generateBtn.disabled = false; + generateBtn.style.opacity = "1"; + generateBtn.style.cursor = "pointer"; + return; } - validateInputs() { - const allMoviesCheckbox = document.getElementById('all-movies-checkbox'); - const generateBtn = document.getElementById('generate-btn'); - - // If "All movies" is checked, always allow generation - if (allMoviesCheckbox.checked) { - generateBtn.disabled = false; - generateBtn.style.opacity = '1'; - generateBtn.style.cursor = 'pointer'; - return; - } + // Otherwise validate range inputs + const minRange = Number.parseInt( + document.getElementById("min-range").value, + ); + const maxRange = Number.parseInt( + document.getElementById("max-range").value, + ); + + const isValid = + minRange >= 1 && + maxRange >= 1 && + minRange <= this.movies.length && + maxRange <= this.movies.length && + minRange <= maxRange; + + generateBtn.disabled = !isValid; + + if (!isValid) { + generateBtn.style.opacity = "0.6"; + generateBtn.style.cursor = "not-allowed"; + } else { + generateBtn.style.opacity = "1"; + generateBtn.style.cursor = "pointer"; + } + } + + generateRandomMovie() { + const allMoviesCheckbox = document.getElementById("all-movies-checkbox"); + + let minRange, maxRange; + + if (allMoviesCheckbox.checked) { + // Use all movies (1 to total length) + minRange = 1; + maxRange = this.movies.length; + } else { + // Use custom range + minRange = Number.parseInt(document.getElementById("min-range").value); + maxRange = Number.parseInt(document.getElementById("max-range").value); + + // Validate ranges only if not using all movies + if ( + minRange < 1 || + maxRange < 1 || + minRange > maxRange || + minRange > this.movies.length || + maxRange > this.movies.length + ) { + this.showError("Ogiltigt intervall. Vänligen kontrollera dina nummer."); + return; + } + } - // Otherwise validate range inputs - const minRange = Number.parseInt(document.getElementById('min-range').value); - const maxRange = Number.parseInt(document.getElementById('max-range').value); + // Show loading state + this.showLoading(); - const isValid = minRange >= 1 && maxRange >= 1 && - minRange <= this.movies.length && - maxRange <= this.movies.length && - minRange <= maxRange; + // Simulate loading delay for better UX + setTimeout(() => { + try { + const randomNumber = this.getRandomNumber(minRange, maxRange); + const selectedMovie = this.movies.find( + (movie) => movie.number === randomNumber, + ); - generateBtn.disabled = !isValid; - - if (!isValid) { - generateBtn.style.opacity = '0.6'; - generateBtn.style.cursor = 'not-allowed'; + if (selectedMovie) { + this.displayMovie(selectedMovie); } else { - generateBtn.style.opacity = '1'; - generateBtn.style.cursor = 'pointer'; + this.showError("Kunde inte hitta Beck-filmen. Vänligen försök igen."); } - } - - generateRandomMovie() { - const allMoviesCheckbox = document.getElementById('all-movies-checkbox'); - - let minRange, maxRange; - - if (allMoviesCheckbox.checked) { - // Use all movies (1 to total length) - minRange = 1; - maxRange = this.movies.length; - } else { - // Use custom range - minRange = Number.parseInt(document.getElementById('min-range').value); - maxRange = Number.parseInt(document.getElementById('max-range').value); - - // Validate ranges only if not using all movies - if (minRange < 1 || maxRange < 1 || minRange > maxRange || - minRange > this.movies.length || maxRange > this.movies.length) { - this.showError('Ogiltigt intervall. Vänligen kontrollera dina nummer.'); - return; - } - } - - // Show loading state - this.showLoading(); - - // Simulate loading delay for better UX - setTimeout(() => { - try { - const randomNumber = this.getRandomNumber(minRange, maxRange); - const selectedMovie = this.movies.find(movie => movie.number === randomNumber); - - if (selectedMovie) { - this.displayMovie(selectedMovie); - } else { - this.showError('Kunde inte hitta Beck-filmen. Vänligen försök igen.'); - } - } catch (error) { - console.error('Error generating random movie:', error); - this.showError('Ett fel uppstod. Vänligen försök igen.'); - } - }, 1000); - } - - getRandomNumber(min, max) { + } catch (error) { + console.error("Error generating random movie:", error); + this.showError("Ett fel uppstod. Vänligen försök igen."); + } + }, 1000); + } + + getRandomNumber(min, max) { // Use crypto.getRandomValues for secure random number generation const range = max - min + 1; const randomBuffer = new Uint32Array(1); globalThis.crypto.getRandomValues(randomBuffer); - const randomNumber = randomBuffer[0] / (0xFFFFFFFF + 1); + const randomNumber = randomBuffer[0] / (0xffffffff + 1); return Math.floor(randomNumber * range) + min; - } + } + + displayMovie(movie) { + const movieResult = document.getElementById("movie-result"); + const movieImage = document.getElementById("movie-image"); + const movieTitle = document.getElementById("movie-title"); + const movieRating = document.getElementById("movie-rating"); + const movieDescription = document.getElementById("movie-description"); + const movieLink = document.getElementById("movie-link"); + + // Set movie information with year + movieTitle.textContent = `${movie.title} (${movie.year})`; - displayMovie(movie) { - const movieResult = document.getElementById('movie-result'); - const movieImage = document.getElementById('movie-image'); - const movieTitle = document.getElementById('movie-title'); - const movieRating = document.getElementById('movie-rating'); - const movieDescription = document.getElementById('movie-description'); - const movieLink = document.getElementById('movie-link'); - - // Set movie information with year - movieTitle.textContent = `${movie.title} (${movie.year})`; - - // Handle IMDB rating and runtime display - if (movie.imdbRating || movie.runtime) { - let ratingContent = ''; - - if (movie.imdbRating) { - ratingContent += ` + // Handle IMDB rating and runtime display + if (movie.imdbRating || movie.runtime) { + let ratingContent = ""; + + if (movie.imdbRating) { + ratingContent += `
IMDB: ${movie.imdbRating} ★
`; - } - - if (movie.runtime) { - ratingContent += ` + } + + if (movie.runtime) { + ratingContent += `
Längd: ${movie.runtime} 🕐
`; - } - - movieRating.innerHTML = ratingContent; - movieRating.classList.remove('hidden'); - } else { - movieRating.classList.add('hidden'); - } - - movieDescription.textContent = movie.description; - - // Set TV4 Play link or hide if not available - if (movie.tv4playUrl) { - movieLink.href = movie.tv4playUrl; - movieLink.textContent = 'Streama filmen på TV4 Play'; - movieLink.style.display = 'inline-flex'; - } else { - movieLink.style.display = 'none'; - } + } - // Handle poster image loading - movieImage.src = `img/moviePosters/beck${movie.number}.png`; - movieImage.alt = `Affisch för ${movie.title}`; - - // Hide loading and error states - this.hideLoading(); - this.hideError(); - - // Show movie result with animation - movieResult.classList.remove('hidden'); - - // Smooth scroll to result - setTimeout(() => { - movieResult.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - }, 100); + movieRating.innerHTML = ratingContent; + movieRating.classList.remove("hidden"); + } else { + movieRating.classList.add("hidden"); } - showLoading() { - const loading = document.getElementById('loading'); - const movieResult = document.getElementById('movie-result'); - const error = document.getElementById('error'); - - movieResult.classList.add('hidden'); - error.classList.add('hidden'); - loading.classList.remove('hidden'); - - // Smooth scroll to loading indicator - setTimeout(() => { - loading.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - }, 100); - } + movieDescription.textContent = movie.description; - hideLoading() { - const loading = document.getElementById('loading'); - loading.classList.add('hidden'); + // Set TV4 Play link or hide if not available + if (movie.tv4playUrl) { + movieLink.href = movie.tv4playUrl; + movieLink.textContent = "Streama filmen på TV4 Play"; + movieLink.style.display = "inline-flex"; + } else { + movieLink.style.display = "none"; } - showError(message) { - const error = document.getElementById('error'); - const movieResult = document.getElementById('movie-result'); - const loading = document.getElementById('loading'); - - error.querySelector('p').textContent = message; - - movieResult.classList.add('hidden'); - loading.classList.add('hidden'); - error.classList.remove('hidden'); - - // Smooth scroll to error - setTimeout(() => { - error.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }); - }, 100); - } - - hideError() { - const error = document.getElementById('error'); - error.classList.add('hidden'); - } + // Handle poster image loading + movieImage.src = `img/moviePosters/beck${movie.number}.png`; + movieImage.alt = `Affisch för ${movie.title}`; + + // Hide loading and error states + this.hideLoading(); + this.hideError(); + + // Show movie result with animation + movieResult.classList.remove("hidden"); + + // Smooth scroll to result + setTimeout(() => { + movieResult.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 100); + } + + showLoading() { + const loading = document.getElementById("loading"); + const movieResult = document.getElementById("movie-result"); + const error = document.getElementById("error"); + + movieResult.classList.add("hidden"); + error.classList.add("hidden"); + loading.classList.remove("hidden"); + + // Smooth scroll to loading indicator + setTimeout(() => { + loading.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 100); + } + + hideLoading() { + const loading = document.getElementById("loading"); + loading.classList.add("hidden"); + } + + showError(message) { + const error = document.getElementById("error"); + const movieResult = document.getElementById("movie-result"); + const loading = document.getElementById("loading"); + + error.querySelector("p").textContent = message; + + movieResult.classList.add("hidden"); + loading.classList.add("hidden"); + error.classList.remove("hidden"); + + // Smooth scroll to error + setTimeout(() => { + error.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 100); + } + + hideError() { + const error = document.getElementById("error"); + error.classList.add("hidden"); + } } // Initialize the application when DOM is loaded -document.addEventListener('DOMContentLoaded', () => { - const generator = new BeckMovieGenerator(); - // Store reference to prevent it from being garbage collected - globalThis.beckGenerator = generator; +document.addEventListener("DOMContentLoaded", () => { + const generator = new BeckMovieGenerator(); + // Store reference to prevent it from being garbage collected + globalThis.beckGenerator = generator; }); // Add some nice visual feedback for inputs -document.addEventListener('DOMContentLoaded', () => { - const inputs = document.querySelectorAll('input[type="number"]'); - - inputs.forEach(input => { - input.addEventListener('focus', function() { - this.style.transform = 'scale(1.05)'; - }); - - input.addEventListener('blur', function() { - this.style.transform = 'scale(1)'; - }); +document.addEventListener("DOMContentLoaded", () => { + const inputs = document.querySelectorAll('input[type="number"]'); + + inputs.forEach((input) => { + input.addEventListener("focus", function () { + this.style.transform = "scale(1.05)"; + }); + + input.addEventListener("blur", function () { + this.style.transform = "scale(1)"; }); + }); }); // Add keyboard support for better accessibility -document.addEventListener('keydown', (event) => { - if (event.key === 'Enter') { - const generateBtn = document.getElementById('generate-btn'); - if (!generateBtn.disabled) { - generateBtn.click(); - } +document.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + const generateBtn = document.getElementById("generate-btn"); + if (!generateBtn.disabled) { + generateBtn.click(); } + } }); diff --git a/src/style.css b/src/style.css index ee9b14f..83af6fb 100644 --- a/src/style.css +++ b/src/style.css @@ -1,508 +1,512 @@ * { - margin: 0; - padding: 0; - box-sizing: border-box; + margin: 0; + padding: 0; + box-sizing: border-box; } body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background-color: #0B0E13; - color: #E5EAF0; - line-height: 1.6; - min-height: 100vh; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + background-color: #0b0e13; + color: #e5eaf0; + line-height: 1.6; + min-height: 100vh; } .container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; - min-height: 100vh; - display: flex; - flex-direction: column; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + min-height: 100vh; + display: flex; + flex-direction: column; } header { - text-align: center; - margin-bottom: 40px; - padding: 40px 20px; + text-align: center; + margin-bottom: 40px; + padding: 40px 20px; } header h1 { - font-size: 2.5rem; - color: #3BA3E5; - margin-bottom: 10px; - font-weight: 700; + font-size: 2.5rem; + color: #3ba3e5; + margin-bottom: 10px; + font-weight: 700; } header p { - font-size: 1.2rem; - color: #9AA2B1; - font-weight: 300; + font-size: 1.2rem; + color: #9aa2b1; + font-weight: 300; } main { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; + flex: 1; + display: flex; + flex-direction: column; + align-items: center; } .controls { - background-color: #141922; - padding: 30px; - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - margin-bottom: 40px; - width: 100%; - max-width: 600px; - border: 1px solid rgba(59, 163, 229, 0.1); + background-color: #141922; + padding: 30px; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + margin-bottom: 40px; + width: 100%; + max-width: 600px; + border: 1px solid rgba(59, 163, 229, 0.1); } .all-movies-option { - text-align: center; - margin-bottom: 25px; + text-align: center; + margin-bottom: 25px; } .checkbox-container { - display: inline-flex; - align-items: center; - font-size: 1.1rem; - font-weight: 500; - color: #E5EAF0; - cursor: pointer; - user-select: none; + display: inline-flex; + align-items: center; + font-size: 1.1rem; + font-weight: 500; + color: #e5eaf0; + cursor: pointer; + user-select: none; } .checkbox-container input { - display: none; + display: none; } .checkmark { - height: 20px; - width: 20px; - background-color: #0B0E13; - border: 2px solid #3BA3E5; - border-radius: 4px; - margin-right: 10px; - position: relative; - transition: all 0.3s ease; + height: 20px; + width: 20px; + background-color: #0b0e13; + border: 2px solid #3ba3e5; + border-radius: 4px; + margin-right: 10px; + position: relative; + transition: all 0.3s ease; } .checkbox-container:hover .checkmark { - border-color: #2E8BC7; - transform: scale(1.05); + border-color: #2e8bc7; + transform: scale(1.05); } .checkbox-container input:checked ~ .checkmark { - background-color: #3BA3E5; - border-color: #3BA3E5; + background-color: #3ba3e5; + border-color: #3ba3e5; } .checkbox-container input:checked ~ .checkmark:after { - content: ""; - position: absolute; - left: 5px; - top: 1px; - width: 6px; - height: 10px; - border: solid white; - border-width: 0 2px 2px 0; - transform: rotate(45deg); + content: ""; + position: absolute; + left: 5px; + top: 1px; + width: 6px; + height: 10px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); } .range-selector { - display: flex; - align-items: center; - justify-content: center; - gap: 15px; - margin-bottom: 30px; - flex-wrap: wrap; - transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + margin-bottom: 30px; + flex-wrap: wrap; + transition: all 0.3s ease; } .range-selector.dimmed { - opacity: 0.4; - pointer-events: none; - transform: scale(0.95); + opacity: 0.4; + pointer-events: none; + transform: scale(0.95); } .range-selector label { - font-weight: 500; - color: #E5EAF0; - font-size: 1rem; + font-weight: 500; + color: #e5eaf0; + font-size: 1rem; } .range-selector input { - background-color: #0B0E13; - border: 2px solid #3BA3E5; - color: #E5EAF0; - padding: 12px 16px; - border-radius: 8px; - font-size: 1rem; - width: 80px; - text-align: center; - transition: all 0.3s ease; + background-color: #0b0e13; + border: 2px solid #3ba3e5; + color: #e5eaf0; + padding: 12px 16px; + border-radius: 8px; + font-size: 1rem; + width: 80px; + text-align: center; + transition: all 0.3s ease; } .range-selector input:focus { - outline: none; - border-color: #3BA3E5; - box-shadow: 0 0 0 3px rgba(59, 163, 229, 0.2); + outline: none; + border-color: #3ba3e5; + box-shadow: 0 0 0 3px rgba(59, 163, 229, 0.2); } .generate-button { - background: linear-gradient(135deg, #287DB0, #2475A7); - color: white; - border: none; - padding: 16px 32px; - font-size: 1.1rem; - font-weight: 600; - border-radius: 12px; - cursor: pointer; - transition: all 0.3s ease; - width: 100%; - box-shadow: 0 4px 16px rgba(59, 163, 229, 0.3); + background: linear-gradient(135deg, #287db0, #2475a7); + color: white; + border: none; + padding: 16px 32px; + font-size: 1.1rem; + font-weight: 600; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; + width: 100%; + box-shadow: 0 4px 16px rgba(59, 163, 229, 0.3); } .generate-button:hover { - background: linear-gradient(135deg, #2475A7, #206A99); - transform: translateY(-2px); - box-shadow: 0 6px 24px rgba(59, 163, 229, 0.4); + background: linear-gradient(135deg, #2475a7, #206a99); + transform: translateY(-2px); + box-shadow: 0 6px 24px rgba(59, 163, 229, 0.4); } .generate-button:active { - transform: translateY(0); - box-shadow: 0 2px 8px rgba(59, 163, 229, 0.3); + transform: translateY(0); + box-shadow: 0 2px 8px rgba(59, 163, 229, 0.3); } .movie-card { - background-color: #141922; - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - overflow: hidden; - width: 100%; - max-width: 800px; - display: flex; - border: 1px solid rgba(59, 163, 229, 0.1); - transition: all 0.3s ease; + background-color: #141922; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + overflow: hidden; + width: 100%; + max-width: 800px; + display: flex; + border: 1px solid rgba(59, 163, 229, 0.1); + transition: all 0.3s ease; } .movie-card:hover { - transform: translateY(-4px); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); + transform: translateY(-4px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); } .movie-poster { - flex: 0 0 300px; - height: 450px; + flex: 0 0 300px; + height: 450px; } .movie-poster img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 0; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 0; } .movie-details { - flex: 1; - padding: 40px; - display: flex; - flex-direction: column; - justify-content: center; + flex: 1; + padding: 40px; + display: flex; + flex-direction: column; + justify-content: center; } .movie-details h2 { - font-size: 2rem; - color: #3BA3E5; - margin-bottom: 15px; - font-weight: 700; + font-size: 2rem; + color: #3ba3e5; + margin-bottom: 15px; + font-weight: 700; } .movie-rating { - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 15px; - padding: 8px 12px; - background-color: #0B0E13; - border-radius: 8px; - border: 1px solid rgba(59, 163, 229, 0.2); - width: fit-content; - flex-wrap: wrap; + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; + padding: 8px 12px; + background-color: #0b0e13; + border-radius: 8px; + border: 1px solid rgba(59, 163, 229, 0.2); + width: fit-content; + flex-wrap: wrap; } .rating-item { - display: flex; - align-items: center; - gap: 6px; + display: flex; + align-items: center; + gap: 6px; } .rating-label { - color: #9AA2B1; - font-size: 0.9rem; - font-weight: 500; + color: #9aa2b1; + font-size: 0.9rem; + font-weight: 500; } .rating-value { - color: #3BA3E5; - font-weight: 600; - font-size: 1rem; + color: #3ba3e5; + font-weight: 600; + font-size: 1rem; } .rating-star { - color: #FFD700; - font-size: 0.9rem; + color: #ffd700; + font-size: 0.9rem; } .runtime-icon { - color: #9AA2B1; - font-size: 0.9rem; + color: #9aa2b1; + font-size: 0.9rem; } .movie-details p { - margin-bottom: 15px; - color: #9AA2B1; - font-size: 1.1rem; - line-height: 1.7; + margin-bottom: 15px; + color: #9aa2b1; + font-size: 1.1rem; + line-height: 1.7; } .movie-link { - background: linear-gradient(135deg, #E60026, #CC0022); - color: white; - text-decoration: none; - font-weight: 600; - font-size: 1.1rem; - margin-top: 20px; - display: inline-flex; - align-items: center; - padding: 12px 24px; - border-radius: 8px; - transition: all 0.3s ease; - box-shadow: 0 4px 12px rgba(230, 0, 38, 0.3); - border: none; - cursor: pointer; + background: linear-gradient(135deg, #e60026, #cc0022); + color: white; + text-decoration: none; + font-weight: 600; + font-size: 1.1rem; + margin-top: 20px; + display: inline-flex; + align-items: center; + padding: 12px 24px; + border-radius: 8px; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(230, 0, 38, 0.3); + border: none; + cursor: pointer; } .movie-link:hover { - background: linear-gradient(135deg, #CC0022, #B8001F); - transform: translateY(-2px); - box-shadow: 0 6px 16px rgba(230, 0, 38, 0.4); + background: linear-gradient(135deg, #cc0022, #b8001f); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(230, 0, 38, 0.4); } .movie-link:active { - transform: translateY(0); - box-shadow: 0 2px 8px rgba(230, 0, 38, 0.3); + transform: translateY(0); + box-shadow: 0 2px 8px rgba(230, 0, 38, 0.3); } .movie-link::before { - content: '▶'; - margin-right: 8px; - font-size: 1rem; + content: "▶"; + margin-right: 8px; + font-size: 1rem; } .movie-link::after { - content: ''; - margin-left: 8px; + content: ""; + margin-left: 8px; } .loading { - text-align: center; - padding: 60px 20px; + text-align: center; + padding: 60px 20px; } .spinner { - width: 50px; - height: 50px; - border: 4px solid rgba(59, 163, 229, 0.2); - border-left: 4px solid #3BA3E5; - border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto 20px; + width: 50px; + height: 50px; + border: 4px solid rgba(59, 163, 229, 0.2); + border-left: 4px solid #3ba3e5; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 20px; } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } .loading p { - color: #9AA2B1; - font-size: 1.2rem; + color: #9aa2b1; + font-size: 1.2rem; } .error { - /* opaque equivalent of rgba(220, 38, 127, 0.1) over the #0B0E13 page background */ - background-color: #20101E; - border: 2px solid rgba(220, 38, 127, 0.3); - color: #ff6b9d; - padding: 30px; - border-radius: 12px; - text-align: center; - max-width: 600px; - margin: 20px auto; + /* opaque equivalent of rgba(220, 38, 127, 0.1) over the #0B0E13 page background */ + background-color: #20101e; + border: 2px solid rgba(220, 38, 127, 0.3); + color: #ff6b9d; + padding: 30px; + border-radius: 12px; + text-align: center; + max-width: 600px; + margin: 20px auto; } .error p { - font-size: 1.1rem; - margin: 0; + font-size: 1.1rem; + margin: 0; } .hidden { - display: none; + display: none; } footer { - text-align: center; - margin-top: 60px; - padding: 30px 20px; - border-top: 1px solid rgba(59, 163, 229, 0.1); + text-align: center; + margin-top: 60px; + padding: 30px 20px; + border-top: 1px solid rgba(59, 163, 229, 0.1); } .footer-content { - display: flex; - align-items: center; - justify-content: center; - gap: 15px; - flex-wrap: wrap; + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + flex-wrap: wrap; } footer p { - color: #9AA2B1; - font-size: 0.9rem; - margin: 0; + color: #9aa2b1; + font-size: 0.9rem; + margin: 0; } .social-links { - display: flex; - align-items: center; - gap: 12px; + display: flex; + align-items: center; + gap: 12px; } .github-link, .linkedin-link { - color: #9AA2B1; - transition: all 0.3s ease; - display: flex; - align-items: center; - text-decoration: none; - padding: 5px; - border-radius: 6px; + color: #9aa2b1; + transition: all 0.3s ease; + display: flex; + align-items: center; + text-decoration: none; + padding: 5px; + border-radius: 6px; } .github-link:hover, .linkedin-link:hover { - color: #3BA3E5; - transform: translateY(-2px); - /* opaque equivalent of rgba(59, 163, 229, 0.1) over the #0B0E13 page background */ - background-color: #101D28; + color: #3ba3e5; + transform: translateY(-2px); + /* opaque equivalent of rgba(59, 163, 229, 0.1) over the #0B0E13 page background */ + background-color: #101d28; } .github-icon, .linkedin-icon { - width: 20px; - height: 20px; - transition: all 0.3s ease; + width: 20px; + height: 20px; + transition: all 0.3s ease; } .github-link:hover .github-icon, .linkedin-link:hover .linkedin-icon { - transform: scale(1.1); + transform: scale(1.1); } /* Mobile Responsiveness */ @media (max-width: 768px) { - header h1 { - font-size: 2rem; - } - - header p { - font-size: 1rem; - } - - .container { - padding: 15px; - } - - .controls { - padding: 25px 20px; - } - - .range-selector { - gap: 10px; - } - - .range-selector input { - width: 70px; - padding: 10px 12px; - font-size: 0.9rem; - } - - .generate-button { - padding: 14px 24px; - font-size: 1rem; - } - - .movie-card { - flex-direction: column; - max-width: 100%; - } - - .movie-poster { - flex: none; - height: 400px; - width: 100%; - } - - .movie-poster img { - object-fit: contain; - background-color: #0B0E13; - } - - .movie-details { - padding: 25px; - } - - .movie-details h2 { - font-size: 1.6rem; - } - - .movie-details p { - font-size: 1rem; - } + header h1 { + font-size: 2rem; + } + + header p { + font-size: 1rem; + } + + .container { + padding: 15px; + } + + .controls { + padding: 25px 20px; + } + + .range-selector { + gap: 10px; + } + + .range-selector input { + width: 70px; + padding: 10px 12px; + font-size: 0.9rem; + } + + .generate-button { + padding: 14px 24px; + font-size: 1rem; + } + + .movie-card { + flex-direction: column; + max-width: 100%; + } + + .movie-poster { + flex: none; + height: 400px; + width: 100%; + } + + .movie-poster img { + object-fit: contain; + background-color: #0b0e13; + } + + .movie-details { + padding: 25px; + } + + .movie-details h2 { + font-size: 1.6rem; + } + + .movie-details p { + font-size: 1rem; + } } @media (max-width: 480px) { - header h1 { - font-size: 1.8rem; - } - - .range-selector { - flex-direction: column; - align-items: stretch; - text-align: center; - } - - .range-selector input { - width: 100%; - max-width: 120px; - margin: 0 auto; - } - - .movie-poster { - height: 350px; - } - - .movie-poster img { - object-fit: contain; - background-color: #0B0E13; - } - - .movie-details { - padding: 20px; - } - - .movie-details h2 { - font-size: 1.4rem; - } + header h1 { + font-size: 1.8rem; + } + + .range-selector { + flex-direction: column; + align-items: stretch; + text-align: center; + } + + .range-selector input { + width: 100%; + max-width: 120px; + margin: 0 auto; + } + + .movie-poster { + height: 350px; + } + + .movie-poster img { + object-fit: contain; + background-color: #0b0e13; + } + + .movie-details { + padding: 20px; + } + + .movie-details h2 { + font-size: 1.4rem; + } } diff --git a/tests/fetchRatings.test.js b/tests/fetchRatings.test.js index 8343a05..58dd796 100644 --- a/tests/fetchRatings.test.js +++ b/tests/fetchRatings.test.js @@ -1,355 +1,496 @@ -jest.mock('fs'); +jest.mock("fs"); -const fs = require('fs'); -const { gzipSync } = require('zlib'); +const fs = require("fs"); +const { gzipSync } = require("zlib"); const { - extractTitleId, - fetchRatingsMap, - formatMovie, - loadMoviesFromDb, - main, - parseMovies, - parseRatingsDataset, - runMain, - serializeMovies, - updateRatings -} = require('../src/fetchRatings.js'); + extractTitleId, + fetchRatingsMap, + formatMovie, + loadMoviesFromDb, + main, + parseMovies, + parseRatingsDataset, + runMain, + serializeMovies, + updateRatings, +} = require("../src/fetchRatings.js"); function createDatasetResponse(datasetContent, overrides = {}) { - const compressed = gzipSync(datasetContent); - return { - ok: true, - status: 200, - statusText: 'OK', - arrayBuffer: jest.fn().mockResolvedValue( - compressed.buffer.slice(compressed.byteOffset, compressed.byteOffset + compressed.byteLength) + const compressed = gzipSync(datasetContent); + return { + ok: true, + status: 200, + statusText: "OK", + arrayBuffer: jest + .fn() + .mockResolvedValue( + compressed.buffer.slice( + compressed.byteOffset, + compressed.byteOffset + compressed.byteLength, ), - ...overrides - }; + ), + ...overrides, + }; } -describe('fetchRatings.js', () => { - let consoleLogSpy; - let consoleErrorSpy; - - beforeEach(() => { - jest.clearAllMocks(); - consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleLogSpy.mockRestore(); - consoleErrorSpy.mockRestore(); - }); - - test('extractTitleId returns the IMDb title id when present', () => { - expect(extractTitleId('https://imdb.com/title/tt0118693')).toBe('tt0118693'); - }); - - test('extractTitleId returns null when the URL is missing or invalid', () => { - expect(extractTitleId()).toBeNull(); - expect(extractTitleId('https://imdb.com/name/nm0000001')).toBeNull(); - }); - - test('parseMovies clones an exported movie array', () => { - const movies = parseMovies([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt1' }]); - expect(movies).toHaveLength(1); - expect(movies[0].title).toBe('Movie'); - expect(movies[0]).not.toBeUndefined(); - }); - - test('parseMovies throws when the database export is not an array', () => { - expect(() => parseMovies({ title: 'Movie' })).toThrow('beckDB.js must export an array of movies.'); - }); - - test('loadMoviesFromDb reads the exported database module', () => { - const movies = loadMoviesFromDb(require.resolve('../database/beckDB.js')); - - expect(Array.isArray(movies)).toBe(true); - expect(movies.length).toBeGreaterThan(0); - expect(movies[0]).toHaveProperty('title'); - }); - - test('loadMoviesFromDb uses the default database path when no path is provided', () => { - const movies = loadMoviesFromDb(); - - expect(Array.isArray(movies)).toBe(true); - expect(movies.length).toBeGreaterThan(0); - }); - - test('parseRatingsDataset filters to the required title ids', () => { - const dataset = ['tconst\taverageRating\tnumVotes', 'tt1\t6.2\t100', 'tt2\t7.3\t200', 'tt3\t8.0\t300', ''].join('\n'); - const ratings = parseRatingsDataset(dataset, new Set(['tt2', 'tt3'])); - - expect(ratings.size).toBe(2); - expect(ratings.get('tt1')).toBeUndefined(); - expect(ratings.get('tt2')).toBe('7.3'); - expect(ratings.get('tt3')).toBe('8.0'); - }); - - test('parseRatingsDataset skips blank lines in the dataset', () => { - const dataset = ['tconst\taverageRating\tnumVotes', 'tt1\t6.2\t100', '', 'tt2\t7.3\t200'].join('\n'); - const ratings = parseRatingsDataset(dataset, new Set(['tt9'])); - - expect(ratings.size).toBe(0); - }); - - test('fetchRatingsMap parses the gzipped dataset response', async () => { - const fetchImpl = jest.fn().mockResolvedValue( - createDatasetResponse(['tconst\taverageRating\tnumVotes', 'tt0118693\t6.2\t1952'].join('\n')) - ); - - const ratings = await fetchRatingsMap(new Set(['tt0118693']), fetchImpl); - - expect(fetchImpl).toHaveBeenCalledTimes(1); - expect(ratings.get('tt0118693')).toBe('6.2'); - }); - - test('fetchRatingsMap throws when fetch is unavailable', async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = undefined; - - await expect(fetchRatingsMap(new Set(['tt1']))).rejects.toThrow('Fetch API is not available'); - - globalThis.fetch = originalFetch; - }); - - test('fetchRatingsMap throws when the dataset request fails', async () => { - const fetchImpl = jest.fn().mockResolvedValue({ - ok: false, - status: 503, - statusText: 'Service Unavailable' - }); +describe("fetchRatings.js", () => { + let consoleLogSpy; + let consoleErrorSpy; + + beforeEach(() => { + jest.clearAllMocks(); + consoleLogSpy = jest.spyOn(console, "log").mockImplementation(() => {}); + consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + consoleErrorSpy.mockRestore(); + }); + + test("extractTitleId returns the IMDb title id when present", () => { + expect(extractTitleId("https://imdb.com/title/tt0118693")).toBe( + "tt0118693", + ); + }); + + test("extractTitleId returns null when the URL is missing or invalid", () => { + expect(extractTitleId()).toBeNull(); + expect(extractTitleId("https://imdb.com/name/nm0000001")).toBeNull(); + }); + + test("parseMovies clones an exported movie array", () => { + const movies = parseMovies([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt1", + }, + ]); + expect(movies).toHaveLength(1); + expect(movies[0].title).toBe("Movie"); + expect(movies[0]).not.toBeUndefined(); + }); + + test("parseMovies throws when the database export is not an array", () => { + expect(() => parseMovies({ title: "Movie" })).toThrow( + "beckDB.js must export an array of movies.", + ); + }); + + test("loadMoviesFromDb reads the exported database module", () => { + const movies = loadMoviesFromDb(require.resolve("../database/beckDB.js")); + + expect(Array.isArray(movies)).toBe(true); + expect(movies.length).toBeGreaterThan(0); + expect(movies[0]).toHaveProperty("title"); + }); + + test("loadMoviesFromDb uses the default database path when no path is provided", () => { + const movies = loadMoviesFromDb(); + + expect(Array.isArray(movies)).toBe(true); + expect(movies.length).toBeGreaterThan(0); + }); + + test("parseRatingsDataset filters to the required title ids", () => { + const dataset = [ + "tconst\taverageRating\tnumVotes", + "tt1\t6.2\t100", + "tt2\t7.3\t200", + "tt3\t8.0\t300", + "", + ].join("\n"); + const ratings = parseRatingsDataset(dataset, new Set(["tt2", "tt3"])); + + expect(ratings.size).toBe(2); + expect(ratings.get("tt1")).toBeUndefined(); + expect(ratings.get("tt2")).toBe("7.3"); + expect(ratings.get("tt3")).toBe("8.0"); + }); + + test("parseRatingsDataset skips blank lines in the dataset", () => { + const dataset = [ + "tconst\taverageRating\tnumVotes", + "tt1\t6.2\t100", + "", + "tt2\t7.3\t200", + ].join("\n"); + const ratings = parseRatingsDataset(dataset, new Set(["tt9"])); + + expect(ratings.size).toBe(0); + }); + + test("fetchRatingsMap parses the gzipped dataset response", async () => { + const fetchImpl = jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt0118693\t6.2\t1952"].join( + "\n", + ), + ), + ); - await expect(fetchRatingsMap(new Set(['tt1']), fetchImpl)).rejects.toThrow('Failed to fetch IMDb ratings dataset: 503 Service Unavailable'); - }); + const ratings = await fetchRatingsMap(new Set(["tt0118693"]), fetchImpl); - test('formatMovie includes all optional fields and escapes strings', () => { - const formatted = formatMovie({ - number: 1, - title: 'Quote "Test"', - year: 2020, - description: 'Back\\slash and "quote"', - imdbUrl: 'https://imdb.com/title/tt1', - tv4playUrl: 'https://tv4play.se', - runtime: '90 min', - imdbRating: '7.5' - }); - - expect(formatted).toContain('title: "Quote \\"Test\\""'); - expect(formatted).toContain('description: "Back\\\\slash and \\"quote\\""'); - expect(formatted).toContain('tv4playUrl: "https://tv4play.se",'); - expect(formatted).toContain('runtime: "90 min",'); - expect(formatted).toContain('imdbRating: "7.5"'); - }); + expect(fetchImpl).toHaveBeenCalledTimes(1); + expect(ratings.get("tt0118693")).toBe("6.2"); + }); - test('serializeMovies preserves formatting for minimal movie objects', () => { - const output = serializeMovies([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt1' }]); - - expect(output).toBe([ - 'const beckMovies = [', - ' {', - ' number: 1,', - ' title: "Movie",', - ' year: 2020,', - ' imdbUrl: "https://imdb.com/title/tt1"', - ' }', - '];', - '', - "if (typeof module !== 'undefined' && module.exports) {", - ' module.exports = beckMovies;', - '}', - '' - ].join('\n')); - }); + test("fetchRatingsMap throws when fetch is unavailable", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = undefined; - test('formatMovie omits optional fields that are not present', () => { - const formatted = formatMovie({ - number: 2, - title: 'TV4 Only', - year: 2021, - imdbUrl: 'https://imdb.com/title/tt2', - tv4playUrl: 'https://tv4play.se/watch' - }); - - expect(formatted).toContain('tv4playUrl: "https://tv4play.se/watch"'); - expect(formatted).not.toContain('description:'); - expect(formatted).not.toContain('runtime:'); - expect(formatted).not.toContain('imdbRating:'); - }); + await expect(fetchRatingsMap(new Set(["tt1"]))).rejects.toThrow( + "Fetch API is not available", + ); - test('formatMovie keeps runtime without adding an imdb rating line', () => { - const formatted = formatMovie({ - number: 3, - title: 'Runtime Only', - year: 2022, - imdbUrl: 'https://imdb.com/title/tt3', - runtime: '110 min' - }); - - expect(formatted).toContain('runtime: "110 min"'); - expect(formatted).not.toContain('imdbRating:'); - }); + globalThis.fetch = originalFetch; + }); - test('updateRatings handles the complete flow with mixed movie states', async () => { - fs.writeFileSync.mockImplementation(() => {}); - const loadMovies = jest.fn().mockReturnValue([ - { - number: 1, - title: 'Rated Movie', - year: 2020, - imdbUrl: 'https://imdb.com/title/tt1234567', - runtime: '90 min' - }, - { - number: 2, - title: 'Missing Rating', - year: 2021, - imdbUrl: 'https://imdb.com/title/tt7654321' - }, - { - number: 3, - title: 'Invalid URL', - year: 2022, - imdbUrl: 'https://imdb.com/name/nm1234567' - }, - { - number: 4, - title: 'No URL', - year: 2023 - } - ]); - - const fetchImpl = jest.fn().mockResolvedValue(createDatasetResponse([ - 'tconst\taverageRating\tnumVotes', - 'tt1234567\t7.5\t1200' - ].join('\n'))); - - const movies = await updateRatings({ fetchImpl, loadMovies }); - - expect(movies[0].imdbRating).toBe('7.5'); - expect(movies[1].imdbRating).toBeUndefined(); - expect(loadMovies).toHaveBeenCalledTimes(1); - expect(fs.writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('imdbRating: "7.5"'), 'utf8'); - expect(fs.writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('module.exports = beckMovies;'), 'utf8'); - expect(consoleLogSpy).toHaveBeenCalledWith('Fetching latest IMDb ratings dataset...'); - expect(consoleLogSpy).toHaveBeenCalledWith('Updating IMDb rating for: Rated Movie'); - expect(consoleLogSpy).toHaveBeenCalledWith(' -> Rating: 7.5'); - expect(consoleLogSpy).toHaveBeenCalledWith('Updating IMDb rating for: Missing Rating'); - expect(consoleLogSpy).toHaveBeenCalledWith(' -> Rating not found'); - expect(consoleLogSpy).toHaveBeenCalledWith('Invalid imdbUrl for: Invalid URL'); - expect(consoleLogSpy).toHaveBeenCalledWith('No imdbUrl for: No URL'); - expect(consoleLogSpy).toHaveBeenCalledWith('beckDB.js updated with latest IMDb ratings.'); + test("fetchRatingsMap throws when the dataset request fails", async () => { + const fetchImpl = jest.fn().mockResolvedValue({ + ok: false, + status: 503, + statusText: "Service Unavailable", }); - test('updateRatings returns null on parse error', async () => { - const loadMovies = jest.fn().mockImplementation(() => { - throw new Error('bad parse'); - }); - - const result = await updateRatings({ fetchImpl: jest.fn(), loadMovies }); - - expect(result).toBeNull(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to parse beckDB.js:', expect.any(Error)); + await expect(fetchRatingsMap(new Set(["tt1"]), fetchImpl)).rejects.toThrow( + "Failed to fetch IMDb ratings dataset: 503 Service Unavailable", + ); + }); + + test("formatMovie includes all optional fields and escapes strings", () => { + const formatted = formatMovie({ + number: 1, + title: 'Quote "Test"', + year: 2020, + description: 'Back\\slash and "quote"', + imdbUrl: "https://imdb.com/title/tt1", + tv4playUrl: "https://tv4play.se", + runtime: "90 min", + imdbRating: "7.5", }); - test('updateRatings returns null when fetching the dataset fails', async () => { - const loadMovies = jest.fn().mockReturnValue([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt1' }]); - - const fetchImpl = jest.fn().mockRejectedValue(new Error('network down')); - const result = await updateRatings({ fetchImpl, loadMovies }); - - expect(result).toBeNull(); - expect(fs.writeFileSync).not.toHaveBeenCalled(); - expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch IMDb ratings dataset:', expect.any(Error)); + expect(formatted).toContain('title: "Quote \\"Test\\""'); + expect(formatted).toContain('description: "Back\\\\slash and \\"quote\\""'); + expect(formatted).toContain('tv4playUrl: "https://tv4play.se",'); + expect(formatted).toContain('runtime: "90 min",'); + expect(formatted).toContain('imdbRating: "7.5"'); + }); + + test("serializeMovies preserves formatting for minimal movie objects", () => { + const output = serializeMovies([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt1", + }, + ]); + + expect(output).toBe( + [ + "const beckMovies = [", + " {", + " number: 1,", + ' title: "Movie",', + " year: 2020,", + ' imdbUrl: "https://imdb.com/title/tt1"', + " }", + "];", + "", + "if (typeof module !== 'undefined' && module.exports) {", + " module.exports = beckMovies;", + "}", + "", + ].join("\n"), + ); + }); + + test("formatMovie omits optional fields that are not present", () => { + const formatted = formatMovie({ + number: 2, + title: "TV4 Only", + year: 2021, + imdbUrl: "https://imdb.com/title/tt2", + tv4playUrl: "https://tv4play.se/watch", }); - test('updateRatings supports injected movie loading and default global fetch', async () => { - const loadMovies = jest.fn().mockReturnValue([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt42' }]); - const writeFileSync = jest.fn(); - const originalFetch = globalThis.fetch; - globalThis.fetch = jest.fn().mockResolvedValue(createDatasetResponse([ - 'tconst\taverageRating\tnumVotes', - 'tt42\t8.4\t4200' - ].join('\n'))); - - const result = await updateRatings({ loadMovies, writeFileSync }); - - expect(result[0].imdbRating).toBe('8.4'); - expect(loadMovies).toHaveBeenCalledTimes(1); - expect(writeFileSync).toHaveBeenCalledTimes(1); - - globalThis.fetch = originalFetch; + expect(formatted).toContain('tv4playUrl: "https://tv4play.se/watch"'); + expect(formatted).not.toContain("description:"); + expect(formatted).not.toContain("runtime:"); + expect(formatted).not.toContain("imdbRating:"); + }); + + test("formatMovie keeps runtime without adding an imdb rating line", () => { + const formatted = formatMovie({ + number: 3, + title: "Runtime Only", + year: 2022, + imdbUrl: "https://imdb.com/title/tt3", + runtime: "110 min", }); - test('updateRatings uses the default database loader when no loader is injected', async () => { - fs.writeFileSync.mockImplementation(() => {}); - const originalFetch = globalThis.fetch; - globalThis.fetch = jest.fn().mockResolvedValue(createDatasetResponse([ - 'tconst\taverageRating\tnumVotes', - 'tt0118693\t6.2\t1952' - ].join('\n'))); - - const result = await updateRatings(); - - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - expect(fs.writeFileSync).toHaveBeenCalledTimes(1); - - globalThis.fetch = originalFetch; - }); - - test('main delegates to updateRatings', async () => { - const loadMovies = jest.fn().mockReturnValue([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt99' }]); - const writeFileSync = jest.fn(); - const originalFetch = globalThis.fetch; - globalThis.fetch = jest.fn().mockResolvedValue(createDatasetResponse([ - 'tconst\taverageRating\tnumVotes', - 'tt99\t6.8\t680' - ].join('\n'))); - - await main({ loadMovies, writeFileSync }); - - expect(writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('imdbRating: "6.8"'), 'utf8'); - - globalThis.fetch = originalFetch; + expect(formatted).toContain('runtime: "110 min"'); + expect(formatted).not.toContain("imdbRating:"); + }); + + test("updateRatings handles the complete flow with mixed movie states", async () => { + fs.writeFileSync.mockImplementation(() => {}); + const loadMovies = jest.fn().mockReturnValue([ + { + number: 1, + title: "Rated Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt1234567", + runtime: "90 min", + }, + { + number: 2, + title: "Missing Rating", + year: 2021, + imdbUrl: "https://imdb.com/title/tt7654321", + }, + { + number: 3, + title: "Invalid URL", + year: 2022, + imdbUrl: "https://imdb.com/name/nm1234567", + }, + { + number: 4, + title: "No URL", + year: 2023, + }, + ]); + + const fetchImpl = jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt1234567\t7.5\t1200"].join( + "\n", + ), + ), + ); + + const movies = await updateRatings({ fetchImpl, loadMovies }); + + expect(movies[0].imdbRating).toBe("7.5"); + expect(movies[1].imdbRating).toBeUndefined(); + expect(loadMovies).toHaveBeenCalledTimes(1); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('imdbRating: "7.5"'), + "utf8", + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining("module.exports = beckMovies;"), + "utf8", + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Fetching latest IMDb ratings dataset...", + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Updating IMDb rating for: Rated Movie", + ); + expect(consoleLogSpy).toHaveBeenCalledWith(" -> Rating: 7.5"); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Updating IMDb rating for: Missing Rating", + ); + expect(consoleLogSpy).toHaveBeenCalledWith(" -> Rating not found"); + expect(consoleLogSpy).toHaveBeenCalledWith( + "Invalid imdbUrl for: Invalid URL", + ); + expect(consoleLogSpy).toHaveBeenCalledWith("No imdbUrl for: No URL"); + expect(consoleLogSpy).toHaveBeenCalledWith( + "beckDB.js updated with latest IMDb ratings.", + ); + }); + + test("updateRatings returns null on parse error", async () => { + const loadMovies = jest.fn().mockImplementation(() => { + throw new Error("bad parse"); }); - test('runMain delegates to main when no unexpected error occurs', async () => { - const loadMovies = jest.fn().mockReturnValue([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt100' }]); - const writeFileSync = jest.fn(); - const originalFetch = globalThis.fetch; - globalThis.fetch = jest.fn().mockResolvedValue(createDatasetResponse([ - 'tconst\taverageRating\tnumVotes', - 'tt100\t7.1\t710' - ].join('\n'))); - - await runMain({ loadMovies, writeFileSync }); - - expect(writeFileSync).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('imdbRating: "7.1"'), 'utf8'); - expect(consoleErrorSpy).not.toHaveBeenCalledWith('Unexpected error while updating ratings:', expect.any(Error)); - - globalThis.fetch = originalFetch; + const result = await updateRatings({ fetchImpl: jest.fn(), loadMovies }); + + expect(result).toBeNull(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to parse beckDB.js:", + expect.any(Error), + ); + }); + + test("updateRatings returns null when fetching the dataset fails", async () => { + const loadMovies = jest.fn().mockReturnValue([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt1", + }, + ]); + + const fetchImpl = jest.fn().mockRejectedValue(new Error("network down")); + const result = await updateRatings({ fetchImpl, loadMovies }); + + expect(result).toBeNull(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to fetch IMDb ratings dataset:", + expect.any(Error), + ); + }); + + test("updateRatings supports injected movie loading and default global fetch", async () => { + const loadMovies = jest.fn().mockReturnValue([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt42", + }, + ]); + const writeFileSync = jest.fn(); + const originalFetch = globalThis.fetch; + globalThis.fetch = jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt42\t8.4\t4200"].join("\n"), + ), + ); + + const result = await updateRatings({ loadMovies, writeFileSync }); + + expect(result[0].imdbRating).toBe("8.4"); + expect(loadMovies).toHaveBeenCalledTimes(1); + expect(writeFileSync).toHaveBeenCalledTimes(1); + + globalThis.fetch = originalFetch; + }); + + test("updateRatings uses the default database loader when no loader is injected", async () => { + fs.writeFileSync.mockImplementation(() => {}); + const originalFetch = globalThis.fetch; + globalThis.fetch = jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt0118693\t6.2\t1952"].join( + "\n", + ), + ), + ); + + const result = await updateRatings(); + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + expect(fs.writeFileSync).toHaveBeenCalledTimes(1); + + globalThis.fetch = originalFetch; + }); + + test("main delegates to updateRatings", async () => { + const loadMovies = jest.fn().mockReturnValue([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt99", + }, + ]); + const writeFileSync = jest.fn(); + const originalFetch = globalThis.fetch; + globalThis.fetch = jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt99\t6.8\t680"].join("\n"), + ), + ); + + await main({ loadMovies, writeFileSync }); + + expect(writeFileSync).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('imdbRating: "6.8"'), + "utf8", + ); + + globalThis.fetch = originalFetch; + }); + + test("runMain delegates to main when no unexpected error occurs", async () => { + const loadMovies = jest.fn().mockReturnValue([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt100", + }, + ]); + const writeFileSync = jest.fn(); + const originalFetch = globalThis.fetch; + globalThis.fetch = jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt100\t7.1\t710"].join("\n"), + ), + ); + + await runMain({ loadMovies, writeFileSync }); + + expect(writeFileSync).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('imdbRating: "7.1"'), + "utf8", + ); + expect(consoleErrorSpy).not.toHaveBeenCalledWith( + "Unexpected error while updating ratings:", + expect.any(Error), + ); + + globalThis.fetch = originalFetch; + }); + + test("runMain reports unexpected errors and sets exitCode", async () => { + const originalExitCode = process.exitCode; + + await runMain({ + loadMovies: jest.fn().mockReturnValue([ + { + number: 1, + title: "Movie", + year: 2020, + imdbUrl: "https://imdb.com/title/tt101", + }, + ]), + writeFileSync: jest.fn(() => { + throw new Error("disk full"); + }), + fetchImpl: jest + .fn() + .mockResolvedValue( + createDatasetResponse( + ["tconst\taverageRating\tnumVotes", "tt101\t7.9\t790"].join("\n"), + ), + ), }); - test('runMain reports unexpected errors and sets exitCode', async () => { - const originalExitCode = process.exitCode; - - await runMain({ - loadMovies: jest.fn().mockReturnValue([{ number: 1, title: 'Movie', year: 2020, imdbUrl: 'https://imdb.com/title/tt101' }]), - writeFileSync: jest.fn(() => { - throw new Error('disk full'); - }), - fetchImpl: jest.fn().mockResolvedValue(createDatasetResponse([ - 'tconst\taverageRating\tnumVotes', - 'tt101\t7.9\t790' - ].join('\n'))) - }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Unexpected error while updating ratings:", + expect.any(Error), + ); + expect(process.exitCode).toBe(1); - expect(consoleErrorSpy).toHaveBeenCalledWith('Unexpected error while updating ratings:', expect.any(Error)); - expect(process.exitCode).toBe(1); - - process.exitCode = originalExitCode; - }); + process.exitCode = originalExitCode; + }); }); diff --git a/tests/script.test.js b/tests/script.test.js index 1452b6e..ed9d3a6 100644 --- a/tests/script.test.js +++ b/tests/script.test.js @@ -1,34 +1,68 @@ // Mock the beckMovies data before requiring the script global.beckMovies = [ - { number: 1, title: "Beck - Lockpojken", year: 1997, description: "Test movie 1", imdbRating: "6.2", tv4playUrl: "https://tv4play.se/1" }, - { number: 2, title: "Beck - Monstret", year: 1998, description: "Test movie 2" }, - { number: 3, title: "Beck - Vita nätter", year: 1998, description: "Test movie 3", imdbRating: "5.9" }, - { number: 4, title: "Beck - Öga för öga", year: 1998, description: "Test movie 4" }, - { number: 5, title: "Beck - Pojken i glaskulan", year: 2002, description: "Test movie 5" }, - { number: 6, title: "Beck - Only Runtime", year: 2004, description: "Test movie with only runtime", runtime: "105 min" } + { + number: 1, + title: "Beck - Lockpojken", + year: 1997, + description: "Test movie 1", + imdbRating: "6.2", + tv4playUrl: "https://tv4play.se/1", + }, + { + number: 2, + title: "Beck - Monstret", + year: 1998, + description: "Test movie 2", + }, + { + number: 3, + title: "Beck - Vita nätter", + year: 1998, + description: "Test movie 3", + imdbRating: "5.9", + }, + { + number: 4, + title: "Beck - Öga för öga", + year: 1998, + description: "Test movie 4", + }, + { + number: 5, + title: "Beck - Pojken i glaskulan", + year: 2002, + description: "Test movie 5", + }, + { + number: 6, + title: "Beck - Only Runtime", + year: 2004, + description: "Test movie with only runtime", + runtime: "105 min", + }, ]; // Mock DOM methods that might be missing in JSDOM Element.prototype.scrollIntoView = jest.fn(); // Mock crypto.getRandomValues -Object.defineProperty(globalThis, 'crypto', { - value: { - getRandomValues: jest.fn((buffer) => { - buffer[0] = Math.floor(Math.random() * 0xFFFFFFFF); - return buffer; - }) - }, - writable: true +Object.defineProperty(globalThis, "crypto", { + value: { + getRandomValues: jest.fn((buffer) => { + buffer[0] = Math.floor(Math.random() * 0xffffffff); + return buffer; + }), + }, + writable: true, }); -describe('Main Script Tests', () => { - let mockElements; - let generator; - let scriptLoaded = false; +describe("Main Script Tests", () => { + let mockElements; + let generator; + let scriptLoaded = false; - beforeEach(() => { - document.body.innerHTML = ` + beforeEach(() => { + document.body.innerHTML = `
@@ -47,217 +81,227 @@ describe('Main Script Tests', () => {
`; - mockElements = { - minRange: document.getElementById('min-range'), - maxRange: document.getElementById('max-range'), - allMoviesCheckbox: document.getElementById('all-movies-checkbox'), - generateBtn: document.getElementById('generate-btn'), - rangeSelector: document.getElementById('range-selector') - }; - - jest.clearAllTimers(); - jest.useFakeTimers(); - - if (!scriptLoaded) { - require('../src/script.js'); - scriptLoaded = true; - } - - document.dispatchEvent(new Event('DOMContentLoaded')); - generator = globalThis.beckGenerator; - }); + mockElements = { + minRange: document.getElementById("min-range"), + maxRange: document.getElementById("max-range"), + allMoviesCheckbox: document.getElementById("all-movies-checkbox"), + generateBtn: document.getElementById("generate-btn"), + rangeSelector: document.getElementById("range-selector"), + }; - afterEach(() => { - jest.useRealTimers(); - }); + jest.clearAllTimers(); + jest.useFakeTimers(); - test('should initialize and handle range validation', () => { - expect(generator).toBeTruthy(); - - // Test out-of-bounds values - mockElements.minRange.value = '0'; - mockElements.minRange.dispatchEvent(new Event('change')); - expect(mockElements.minRange.value).toBe('1'); - - mockElements.maxRange.value = '100'; - mockElements.maxRange.dispatchEvent(new Event('change')); - expect(mockElements.maxRange.value).toBe('6'); - - // Test min/max adjustment - mockElements.minRange.value = '4'; - mockElements.maxRange.value = '2'; - mockElements.minRange.dispatchEvent(new Event('change')); - expect(mockElements.maxRange.value).toBe('4'); - - // Test invalid range disables button - mockElements.minRange.value = '6'; - mockElements.maxRange.value = '2'; - mockElements.minRange.dispatchEvent(new Event('input')); - expect(mockElements.generateBtn.disabled).toBe(true); - - // Test maxRange input event with valid range - mockElements.minRange.value = '2'; - mockElements.maxRange.value = '4'; - mockElements.maxRange.dispatchEvent(new Event('input')); - expect(mockElements.generateBtn.disabled).toBe(false); - }); + if (!scriptLoaded) { + require("../src/script.js"); + scriptLoaded = true; + } - test('should handle all movies checkbox toggle', () => { - mockElements.allMoviesCheckbox.checked = true; - mockElements.allMoviesCheckbox.dispatchEvent(new Event('change')); - expect(mockElements.rangeSelector.classList.contains('dimmed')).toBe(true); - expect(mockElements.generateBtn.disabled).toBe(false); + document.dispatchEvent(new Event("DOMContentLoaded")); + generator = globalThis.beckGenerator; + }); - mockElements.allMoviesCheckbox.checked = false; - mockElements.allMoviesCheckbox.dispatchEvent(new Event('change')); - expect(mockElements.rangeSelector.classList.contains('dimmed')).toBe(false); - }); + afterEach(() => { + jest.useRealTimers(); + }); - test('should add visual feedback on focus/blur', () => { - mockElements.minRange.dispatchEvent(new Event('focus')); - expect(mockElements.minRange.style.transform).toBe('scale(1.05)'); - - mockElements.minRange.dispatchEvent(new Event('blur')); - expect(mockElements.minRange.style.transform).toBe('scale(1)'); + test("should initialize and handle range validation", () => { + expect(generator).toBeTruthy(); - mockElements.maxRange.dispatchEvent(new Event('focus')); - expect(mockElements.maxRange.style.transform).toBe('scale(1.05)'); - }); + // Test out-of-bounds values + mockElements.minRange.value = "0"; + mockElements.minRange.dispatchEvent(new Event("change")); + expect(mockElements.minRange.value).toBe("1"); - test('should handle keyboard accessibility', () => { - const clickSpy = jest.spyOn(mockElements.generateBtn, 'click'); - - // Should trigger on Enter when enabled - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - expect(clickSpy).toHaveBeenCalled(); - - clickSpy.mockClear(); - - // Should not trigger on other keys - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); - expect(clickSpy).not.toHaveBeenCalled(); - - // Should not trigger when button disabled - mockElements.minRange.value = '10'; - mockElements.minRange.dispatchEvent(new Event('input')); - mockElements.maxRange.value = '1'; - mockElements.maxRange.dispatchEvent(new Event('input')); - document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - expect(clickSpy).toHaveBeenCalledTimes(0); - - clickSpy.mockRestore(); - }); + mockElements.maxRange.value = "100"; + mockElements.maxRange.dispatchEvent(new Event("change")); + expect(mockElements.maxRange.value).toBe("6"); - test('should show error for invalid range', () => { - mockElements.minRange.value = '10'; - mockElements.maxRange.value = '2'; - mockElements.generateBtn.dispatchEvent(new Event('click')); + // Test min/max adjustment + mockElements.minRange.value = "4"; + mockElements.maxRange.value = "2"; + mockElements.minRange.dispatchEvent(new Event("change")); + expect(mockElements.maxRange.value).toBe("4"); - const error = document.getElementById('error'); - expect(error.classList.contains('hidden')).toBe(false); - expect(error.querySelector('p').textContent).toContain('Ogiltigt intervall'); - }); + // Test invalid range disables button + mockElements.minRange.value = "6"; + mockElements.maxRange.value = "2"; + mockElements.minRange.dispatchEvent(new Event("input")); + expect(mockElements.generateBtn.disabled).toBe(true); - test('should generate and display movie with all fields', () => { - mockElements.minRange.value = '1'; - mockElements.maxRange.value = '1'; - mockElements.generateBtn.dispatchEvent(new Event('click')); - - jest.advanceTimersByTime(1100); - - const movieResult = document.getElementById('movie-result'); - expect(movieResult.classList.contains('hidden')).toBe(false); - - // Check IMDB rating shown - const movieRating = document.getElementById('movie-rating'); - expect(movieRating.classList.contains('hidden')).toBe(false); - expect(movieRating.innerHTML).toContain('6.2'); - - // Check TV4 link shown - const movieLink = document.getElementById('movie-link'); - expect(movieLink.style.display).toBe('inline-flex'); - - // Check poster set - const movieImage = document.getElementById('movie-image'); - expect(movieImage.src).toContain('img/moviePosters/beck1.png'); - }); + // Test maxRange input event with valid range + mockElements.minRange.value = "2"; + mockElements.maxRange.value = "4"; + mockElements.maxRange.dispatchEvent(new Event("input")); + expect(mockElements.generateBtn.disabled).toBe(false); + }); - test('should display only runtime when no IMDB rating', () => { - mockElements.minRange.value = '6'; - mockElements.maxRange.value = '6'; - mockElements.generateBtn.dispatchEvent(new Event('click')); - - jest.advanceTimersByTime(1100); - - const movieResult = document.getElementById('movie-result'); - expect(movieResult.classList.contains('hidden')).toBe(false); - - // Check only runtime is shown, no IMDB rating - const movieRating = document.getElementById('movie-rating'); - expect(movieRating.classList.contains('hidden')).toBe(false); - expect(movieRating.innerHTML).not.toContain('IMDB:'); - expect(movieRating.innerHTML).toContain('105 min'); - expect(movieRating.innerHTML).toContain('🕐'); - }); + test("should handle all movies checkbox toggle", () => { + mockElements.allMoviesCheckbox.checked = true; + mockElements.allMoviesCheckbox.dispatchEvent(new Event("change")); + expect(mockElements.rangeSelector.classList.contains("dimmed")).toBe(true); + expect(mockElements.generateBtn.disabled).toBe(false); - test('should handle movie without optional fields', () => { - mockElements.minRange.value = '2'; - mockElements.maxRange.value = '2'; - mockElements.generateBtn.dispatchEvent(new Event('click')); - - jest.advanceTimersByTime(1100); + mockElements.allMoviesCheckbox.checked = false; + mockElements.allMoviesCheckbox.dispatchEvent(new Event("change")); + expect(mockElements.rangeSelector.classList.contains("dimmed")).toBe(false); + }); - expect(document.getElementById('movie-rating').classList.contains('hidden')).toBe(true); - expect(document.getElementById('movie-link').style.display).toBe('none'); - }); + test("should add visual feedback on focus/blur", () => { + mockElements.minRange.dispatchEvent(new Event("focus")); + expect(mockElements.minRange.style.transform).toBe("scale(1.05)"); - test('should handle range adjustment edge cases', () => { - // Test maxValue < minValue adjustment - mockElements.maxRange.value = '2'; - mockElements.minRange.value = '4'; - mockElements.maxRange.dispatchEvent(new Event('change')); - expect(mockElements.minRange.value).toBe('2'); - - // Test minValue > maxValue adjustment (reverse case) - mockElements.minRange.value = '5'; - mockElements.maxRange.value = '3'; - mockElements.minRange.dispatchEvent(new Event('change')); - expect(mockElements.maxRange.value).toBe('5'); - }); + mockElements.minRange.dispatchEvent(new Event("blur")); + expect(mockElements.minRange.style.transform).toBe("scale(1)"); - test('should generate with all movies checkbox', () => { - mockElements.allMoviesCheckbox.checked = true; - mockElements.allMoviesCheckbox.dispatchEvent(new Event('change')); - mockElements.generateBtn.dispatchEvent(new Event('click')); - - jest.advanceTimersByTime(1100); - - expect(document.getElementById('movie-result').classList.contains('hidden')).toBe(false); - }); + mockElements.maxRange.dispatchEvent(new Event("focus")); + expect(mockElements.maxRange.style.transform).toBe("scale(1.05)"); + }); + + test("should handle keyboard accessibility", () => { + const clickSpy = jest.spyOn(mockElements.generateBtn, "click"); + + // Should trigger on Enter when enabled + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); + expect(clickSpy).toHaveBeenCalled(); + + clickSpy.mockClear(); + + // Should not trigger on other keys + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); + expect(clickSpy).not.toHaveBeenCalled(); + + // Should not trigger when button disabled + mockElements.minRange.value = "10"; + mockElements.minRange.dispatchEvent(new Event("input")); + mockElements.maxRange.value = "1"; + mockElements.maxRange.dispatchEvent(new Event("input")); + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); + expect(clickSpy).toHaveBeenCalledTimes(0); + + clickSpy.mockRestore(); + }); + + test("should show error for invalid range", () => { + mockElements.minRange.value = "10"; + mockElements.maxRange.value = "2"; + mockElements.generateBtn.dispatchEvent(new Event("click")); + + const error = document.getElementById("error"); + expect(error.classList.contains("hidden")).toBe(false); + expect(error.querySelector("p").textContent).toContain( + "Ogiltigt intervall", + ); + }); + + test("should generate and display movie with all fields", () => { + mockElements.minRange.value = "1"; + mockElements.maxRange.value = "1"; + mockElements.generateBtn.dispatchEvent(new Event("click")); + + jest.advanceTimersByTime(1100); - test('should handle movie not found and exceptions', () => { - const originalGetRandom = generator.getRandomNumber; - - // Test movie not found - generator.getRandomNumber = jest.fn(() => 999); - mockElements.generateBtn.dispatchEvent(new Event('click')); - jest.advanceTimersByTime(1100); - - let error = document.getElementById('error'); - expect(error.classList.contains('hidden')).toBe(false); - expect(error.querySelector('p').textContent).toContain('Kunde inte hitta'); - - // Test exception - generator.getRandomNumber = jest.fn(() => { throw new Error('Test'); }); - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - - mockElements.generateBtn.dispatchEvent(new Event('click')); - jest.advanceTimersByTime(1100); - - expect(consoleSpy).toHaveBeenCalled(); - expect(error.querySelector('p').textContent).toContain('Ett fel uppstod'); - - generator.getRandomNumber = originalGetRandom; - consoleSpy.mockRestore(); + const movieResult = document.getElementById("movie-result"); + expect(movieResult.classList.contains("hidden")).toBe(false); + + // Check IMDB rating shown + const movieRating = document.getElementById("movie-rating"); + expect(movieRating.classList.contains("hidden")).toBe(false); + expect(movieRating.innerHTML).toContain("6.2"); + + // Check TV4 link shown + const movieLink = document.getElementById("movie-link"); + expect(movieLink.style.display).toBe("inline-flex"); + + // Check poster set + const movieImage = document.getElementById("movie-image"); + expect(movieImage.src).toContain("img/moviePosters/beck1.png"); + }); + + test("should display only runtime when no IMDB rating", () => { + mockElements.minRange.value = "6"; + mockElements.maxRange.value = "6"; + mockElements.generateBtn.dispatchEvent(new Event("click")); + + jest.advanceTimersByTime(1100); + + const movieResult = document.getElementById("movie-result"); + expect(movieResult.classList.contains("hidden")).toBe(false); + + // Check only runtime is shown, no IMDB rating + const movieRating = document.getElementById("movie-rating"); + expect(movieRating.classList.contains("hidden")).toBe(false); + expect(movieRating.innerHTML).not.toContain("IMDB:"); + expect(movieRating.innerHTML).toContain("105 min"); + expect(movieRating.innerHTML).toContain("🕐"); + }); + + test("should handle movie without optional fields", () => { + mockElements.minRange.value = "2"; + mockElements.maxRange.value = "2"; + mockElements.generateBtn.dispatchEvent(new Event("click")); + + jest.advanceTimersByTime(1100); + + expect( + document.getElementById("movie-rating").classList.contains("hidden"), + ).toBe(true); + expect(document.getElementById("movie-link").style.display).toBe("none"); + }); + + test("should handle range adjustment edge cases", () => { + // Test maxValue < minValue adjustment + mockElements.maxRange.value = "2"; + mockElements.minRange.value = "4"; + mockElements.maxRange.dispatchEvent(new Event("change")); + expect(mockElements.minRange.value).toBe("2"); + + // Test minValue > maxValue adjustment (reverse case) + mockElements.minRange.value = "5"; + mockElements.maxRange.value = "3"; + mockElements.minRange.dispatchEvent(new Event("change")); + expect(mockElements.maxRange.value).toBe("5"); + }); + + test("should generate with all movies checkbox", () => { + mockElements.allMoviesCheckbox.checked = true; + mockElements.allMoviesCheckbox.dispatchEvent(new Event("change")); + mockElements.generateBtn.dispatchEvent(new Event("click")); + + jest.advanceTimersByTime(1100); + + expect( + document.getElementById("movie-result").classList.contains("hidden"), + ).toBe(false); + }); + + test("should handle movie not found and exceptions", () => { + const originalGetRandom = generator.getRandomNumber; + + // Test movie not found + generator.getRandomNumber = jest.fn(() => 999); + mockElements.generateBtn.dispatchEvent(new Event("click")); + jest.advanceTimersByTime(1100); + + let error = document.getElementById("error"); + expect(error.classList.contains("hidden")).toBe(false); + expect(error.querySelector("p").textContent).toContain("Kunde inte hitta"); + + // Test exception + generator.getRandomNumber = jest.fn(() => { + throw new Error("Test"); }); + const consoleSpy = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + + mockElements.generateBtn.dispatchEvent(new Event("click")); + jest.advanceTimersByTime(1100); + + expect(consoleSpy).toHaveBeenCalled(); + expect(error.querySelector("p").textContent).toContain("Ett fel uppstod"); + + generator.getRandomNumber = originalGetRandom; + consoleSpy.mockRestore(); + }); });