Lucky customers (16 / 32):
We have some customer records in a text file (customers.json) — one customer per line, JSON-encoded. We want to invite any customer within 100km of our Dublin office for some food and drinks on us. Write a program that will read the full list of customers and output the names and user ids of matching customers (within 100km), sorted by User ID (ascending).
- You can use the first formula from this Wikipedia article to calculate distance. Don't forget, you'll need to convert degrees to radians.
- The GPS coordinates for our Dublin office are 53.3393,-6.2576841.
- You can find the Customer list here.
- Please don’t forget, your code should be production ready, clean and tested!
Hey Intercom team! Andrey here.
Thank you so much for the "production ready, clean and tested" part of the assignment. It lets me demonstrate my skills and actually stand out! Oh, and it's been a real pleasure to build. Please read on to know why!
In case you look into the code and get surprised, I use a relaxed version of the StandardJS code style, ensured by ESLint. Say goodbye to the husk of semicolons, say hello to the spread operator that JSHint chokes on.
I use the pods structure for routes, controllers, components, their templates and styles, and also for their tests and page objects. Other classes such as Ember Data ones are outside of pods.
The data layer
I decided to kick it up a notch: let you switch between several cities and see which customers are close enough to the selected city. The distance limit is also adjustable.
This setup raises a question: where to keep the computed property that calculates the distances? Here are some options that I rejected:
- On the
usermodel. In this case, the model must be aware of the UI state. It can be achieved with a service, but that increases coupling.
- On a service. A feasible option, but I don't like all-knowing services, preferring small fine-grained entities.
- On the controller. This is a great option, but the computed property can't be reused in several places. There's
this.controllerFor(), but it won't work if that controller hasn't yet been visited.
- On a component. It has all the disadvantages of the controller option, but adds another one: when the user leaves the page, the component gets destroyed, and the computed property is no longer cached.
Instead of all of the above, I created a user-city junction model. It represents a unique combination of a user and a city. This is a clientside-only model, populated via
store.push from the model hook.
store.push can be safely called more than once: when the user revisits the route, it's called again, but it doesn't cause duplicates to appear.
With the junction model, every
user-city-junction record is created exactly when it's first needed. The distance is calculated in its computed property and cached on the record, so that the app doesn't need to calculate it again.
This approach might seem an overkill for this simple use case, but I was excited to try it, and it proved to be very clean, consise and efficient.
The network layer
I used the simplest possible adapter implementation: you pass an URL into the
query method, the adapter fetches the content from that URL and passes it on to the serializer. Keep it simple, soldier!
This app is a backendless FastBoot instance. "Backendless FastBoot" sounds contradictory, but it's quite reasonable when you think about it:
ember buildruns FastBoot and generates static HTML.
- The HTML can be uploaded to a static hosting such as the free GitHub pages.
- When the user opens the app, they immediately see it prerendered. All links behave like web 1.0 links.
- When the JS bundle loads, Ember starts and "hydrates" the HTML, turning the page into a single page application.
The above is possible thanks to this amazing addon: robwebdev/ember-cli-staticboot.
The best part is that
ember-cli-staticboot fills in for URL rewriting which GitHub Pages doesn't support. We no longer need to switch
location mode to
hash when using GitHub Pages.
Make sure to try this page with JS disabled!
For quite a while, I've been claiming to be capable of BDD. But the truth is that in all my commercial projects I had to cut corners for various reasons, such as:
- The business pressure was too tough.
- The management didn't see a benefit in spending time on tests.
- The app evolution was so rapid and erratic that rewriting tests to keep them up to date would've eaten ~90% of time.
With this assignment, I finally leveraged the BDD workflow from start to finish. And I must say, it's been a great pleasure! The level of confidence BDD provides is unprecedented. Not only coding becomes stress-free, but also refactoring is no longer risky, so there's no reason to avoid it!
Here's a list of technologies and approaches that I've used:
- The Chai assertion library with its
- Mocha test suite. When I started working with Ember, ember-cli-chai wasn't available, and QUnit's assertion library was very scanty. My first attempt to resolve this was my ember-cli-custom-assertions-collection, but then I made a decision to switch to Mocha. I had to implement a custom test runner to obtain the "no try/catch" feature.
- Mirage. I didn't bother implementing a proper factory/model layer with Mirage and used simple fixtures instead. But rest assured, I have good experience with Mirage. For example, it's me who implemented the support for JSONAPI
includedfeature in Mirage.
- The page object pattern via ember-cli-page-object makes tests so much more readable! I also use my technique to make page objects more concise and at the same time more powerful.
- The async/await syntax is another thing that substantially improves test readability, which I care a lot about. It's a known wisdom that reading code is more difficult than writing it, but reading tests is even more difficult. An unreadable test is a dead test.
- Finally, I use my own code style for tests where I pull the assertion message above the assertion expression.
Here are links to some tests for you to check out:
- Acceptance test: city route and the corresponding page object.
- Integration test of a component with an action: cities-chooser component, page object.
- Different styles of unit tests:
- Model hook of the city route.
- Helper: round
- A computed property on the user-city-junction model.
I ran short on time and didn't test some extra features that weren't part of the assignment, e. g. the map.
BTW, ember-leaflet is an awesome addon, but it doesn't provide any testing abstractions. :( I'm not even sure how interactive maps are supposed to be tested. 😕 I can only think of a very low-level approach with a lot of boilerplate code. Implementing a clean set of high-level testing abstractions would be an amazing open source contribution.
I use my own methodology to organize the Sass codebase, so that it is infinitely scalable and prevents leaks. It's a simplified version of BEM with some radical, yet well-thought restrictions.
I gave a talk on it in Russian on the MoscowJS meetup.
Please check out my Sass code style: app/pods/city/style.sass.
Responsive Web Design
This page is responsive and realigns nicely on any viewport size. Try it out!
For RWD in Ember, I typically use my ember-element-query addon. It lets you define responsive CSS rules based on element width, not page width. This lets you implement reusable responsive components, with their responsiveness being agnostic to where you put it in your page layout. Also, RWD styles become so much more concise!
Unfortunately, the element query technique requires JS. It could be worked around in FastBoot if browsers reported viewport width in their HTTP requests, but they don't.
So I had to fallback to media queries which I implement using my know-how: the magnificent breakpoint-slicer. By the way, has whopping 283 stars on GitHub! </irony>
What else could have been done
I had to draw the line somewhere, leaving some features behind. I wish I had more time to implement these:
- Sorting the customers table by clicking table headers.
- Using the FastBoot Shoebox to pass data from the build into Ember Data, so that it doesn't have to redownload the data. I have implemented this for my personal website with a custom implementation of the ember-data-fastboot approach.
- Due to FastBoot, I had to use a JS-free implementation of the "expand the assignment" checkbox. When Ember boots, it rerenders the HTML, and the checkbox state resets. I didn't resolve this issue here, but on my personal website, I capture the state of all scrollbars and CSS-driven checkboxes and reapply it after the initial rerender. Hopefully, FastBoot eventually becomes capable of reusing the prerendered HTML and this trick becomes unnecessary.
- Authentication. It would be nice to let you edit cities and customers. It's definitely too much for a test assignment, but on my website I've implemented authentication so that you can star my Ember addons via the GitHub API. Should I implement batch starring as well? :)
- Accessibility and i18n are two features that are a must for any commercial project.
For the technologies used in the codebase, see package.json.
Here are noteworthy non-codebase tools: