curriculum/challenges/english/blocks/lab-currency-converter/67eaa957114d373deb3a9149.md
Objective: Fulfill the user stories below and get all the tests to pass to complete the lab.
User Stories:
Your CurrencyConverter component should render an input element to accept the amount to be converted from.
Your input element should accept numbers.
Your CurrencyConverter component should render two select elements to choose the currency to convert from and to.
Your select element should include options for at least USD, EUR, GBP, and JPY. You may use any exchange rate, provided there is no one-to-one mapping between the currencies. Here are some examples of good and bad mappings:
const badMapping = {
USD: 1,
EUR: 1,
GBP: 1,
JPY: 1
};
const goodMapping = {
USD: 1,
EUR: 0.92,
GBP: 0.78,
JPY: 156.7
};
Your CurrencyConverter component should memoize the calculation of the converted amounts for the from currency such that a change in the to select option will not recalculate the converted amounts.
Your CurrencyConverter component should render an element showing the converted amount in the format XX.XX CCC, where XX.XX is the converted amount and CCC is the currency code.
The converted amount should be rounded to two decimal places.
You should export a CurrencyConverter component.
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
assert.property(exports, "CurrencyConverter");
You should have one input[type="number"] element to accept the amount to be converted from.
const inp = document.querySelectorAll('input[type="number"]');
assert.equal(inp.length, 1);
You should have two select elements.
const selects = document.querySelectorAll('select');
assert.equal(selects.length, 2);
The select elements should have options for at least USD, EUR, GBP, and JPY.
const selects = [...document.querySelectorAll('select')];
assert.equal(selects.length, 2);
for (const select of selects) {
const options = [...select.options].map((o) => o.value);
assert.includeMembers(options, ["USD", "EUR", "GBP", "JPY"]);
}
Changing the value of the first select element should cause the converted amounts to be recalculated.
function spyOn(
obj,
method
) {
const original = obj[method];
const calls = [];
const fn = (cb, deps) => {
const result = original(() => {calls.push(1); return cb();}, deps);
return result;
};
obj[method] = fn;
fn.calls = calls;
return fn;
}
const abuseMemo = spyOn(React, "useMemo");
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const s = await __helpers.prepTestComponent(exports.CurrencyConverter);
const [first, _second] = s.querySelectorAll('select');
assert.exists(first);
await changeSelectValue(first);
assert.equal(abuseMemo.calls.length, 2);
Changing the value of the second select element should not cause the converted amounts to be recalculated.
function spyOn(
obj,
method
) {
const original = obj[method];
const calls = [];
const fn = (cb, deps) => {
const result = original(() => {calls.push(1); return cb();}, deps);
return result;
};
obj[method] = fn;
fn.calls = calls;
return fn;
}
const abuseMemo = spyOn(React, "useMemo");
const script = [...document.querySelectorAll("script")].find((s) => s.dataset.src === "index.jsx").innerText;
const exports = {};
const a = eval(script);
const s = await __helpers.prepTestComponent(exports.CurrencyConverter);
const [_first, second] = s.querySelectorAll('select');
assert.exists(second);
await changeSelectValue(second);
assert.equal(abuseMemo.calls.length, 1);
Changing the value of the first select element should display the new converted amount.
const s = await __helpers.prepTestComponent(window.index.CurrencyConverter);
const inp = s.querySelector('input[type="number"]');
assert.exists(inp);
await React.act(async () => {
inp.value = 10;
const ev = new Event("change", { bubbles: true, cancelable: false });
inp[Object.keys(inp).find((k) => k.startsWith("__reactProps"))]
.onChange({...ev, target: inp});
});
const nonFormContentBefore = getInnerTextExcept(s, "input,select");
const resultBefore = nonFormContentBefore.match(/(\d+\.\d{2})\s*([A-Z]{3})/);
const [first, _second] = s.querySelectorAll('select');
assert.exists(first);
await changeSelectValue(first);
const nonFormContentAfter = getInnerTextExcept(s, "input,select");
const resultAfter = nonFormContentAfter.match(/(\d+\.\d{2})\s*([A-Z]{3})/);
try {
assert.notEqual(nonFormContentBefore, nonFormContentAfter);
assert.notEqual(resultBefore[1], resultAfter[1]);
assert.strictEqual(resultBefore[2], resultAfter[2]);
} catch (e) {
console.error(e);
throw e;
}
Changing the value of the second select element should display the new converted amount and currency.
const s = await __helpers.prepTestComponent(window.index.CurrencyConverter);
const inp = s.querySelector('input[type="number"]');
assert.exists(inp);
await React.act(async () => {
inp.value = 10;
const ev = new Event("change", { bubbles: true, cancelable: false });
inp[Object.keys(inp).find((k) => k.startsWith("__reactProps"))]
.onChange({...ev, target: inp});
});
const nonFormContentBefore = getInnerTextExcept(s, "input,select");
const resultBefore = nonFormContentBefore.match(/(\d+\.\d{2})\s*([A-Z]{3})/);
const [_first, second] = s.querySelectorAll('select');
assert.exists(second);
await changeSelectValue(second);
const nonFormContentAfter = getInnerTextExcept(s, "input,select");
const resultAfter = nonFormContentAfter.match(/(\d+\.\d{2})\s*([A-Z]{3})/);
try {
assert.notEqual(nonFormContentBefore, nonFormContentAfter);
assert.notEqual(resultBefore[1], resultAfter[1]);
assert.notEqual(resultBefore[2], resultAfter[2]);
assert.strictEqual(resultAfter[2], second.value);
} catch (e) {
console.error(e);
throw e;
}
The converted amount should be displayed in the format XX.XX CCC, where XX.XX is the converted amount rounded to two decimal places and CCC is the currency code.
const s = await __helpers.prepTestComponent(window.index.CurrencyConverter);
const inp = s.querySelector('input[type="number"]');
assert.exists(inp);
const [_first, second] = s.querySelectorAll('select');
assert.exists(second);
await changeSelectValue(second);
await React.act(async () => {
inp.value = 10;
const ev2 = new Event("change", { bubbles: true, cancelable: false });
inp[Object.keys(inp).find((k) => k.startsWith("__reactProps"))].onChange({...ev2, target: inp});
});
const nonFormContent = getInnerTextExcept(s, "input,select");
try {
const currencyCode = second.value;
const reg = new RegExp(`\\d+\\.\\d{2} ${currencyCode}`);
assert.match(nonFormContent, reg);
} catch (e) {
console.error(e);
throw e;
}
The converted amount should be different from the input amount.
const s = await __helpers.prepTestComponent(window.index.CurrencyConverter);
const inp = s.querySelector('input[type="number"]');
assert.exists(inp);
const [first, second] = s.querySelectorAll('select');
assert.exists(first);
assert.exists(second);
for (let i = 0; i < first.options.length; i++) {
for (let j = 0; j < second.options.length; j++) {
if (first.options[i].value === second.options[j].value) {
continue;
}
await React.act(async () => {
first.value = first.options[i].value;
const ev = new Event("change", { bubbles: true, cancelable: false });
first[Object.keys(first).find((k) => k.startsWith("__reactProps"))].onChange({...ev, target: first});
second.value = second.options[j].value;
const ev2 = new Event("change", { bubbles: true, cancelable: false });
second[Object.keys(second).find((k) => k.startsWith("__reactProps"))].onChange({...ev2, target: second});
inp.value = 10;
const ev3 = new Event("change", { bubbles: true, cancelable: false });
inp[Object.keys(inp).find((k) => k.startsWith("__reactProps"))].onChange({...ev3, target: inp});
});
const nonFormContent = getInnerTextExcept(s, "input,select");
const { amount } = nonFormContent.match(/(?<amount>\d+\.\d{2}) [A-Z]{3}/).groups;
try {
assert.notEqual(Number(amount), Number(inp.value));
} catch (e) {
console.error(e);
throw e;
}
}
}
async function delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
function getInnerTextExcept(doc, removingSelector) {
const body = doc.cloneNode(true);
const squareElements = body.querySelectorAll(removingSelector);
squareElements.forEach(element => {
element.parentNode.removeChild(element)
});
return body.innerText;
}
const changeSelectValue = async (selectElement) => {
await React.act(async () => {
const notSelected = [...selectElement.options].find((o) => !o.selected);
selectElement.value = notSelected.value;
const ev = new Event("change", { bubbles: true, cancelable: false });
selectElement[Object.keys(selectElement).find((k) => k.startsWith("__reactProps"))].onChange({...ev, target: selectElement});
});
};
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Currency Converter</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
src="index.jsx"
></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="root"></div>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { CurrencyConverter } from './index.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(<CurrencyConverter />);
</script>
</body>
</html>
const { useState, useMemo } = React;
export function CurrencyConverter() {
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Currency Converter</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.26.5/babel.min.js"></script>
<script
data-plugins="transform-modules-umd"
type="text/babel"
src="index.jsx"
></script>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div id="root"></div>
<script
data-plugins="transform-modules-umd"
type="text/babel"
data-presets="react"
data-type="module"
>
import { CurrencyConverter } from './index.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(<CurrencyConverter />);
</script>
</body>
</html>
body {
background-color: #0a0a23;
font-family: Lato, sans-serif;
}
main {
background-color: #1b1b32;
padding: 20px;
border-radius: 8px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
width: 400px;
margin: 50px auto;
}
h1 {
margin-bottom: 16px;
color: #f5f6f7;
}
label {
color: #f5f6f7;
}
input,
select {
width: 100%;
display: block;
box-sizing: border-box;
padding: 8px;
margin-bottom: 12px;
margin-top: 4px;
border: 1px solid #99c9ff;
border-radius: 4px;
}
.conversion-display {
font-size: 16px;
color: #f5f6f7;
margin-bottom: 10px;
}
p {
font-size: 18px;
font-weight: bold;
color: #acd157;
}
export function CurrencyConverter() {
const [amount, setAmount] = React.useState(1);
const [startCurrency, setStartCurrency] = React.useState("USD");
const [endCurrency, setEndCurrency] = React.useState("EUR");
const exchangeRates = {
USD: 1,
EUR: 0.85,
GBP: 0.75,
JPY: 110
}
const convertedAmounts = React.useMemo(() => {
const converted = {};
Object.keys(exchangeRates).forEach((curr) => {
converted[curr] = ((amount / exchangeRates[startCurrency]) * exchangeRates[curr]).toFixed(2);
});
return converted;
}, [amount, startCurrency]);
return (
<main>
<h1>Currency Converter</h1>
<p className="conversion-display">{startCurrency} to {endCurrency} Conversion</p>
<input
type="number"
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
/>
<label>
Start Currency:
<select value={startCurrency} onChange={(e) => setStartCurrency(e.target.value)}>
{Object.keys(exchangeRates).map((curr) => (
<option key={curr} value={curr}>
{curr}
</option>
))}
</select>
</label>
<label>
Target Currency:
<select value={endCurrency} onChange={(e) => setEndCurrency(e.target.value)}>
{Object.keys(exchangeRates).map((curr) => (
<option key={curr} value={curr}>
{curr}
</option>
))}
</select>
</label>
<p>Converted Amount: <span>{convertedAmounts[endCurrency]}</span> {endCurrency}</p>
</main>
);
}