diff --git a/6-refactoring/exercises/dom-and-events/mouse-coordinates/data/constants.js b/6-refactoring/exercises/dom-and-events/mouse-coordinates/data/constants.js index e69de29b..c19eca8f 100644 --- a/6-refactoring/exercises/dom-and-events/mouse-coordinates/data/constants.js +++ b/6-refactoring/exercises/dom-and-events/mouse-coordinates/data/constants.js @@ -0,0 +1,3 @@ +export const MOUSE_USER_INTERFACE = 'user-interface'; +export const MOUSE_MOVE = 'mousemove'; +export const MOUSE_POSITION = 'mouse-position'; \ No newline at end of file diff --git a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/handler.js b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/handler.js index 68c01a10..fd08f2c3 100644 --- a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/handler.js +++ b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/handler.js @@ -1,3 +1,16 @@ -import { formatCoordinates } from './utils.js'; +import { formatCoordinates } from './util.js'; -export const showMouseCoordinates = () => {}; +export const showMouseCoordinates = (event) => { + debugger; + // read & process user input + const xValue = event.pageX; + const yValue = event.pageY; + // execute core logic + const formattedCoordinates = formatCoordinates(xValue, yValue); + // render result for user + document.getElementById(MOUSE_POSITION).innerHTML = formattedCoordinates; + // log action for developers + console.log('\n--- new coordinates ---'); + console.log('x:', xValue); + console.log('y:', yValue); + }; \ No newline at end of file diff --git a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/init.js b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/init.js index 52f28e2d..0a5662e2 100644 --- a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/init.js +++ b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/init.js @@ -1,4 +1,7 @@ -document + +import './listener.js' + +/*document .getElementById('user-interface') .addEventListener('mousemove', (event) => { debugger; @@ -16,4 +19,4 @@ document console.log('\n--- new coordinates ---'); console.log('x:', xValue); console.log('y:', yValue); - }); + });*/ diff --git a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/listener.js b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/listener.js index 8c9e2681..62990cd8 100644 --- a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/listener.js +++ b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/listener.js @@ -1 +1,13 @@ import { showMouseCoordinates } from './handler.js'; +document + .getElementById(MOUSE_USER_INTERFACE) + .addEventListener(MOUSE_MOVE, showMouseCoordinates); + + + + + + + + + diff --git a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.js b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.js index 713bde23..f3e7eb4a 100644 --- a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.js +++ b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.js @@ -1,4 +1,5 @@ -/** - * - */ -export const formatCoordinates = () => {}; +export const formatCoordinates = (xValue , yValue) => { + // execute core logic + const formattedCoordinates = 'X: ' + xValue + '\nY: ' + yValue; + return formattedCoordinates; +}; \ No newline at end of file diff --git a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.spec.js b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.spec.js index d4aca866..ae902a69 100644 --- a/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.spec.js +++ b/6-refactoring/exercises/dom-and-events/mouse-coordinates/src/util.spec.js @@ -1,4 +1,4 @@ -import { formatCoordinates } from './utils.js'; +import { formatCoordinates } from './util.js'; describe('formatCoordinates: formats two numbers into a coordinates string', () => { it('positive numbers', () => { diff --git a/exercises/convertTemperature/index.html b/exercises/convertTemperature/index.html new file mode 100644 index 00000000..6295a3bc --- /dev/null +++ b/exercises/convertTemperature/index.html @@ -0,0 +1,18 @@ + + + + + Convert Temperatures + + + + +
+ + + +
+ + + + diff --git a/exercises/convertTemperature/src/data/constants.js b/exercises/convertTemperature/src/data/constants.js new file mode 100644 index 00000000..e14994b4 --- /dev/null +++ b/exercises/convertTemperature/src/data/constants.js @@ -0,0 +1,15 @@ +// Events +export const ON_CHANGE = 'change'; + +// Selectors +export const TEMPERATURES_INPUT = 'temperatures'; +export const CONVERT_TEMPERATURE_CONTAINER = 'convertedTemperatures'; + +// Regex +export const SPACE_REGEX = /\s+/; +export const NUMBERS_SPACES_REGEX = /^[0-9\s]*$/; +export const PLACEHOLDER_REGEX = /{([0-9]+)}/g; + +// Messages +export const MESSAGE_ERROR_NOT_INTEGER = + "The '{0}' contains values different of integer numbers"; diff --git a/exercises/convertTemperature/src/handlers/convert-temperatures-handler.js b/exercises/convertTemperature/src/handlers/convert-temperatures-handler.js new file mode 100644 index 00000000..af79336a --- /dev/null +++ b/exercises/convertTemperature/src/handlers/convert-temperatures-handler.js @@ -0,0 +1,65 @@ +import { + CONVERT_TEMPERATURE_CONTAINER, + MESSAGE_ERROR_NOT_INTEGER, +} from '../data/constants.js'; +import { fahrenheitToCelcius } from '../logic/convert-temperatures.js'; +import { parseToNumberArray } from '../utils/parsers.js'; +import { replacePlaceholders } from '../utils/strings.js'; +import { containsOnlyNumbersAndSpaces } from '../utils/validators.js'; + +const prepareOutputContainer = () => { + const convertedTemperaturesContainer = document.getElementById( + CONVERT_TEMPERATURE_CONTAINER, + ); + convertedTemperaturesContainer.innerHTML = ''; + return convertedTemperaturesContainer; +}; + +const renderOutputContainer = (outputContainer, celsiusList) => { + celsiusList.forEach((celsius) => { + const liString = `
  • ${celsius.toFixed(2)}
  • `; + outputContainer.innerHTML = outputContainer.innerHTML + liString; + }); +}; + +const validateTextInput = (text) => { + if (!text) { + throw new Error(); + } + + if (!containsOnlyNumbersAndSpaces(text)) { + throw new Error(replacePlaceholders(MESSAGE_ERROR_NOT_INTEGER, text)); + } +}; + +const convertTemperatures = (fahrenheitTextList) => { + validateTextInput(fahrenheitTextList); + const fahrenheitList = parseToNumberArray(fahrenheitTextList); + const celsiusList = fahrenheitList.map((fahrenheit) => { + return fahrenheitToCelcius(fahrenheit); + }); + return celsiusList; +}; + +/** + * Add an 'change' event listener to the element whose id is 'temperatures'. + * + * @param {Event} event - the 'change' event. + */ +export const convertTemperaturesHandler = (event) => { + try { + // Input + const convertedTemperaturesContainer = prepareOutputContainer(); + + // Logic + const fahrenheitTextList = event.target.value; + const celsiusList = convertTemperatures(fahrenheitTextList); + + // Output + renderOutputContainer(convertedTemperaturesContainer, celsiusList); + } catch (error) { + if (error.message) { + window.alert(error.message); + } + } +}; diff --git a/exercises/convertTemperature/src/handlers/convert-temperatures-handler.test.js b/exercises/convertTemperature/src/handlers/convert-temperatures-handler.test.js new file mode 100644 index 00000000..2b0942e8 --- /dev/null +++ b/exercises/convertTemperature/src/handlers/convert-temperatures-handler.test.js @@ -0,0 +1,49 @@ +/** + * @jest-environment jsdom + */ + +import { + CONVERT_TEMPERATURE_CONTAINER, + MESSAGE_ERROR_NOT_INTEGER, + TEMPERATURES_INPUT, +} from '../data/constants'; +import { replacePlaceholders } from '../utils/strings'; +import { convertTemperaturesHandler } from './convert-temperatures-handler'; + +describe('convertTemperaturesHandler', () => { + beforeEach(() => { + document.body.innerHTML = ` +
    + +
    +
    + `; + }); + + it('should convert the input temperatures', () => { + // GIVEN + const event = { target: { value: '0 32 64' } }; + // WHEN + convertTemperaturesHandler(event); + // THEN + const innerHTML = document.getElementById( + CONVERT_TEMPERATURE_CONTAINER, + ).innerHTML; + expect(innerHTML).toContain('>-17.78<'); + expect(innerHTML).toContain('>0.00<'); + expect(innerHTML).toContain('>17.78<'); + }); + + it('should show an alert when invalid input', () => { + // GIVEN + const input = '0 invalid 64'; + const event = { target: { value: input } }; + window.alert = jest.fn(); + // WHEN + convertTemperaturesHandler(event); + // THEN + expect(window.alert).toHaveBeenCalledWith( + replacePlaceholders(MESSAGE_ERROR_NOT_INTEGER, input), + ); + }); +}); diff --git a/exercises/convertTemperature/src/init.js b/exercises/convertTemperature/src/init.js new file mode 100644 index 00000000..9ff9e758 --- /dev/null +++ b/exercises/convertTemperature/src/init.js @@ -0,0 +1,4 @@ +import { convertTemperaturesHandler } from './handlers/convert-temperatures-handler.js'; +import { addTemperaturesChangeListener } from './listeners/temperatures-input-listener.js'; + +addTemperaturesChangeListener(convertTemperaturesHandler); diff --git a/exercises/convertTemperature/src/listeners/temperatures-input-listener.js b/exercises/convertTemperature/src/listeners/temperatures-input-listener.js new file mode 100644 index 00000000..b9c3e71a --- /dev/null +++ b/exercises/convertTemperature/src/listeners/temperatures-input-listener.js @@ -0,0 +1,12 @@ +import { ON_CHANGE, TEMPERATURES_INPUT } from '../data/constants.js'; + +/** + * Add an 'change' event listener to the element whose id is 'temperatures'. + * + * @param {function} handler - Callback function to be called when the event occurs. + */ +export const addTemperaturesChangeListener = (handler) => { + document + .getElementById(TEMPERATURES_INPUT) + ?.addEventListener(ON_CHANGE, handler); +}; diff --git a/exercises/convertTemperature/src/listeners/temperatures-input-listener.test.js b/exercises/convertTemperature/src/listeners/temperatures-input-listener.test.js new file mode 100644 index 00000000..ddad61dc --- /dev/null +++ b/exercises/convertTemperature/src/listeners/temperatures-input-listener.test.js @@ -0,0 +1,31 @@ +/** + * @jest-environment jsdom + */ + +import { ON_CHANGE, TEMPERATURES_INPUT } from '../data/constants'; +import { addTemperaturesChangeListener } from './temperatures-input-listener'; + +describe('addTemperatureChangeListener', () => { + beforeEach(() => { + document.body.innerHTML = ` +
    + +
    + `; + }); + + it('should add a handler to the temperature change', () => { + // GIVEN + const handler = jest.fn(); + const dispatchChangeTemperatures = () => { + document + .getElementById(TEMPERATURES_INPUT) + .dispatchEvent(new window.Event(ON_CHANGE)); + }; + // WHEN + addTemperaturesChangeListener(handler); + dispatchChangeTemperatures(); + // THEN + expect(handler).toHaveBeenCalledTimes(1); + }); +}); diff --git a/exercises/convertTemperature/src/logic/convert-temperatures.js b/exercises/convertTemperature/src/logic/convert-temperatures.js new file mode 100644 index 00000000..5e0d5901 --- /dev/null +++ b/exercises/convertTemperature/src/logic/convert-temperatures.js @@ -0,0 +1,9 @@ +/** + * Convert a temperature in Fahrenheit degrees to Celcius degrees. + * + * @param {number} fahrenheit - Temperature in Fahrenheit degrees. + * @returns {number} - Temperature in Celcius degrees. + */ +export const fahrenheitToCelcius = (fahrenheit) => { + return ((fahrenheit - 32) * 5) / 9; +}; diff --git a/exercises/convertTemperature/src/logic/convert-temperatures.test.js b/exercises/convertTemperature/src/logic/convert-temperatures.test.js new file mode 100644 index 00000000..f65252ce --- /dev/null +++ b/exercises/convertTemperature/src/logic/convert-temperatures.test.js @@ -0,0 +1,14 @@ +import { fahrenheitToCelcius } from './convert-temperatures'; + +describe('fahrenheitToCelcius', () => { + describe.each` + fahrenheit | expected + ${0} | ${-17.77777777777778} + ${32} | ${0} + ${64} | ${17.77777777777778} + `('$fahrenheit fahrenheit', ({ fahrenheit, expected }) => { + it(`returns ${expected} celcius`, () => { + expect(fahrenheitToCelcius(fahrenheit)).toEqual(expected); + }); + }); +}); diff --git a/exercises/convertTemperature/src/utils/parsers.js b/exercises/convertTemperature/src/utils/parsers.js new file mode 100644 index 00000000..e1dadc23 --- /dev/null +++ b/exercises/convertTemperature/src/utils/parsers.js @@ -0,0 +1,18 @@ +import { SPACE_REGEX } from '../data/constants.js'; + +/** + * Convert a text containing numbers to an array of numbers. + * If the input text contains non valid numbers, NaN is used as its substitute. + * + * @param {string} text - The text to be parsed. + * @returns {number[]} - Array of numbers. + */ +export const parseToNumberArray = (text) => { + if (text) { + return text + .trim() + .split(SPACE_REGEX) + .map((textNumber) => Number(textNumber)); + } + return []; +}; diff --git a/exercises/convertTemperature/src/utils/parsers.test.js b/exercises/convertTemperature/src/utils/parsers.test.js new file mode 100644 index 00000000..e6b33ecc --- /dev/null +++ b/exercises/convertTemperature/src/utils/parsers.test.js @@ -0,0 +1,18 @@ +import { parseToNumberArray } from './parsers'; + +describe('parseToNumberArray', () => { + describe.each` + text | expected + ${null} | ${[]} + ${undefined} | ${[]} + ${''} | ${[]} + ${'1 2 3'} | ${[1, 2, 3]} + ${' \t 1 \n 2 3 '} | ${[1, 2, 3]} + ${'-1 -2 -3'} | ${[-1, -2, -3]} + ${'1 a @'} | ${[1, NaN, NaN]} + `('text $text', ({ text, expected }) => { + it(`returns ${expected}`, () => { + expect(parseToNumberArray(text)).toEqual(expected); + }); + }); +}); diff --git a/exercises/convertTemperature/src/utils/strings.js b/exercises/convertTemperature/src/utils/strings.js new file mode 100644 index 00000000..33e0f3d8 --- /dev/null +++ b/exercises/convertTemperature/src/utils/strings.js @@ -0,0 +1,18 @@ +import { PLACEHOLDER_REGEX } from '../data/constants.js'; + +/** + * Returns a new string with all placeholders ({0}, {1}, {n}) replaced. + * Reference: https://medium.com/@onlinemsr/javascript-string-format-the-best-3-ways-to-do-it-c6a12b4b94ed + * + * @param {string} template - The string with placeholders. + * @param {string[]} args - The replacements. + * @returns {string} - The string with all placeholders ({0}, {1}, {n}) replaced. + */ +export const replacePlaceholders = (template, ...args) => { + if (template) { + return template.replace(PLACEHOLDER_REGEX, function (match, index) { + return typeof args[index] === 'undefined' ? match : args[index]; + }); + } + return template; +}; diff --git a/exercises/convertTemperature/src/utils/strings.test.js b/exercises/convertTemperature/src/utils/strings.test.js new file mode 100644 index 00000000..06231241 --- /dev/null +++ b/exercises/convertTemperature/src/utils/strings.test.js @@ -0,0 +1,24 @@ +import { replacePlaceholders } from './strings'; + +describe('replacePlaceholders', () => { + describe.each` + text | replacements | expected + ${null} | ${[]} | ${null} + ${null} | ${['text']} | ${null} + ${''} | ${[]} | ${''} + ${''} | ${['text']} | ${''} + ${'text'} | ${['text']} | ${'text'} + ${'{0} text {0}'} | ${['zero']} | ${'zero text zero'} + ${'{0} text {1}'} | ${['zero', 'one']} | ${'zero text one'} + ${'{0} text {1}'} | ${['zero', 'one']} | ${'zero text one'} + ${'{0} text {1}'} | ${[]} | ${'{0} text {1}'} + ${'{0} text {1}'} | ${['zero']} | ${'zero text {1}'} + `( + 'text $text with replacements $replacements', + ({ text, replacements, expected }) => { + it(`returns ${expected}`, () => { + expect(replacePlaceholders(text, ...replacements)).toBe(expected); + }); + }, + ); +}); diff --git a/exercises/convertTemperature/src/utils/validators.js b/exercises/convertTemperature/src/utils/validators.js new file mode 100644 index 00000000..6d991ea5 --- /dev/null +++ b/exercises/convertTemperature/src/utils/validators.js @@ -0,0 +1,11 @@ +import { NUMBERS_SPACES_REGEX } from '../data/constants.js'; + +/** + * Check if the input text contains only number characters or spaces. + * + * @param {string} text - The text to be checked. + * @returns {boolean} - It returns true if the text match with /^[0-9\s]*$/ or false otherwise. + */ +export const containsOnlyNumbersAndSpaces = (text) => { + return NUMBERS_SPACES_REGEX.test(text); +}; diff --git a/exercises/convertTemperature/src/utils/validators.test.js b/exercises/convertTemperature/src/utils/validators.test.js new file mode 100644 index 00000000..3905329e --- /dev/null +++ b/exercises/convertTemperature/src/utils/validators.test.js @@ -0,0 +1,18 @@ +import { containsOnlyNumbersAndSpaces } from './validators'; + +describe('containsOnlyNumbersAndSpaces', () => { + describe.each` + text | expected + ${null} | ${false} + ${undefined} | ${false} + ${''} | ${true} + ${'1 2 3'} | ${true} + ${' \t 1 \n 2 3 '} | ${true} + ${'-1 -2 -3'} | ${false} + ${'1 a @'} | ${false} + `('text $text', ({ text, expected }) => { + it(`returns ${expected}`, () => { + expect(containsOnlyNumbersAndSpaces(text)).toEqual(expected); + }); + }); +}); diff --git a/exercises/convertTemperature/styles.css b/exercises/convertTemperature/styles.css new file mode 100644 index 00000000..a8ac6631 --- /dev/null +++ b/exercises/convertTemperature/styles.css @@ -0,0 +1,3 @@ +.number-item { + font-family: fantasy; +} diff --git a/package-lock.json b/package-lock.json index 6d02f551..d8e1810d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@types/jest": "^29.5.12", "jest": "^29.6.1", "jest-environment-jsdom": "^29.6.1", "prettier": "^2.3.2" @@ -1038,6 +1039,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/jsdom": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", @@ -4792,6 +4803,16 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/jsdom": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", diff --git a/package.json b/package.json index 4ae947a5..1f575409 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,10 @@ }, "homepage": "https://github.com/HackYourFutureBelgium/separation-of-concerns#readme", "devDependencies": { - "prettier": "^2.3.2", "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@types/jest": "^29.5.12", "jest": "^29.6.1", - "jest-environment-jsdom": "^29.6.1" + "jest-environment-jsdom": "^29.6.1", + "prettier": "^2.3.2" } }