Test
assignment
for
Intercom

built with ❤
by Andrey
Mikhaylov
(lolmaus)

Dublin 53.3393, -6.2576841
Distance limit: 100 km

Lucky customers (16 / 32):

Name
ID: ID
Latitude: Latitude
Longitude: Longitude
Distance
Alice Cahill
ID: 1
Latitude: 51.92893
Longitude: -10.27699
Distance: 313.25 km
Ian McArdle
ID: 2
Latitude: 51.8856167
Longitude: -10.4240951
Distance: 324.37 km
Jack Enright
ID: 3
Latitude: 52.3191841
Longitude: -8.5072391
Distance: 188.95 km
Ian Kehoe
ID: 4
Latitude: 53.2451022
Longitude: -6.238335
Distance: 10.55 km
Nora Dempsey
ID: 5
Latitude: 53.1302756
Longitude: -6.2397222
Distance: 23.27 km
Theresa Enright
ID: 6
Latitude: 53.1229599
Longitude: -6.2705202
Distance: 24.07 km
Frank Kehoe
ID: 7
Latitude: 53.4692815
Longitude: -9.436036
Distance: 211.17 km
Eoin Ahearn
ID: 8
Latitude: 54.0894797
Longitude: -6.18671
Distance: 83.55 km
Jack Dempsey
ID: 9
Latitude: 52.2559432
Longitude: -7.1048927
Distance: 133.25 km
Georgina Gallagher
ID: 10
Latitude: 52.240382
Longitude: -6.972413
Distance: 131.3 km
Richard Finnegan
ID: 11
Latitude: 53.008769
Longitude: -6.1056711
Distance: 38.12 km
Christina McArdle
ID: 12
Latitude: 52.986375
Longitude: -6.043701
Distance: 41.76 km
Olive Ahearn
ID: 13
Latitude: 53
Longitude: -7
Distance: 62.22 km
Helen Cahill
ID: 14
Latitude: 51.999447
Longitude: -9.742744
Distance: 278.2 km
Michael Ahearn
ID: 15
Latitude: 52.966
Longitude: -6.463
Distance: 43.71 km
Ian Larkin
ID: 16
Latitude: 52.366037
Longitude: -8.179118
Distance: 168.39 km
Patricia Cahill
ID: 17
Latitude: 54.180238
Longitude: -5.920898
Distance: 96.09 km
Bob Larkin
ID: 18
Latitude: 52.228056
Longitude: -7.915833
Distance: 166.44 km
Enid Cahill
ID: 19
Latitude: 55.033
Longitude: -8.112
Distance: 223.65 km
Enid Enright
ID: 20
Latitude: 53.521111
Longitude: -9.831111
Distance: 237.58 km
David Ahearn
ID: 21
Latitude: 51.802
Longitude: -9.442
Distance: 274.79 km
Charlie McArdle
ID: 22
Latitude: 54.374208
Longitude: -8.371639
Distance: 180.16 km
Eoin Gallagher
ID: 23
Latitude: 54.080556
Longitude: -6.361944
Distance: 82.71 km
Rose Enright
ID: 24
Latitude: 54.133333
Longitude: -6.433333
Distance: 89.04 km
David Behan
ID: 25
Latitude: 52.833502
Longitude: -8.522366
Distance: 161.36 km
Stephen McArdle
ID: 26
Latitude: 53.038056
Longitude: -7.653889
Distance: 98.87 km
Enid Gallagher
ID: 27
Latitude: 54.1225
Longitude: -8.143333
Distance: 151.55 km
Charlie Halligan
ID: 28
Latitude: 53.807778
Longitude: -7.714444
Distance: 109.38 km
Oliver Ahearn
ID: 29
Latitude: 53.74452
Longitude: -7.11167
Distance: 72.21 km
Nick Enright
ID: 30
Latitude: 53.761389
Longitude: -7.2875
Distance: 82.65 km
Alan Behan
ID: 31
Latitude: 53.1489345
Longitude: -6.8422408
Distance: 44.28 km
Lisa Ahearn
ID: 39
Latitude: 53.0033946
Longitude: -6.3877505
Distance: 38.34 km
No customers are this close to Dublin. Don't drink alone!

The assignment

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!

Cover note

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!

Code style

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 user model. 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

Customers and cities aren't hardcoded but rather fetched from gist.github.com via the rawgit.com CDN. The CDN step is necessary to work around GitHub's strict CORS policy.

The customers.json file provided with the assignment isn't a valid JSON document, so I had to implement a custom serializer to parse it.

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!

FastBoot! 😎

This app is a backendless FastBoot instance. "Backendless FastBoot" sounds contradictory, but it's quite reasonable when you think about it:

  1. ember build runs FastBoot and generates static HTML.
  2. The HTML can be uploaded to a static hosting such as the free GitHub pages.
  3. When the user opens the app, they immediately see it prerendered. All links behave like web 1.0 links.
  4. 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!

Testing

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!

The BDD cycle

Here's a list of technologies and approaches that I've used:

  • The Chai assertion library with its expect DSL.
  • 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 included feature 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:

Please try running tests online and view the coverage report!

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.

Styling codebase

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.

Tools used

For the technologies used in the codebase, see package.json.

Here are noteworthy non-codebase tools:

Drop me a line!

[email protected]

Fork me on GitHub