Skip to content

Commit

Permalink
Merge pull request #55 from mbland/calculator-component
Browse files Browse the repository at this point in the history
Add Calculator frontend component
  • Loading branch information
mbland committed Dec 17, 2023
2 parents bb5c82f + f65d60d commit f2b9ce6
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 4 deletions.
13 changes: 13 additions & 0 deletions strcalc/src/main/frontend/components/calculator.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{!--
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
--}}
<form action="{{ apiUrl }}" method="post">
<label for="numbers" name="numbersLabel" id="numbersLabel">
Enter the string of numbers to add:
</label>
<input type="text" name="numbers" id="numbers" required />
<input type="submit" name="numbersSubmit" id="numbersSubmit" value="Add" />
</form>
<div class="result"><p>The result will appear here.</p></div>
40 changes: 40 additions & 0 deletions strcalc/src/main/frontend/components/calculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import Template from './calculator.hbs'

export default class Calculator {
/**
* Initializes the Calculator within the document.
* @param {object} params - parameters made available to all initializers
* @param {Element} params.appElem - parent Element containing all components
* @param {string} params.apiUrl - API backend server URL
* @param {Function} params.postForm - posts form data to API
*/
static init({ appElem, apiUrl, postForm }) {
const t = Template({ apiUrl })
const [ form, resultElem ] = t.children

appElem.appendChild(t)
form.addEventListener(
'submit', e => this.#submitRequest(e, resultElem, postForm)
)
}

// https://simonplend.com/how-to-use-fetch-to-post-form-data-as-json-to-your-api/
static async #submitRequest(event, resultElem, postForm) {
event.preventDefault()

const result = resultElem.querySelector('p')

try {
const response = await postForm(event.currentTarget)
result.textContent = `Result: ${response.result}`
} catch (err) {
result.textContent = err
}
}
}
51 changes: 51 additions & 0 deletions strcalc/src/main/frontend/components/calculator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-env browser, node, jest, vitest */
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import Calculator from './calculator'
import { afterAll, afterEach, describe, expect, test, vi } from 'vitest'
import { resolvedUrl } from '../test/helpers.js'
import StringCalculatorPage from '../test/page'

// @vitest-environment jsdom
describe('Calculator', () => {
const page = StringCalculatorPage.new()

const setup = () => {
const postForm = vi.fn()
Calculator.init({ appElem: page.appElem, apiUrl: './add', postForm })
return { page, postForm }
}

afterEach(() => page.clear())
afterAll(() => page.remove())

test('renders form and result placeholder', async () => {
const { page } = setup()

expect(page.form()).not.toBeNull()
expect(page.form().action).toBe(resolvedUrl('./add'))
})

test('updates result placeholder with successful result', async () => {
const { page, postForm } = setup()
postForm.mockResolvedValueOnce({ result: 5 })

const result = vi.waitFor(page.enterValueAndSubmit('2,2'))

await expect(result).resolves.toBe('Result: 5')
expect(postForm).toHaveBeenCalledWith(page.form())
})

test('updates result placeholder with error message', async () => {
const { page, postForm } = setup()
postForm.mockRejectedValueOnce(new Error('D\'oh!'))

const result = vi.waitFor(page.enterValueAndSubmit('2,2'))

await expect(result).resolves.toBe('Error: D\'oh!')
})
})
5 changes: 4 additions & 1 deletion strcalc/src/main/frontend/components/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import Placeholder from './placeholder'
import Calculator from './calculator'

/**
* Instantiates the top level objects and calls the `init()` method on each.
Expand All @@ -19,7 +20,9 @@ import Placeholder from './placeholder'
* demonstrate how to design much larger applications for testability.
* @param {object} params - parameters made available to all initializers
* @param {Element} params.appElem - parent Element containing all components
* @param {string} params.apiUrl - API backend server URL
* @param {Function} params.postForm - posts form data to API
*/
export default function initApp(params) {
[Placeholder].forEach(c => c.init(params))
[Placeholder, Calculator].forEach(c => c.init(params))
}
5 changes: 3 additions & 2 deletions strcalc/src/main/frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
* @module main
*/
import initApp from './components/init'
import { postForm } from './components/request'

/**
* Calls the application initializer with the global window and document.
* Calls the app initializer with production parameters.
*
* Wraps the initApp() call in a DOMContentLoaded event listener.
* - main.test.js uses PageLoader to validate that initApp() fires on
Expand All @@ -29,7 +30,7 @@ document.addEventListener(
'DOMContentLoaded',
() => {
const appElem = document.querySelector('#app')
initApp({ appElem })
initApp({ appElem, apiUrl: './add', postForm })
},
{ once: true }
)
24 changes: 23 additions & 1 deletion strcalc/src/main/frontend/test/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,30 @@ export default class StringCalculatorPage {
}

clear() { this.appElem.replaceChildren() }

remove() { this.appElem.remove() }

placeholder() { return this.#select('.placeholder a') }
form() { return this.#select('form') }
input() { return this.#select('form input[name="numbers"]') }
submit() { return this.#select('form input[type="submit"]') }
result() { return this.#select('.result p') }

enterValueAndSubmit(value) {
const orig = this.result().textContent

this.input().value = value

// You would _think_ that this.submit().click() would submit the form...
// Nope:
// - https://developer.mozilla.org/docs/Web/API/HTMLFormElement/requestSubmit
this.form().requestSubmit(this.submit())

return async () => {
const result = this.result().textContent
if (result === orig) {
return Promise.reject(`Result field hasn't changed: ${orig}`)
}
return Promise.resolve(result)
}
}
}

0 comments on commit f2b9ce6

Please sign in to comment.