Skip to content

msorens/images-from-the-aether

Repository files navigation

Images From the Aether

1. Purpose

"Images from the Aether" is a cloud image viewer web application. What, another one? There are plenty of image viewers out there, of course. And, in fact, I did not sit down to create one. What I wanted to do was create a modern web app from scratch using best design principles and practices as a showcase piece for my portfolio. And this seemed like a project that was small enough to make it the amount of code easy to digest for a reader, yet still require quite a bit of different patterns, interfaces, and libraries to make it interesting.

So, yes, this is a real, working web app; not a toy or demo.

These are the high-level requirements I began with:

  • Display a photo collection keyed off a user-specified string.
  • Have a search bar that responds as the user types.
  • Display results in a responsive grid.
  • Use infinite scroll.
  • Open an enlargement of a photo (and include the author's name) by clicking on one in the results.
  • Image should be downloadable from that detail modal.
  • Application should be accessible, responsive, and have strong UX-fu.

That leaves a large number of details unspecified, of course. What I wanted to do was a clean TDD design so that, at the end, the unit tests itemize low-level requirements, providing a contract of the application's behavior -- guaranteed to be an accurate list of requirements as long as the tests pass!

My end result was a thorough but not quite complete list. There are some things not really amenable to unit tests: accessibility, performance, responsiveness of the display, visual positioning (where a button or field is positioned on the page, alignment of elements, etc.), and probably a few others.

2. Using The Application

You can certainly clone the repository and play with this application on your own machine, or you can just launch it immediately in your browser at https://msorens.github.io/images-from-the-aether/, courtesy of GitHub Pages. Be advised, however, that you will need a Pexels API key to do anything (see API Access below for further details).

3. The Design And The Result

What is the quickest way for you, the reader, to understand what this application does, down to every nitty gritty detail? Sure you could read the code but it is quite a leap from that to really knowing what the app does. Or you could run the code--but you really would not know how to exercise everything that the app does. Better than either of those: come with me as I describe the behavior to you -- through unit tests (and pictures).

3.1. Overview

There are just three components...

  • The AppComponent (the main component) manages a key for the API Calls, as well as the user input.
  • The ViewPhotosComponent houses the collection of photos in an infinite scroll region. It provides several visual cues (no search started, no results found, end of results reached, fetch in progress), along with providing a modal to show details of a selected image.
  • The BaseModalComponent is a generic modal component that can be customized by providing content. It is used both to display image details (larger image, author, download link) and for prompting the user for an initial API key.

...and two services:

  • ImageService manages the API calls to test the API key and to load batches of photos.
  • KeyService manages storing and retrieving the API key in your browser's local storage.

Next let us walk through the tests.

3.2. Starting Out

To begin, I of course want the application to launch, and it should display the name of the application and a search field for the user to interact with.

Section: AppComponent >> display
• should create the app
• shows a search box
• shows the application name

3.3. User Input

I want to let the user type anything in the search box--except that leading and/or trailing whitespace should be ignored. But that means I need to also make sure that whitespace elsewhere is still preserved, and other special characters (e.g. punctuation) are also preserved. This set of tests covers all those:

Section: AppComponent >> user input >> normalizing input
• leading and trailing whitespace in user input ignored
• leading whitespace in user input ignored
• trailing whitespace in user input ignored
• user input used verbatim when no whitespace present
• intermediate whitespace in user input is preserved
• special characters used verbatim

3.4. Acting On User Input

As the user types I want to respond briskly, to fetch matching photos. So I want to react to every keystroke, yes, but only when the user pauses, say after a "debounce period" of 400 milliseconds.

Section: AppComponent >> user input >> react to keystrokes
• images are fetched on every keystroke (after debounce period)
• images are NOT fetched if debounce period has not expired
• images are NOT fetched when input is empty

3.5. Fetching Results

Once that debounce period has elapsed, we call the API to fetch the first batch of photos. Here is a typical screen showing the input and output so far. Each image is annotated with an index number to give you a better sense of where you are in the results as you scroll. Only a small number of images are fetched at a time; as you near the bottom of the scroll region, another batch is fetched.

nominal results

Subsequent batches are fetched automatically as the user scrolls to the end of the previous batch in the virtual scroll container. This occurs by observing the events that are emitted as the user scrolls. Only when an event corresponds to the last photo of the previous batch do we fetch more. Which means there are a variety of times that we do not fetch more. Here are the tests:

Section: ViewPhotosComponent >> with mock API >> fetching more photos during scrolling
• more photos are fetched when final photo is encountered
• more photos are NOT fetched when any non-final photo is encountered
• more photos are NOT fetched when initializing event triggers
• more photos are NOT fetched when end of input reached
• more photos are NOT fetched while still loading previous batch

When the user has scrolled to the bottom and a request for more photos is initiated, the next batch will arrive after some short delay based on the user's internet connection speed, machine load, etc. That interval may be imperceptibly short... or it may not. While loading, then, it is helpful to have a visual cue -- a spinner -- to let the user know loading is in progress. Also, once the final batch of photos arrives, a visual cue indicating the end has been reached is handy. This set of test verifies both those visual cues:

Section: ViewPhotosComponent >> with mock API >> status indicators
• displays end marker when collection exhausted (logic)
• displays end marker when collection exhausted (rendering)
• displays spinner while fetching photos

Here is a close up of the end marker (in the same location as the spinner).

end results

With a collection of results to view, one can select any image to open a detail view, showing an enlargement of the selected photo along with some details of the photograph, as detailed by these tests:

Section: ViewPhotosComponent >> with modal component >> detail view
• signals modal to open when user selects an image
• renders larger image of selected photo
• author name
  • enclosed with link to author details
  • renders with detail view
  • author link will open in a new tab (or window)
• save button
  • downloads image file URL when clicked
  • renders with detail view
  • displays download icon from material icons
• save button tooltip displays filename
  • for domain + only file + extension
  • for domain + only file + NO extension
  • for domain + path + file
  • for domain + path + file with spaces
  • exception: handles empty path gracefully with empty string
  • exception: handles empty url gracefully with empty string
  • exception: handles invalid url gracefully with empty string

A typical representation:

detail view

Back to the main screen, for a moment. When the user enters some search string only the first batch of matches is fetched, as described earlier. But the back end already knows how many total matches are available, so that information is passed along, too, and it is helpful to provide it to the user. The total available count then is posted in the upper right of the screen. While we could display it all the time, it provides no value at certain times (at initial program load, while new data is being fetched, etc.). These tests cover the various scenarios:

Section: AppComponent >> display >> reporting of total available images
• shows no total matches while loading
• shows total available NON-ZERO Matches AFTER a search
• shows total with zero matches on HTTP error
• shows total available ZERO Matches AFTER a search
• shows no total matches BEFORE a search

3.6. Variations On A Result

Seeing a collection of photos is ultimately what a user is after when using this application. But there are several other things the user might see depending on circumstances.

(1) When first opening up the app, nothing has been searched for so clearly there can be no results to display. Instead, the user will see the no-search-started graphic, a cute-ish image that conveys the "start here" message.

(2) While the available collection of images is vast it is not limitless so it is possible that no images are found to match what the user types. In that event, the no-results graphic appears.

(3) In order to access the photo collection, one has to have a valid access key (discussed in the next section). If the key is not valid and one asks for some images, the unauthorized graphic appears.

(4) Inevitably there are other assorted error circumstances, including no internet connection, server limit exceeded, etc. For any of these unexpected errors a general-error graphic appears.

Here are thumbnails of those four displays:

composite view

The set of unit tests below confirm each of those images appears at the correct time, only one ever occurs at a time, and none appear while loading is ongoing.

Section: ViewPhotosComponent >> with spy API >> result graphics
• displays no-search-started graphic with no input
• displays no-results graphic with some input but no results
• displays unauthorized graphic for authorization failure
• displays note for authorization failure
• displays general-error graphic for NON-authorization failure
• displays error code and message for NON-authorization failure
• displays general-error graphic for empty response
• displays "empty response" for empty response
• does not display any graphic while loading

3.7. API Access

As mentioned in the last section, the application requires an access key to obtain results from the Pexels API. While I could have hard-wired a single access key that would be used by everyone who downloaded the app, I thought it would yield a more interesting design by thinking about that API key as credentials for the application itself and, as such, it would not be overly burdensome to require each user to obtain and register their own key with the app. Thus, upon startup the first thing that occurs is a check for a stored key; if not found, it pops up a modal asking the user to supply one.

initial no key present

That same modal may be re-opened at any time by selecting the key icon in the upper right corner of the display.

These unit tests elaborate on the opening behavior of the modal for the API key:

Section: AppComponent >> API key
• at startup, retrieves the previously saved user-entered key from browser local storage
• stores the user-entered key in browser local storage
• key-entering modal opening
  • opens automatically when no key found stored
  • does NOT open automatically when key is found stored
• button to re-open key modal
  • has label with key icon from material icons
  • includes tooltip
  • renders on main window
  • opens the key-entering modal manually when clicked

That deceptively simple modal has a number of behaviors to provide a useful user experience. The illustration (my naive attempt at creating a kineograph) and the unit tests following provide the complete story.

composite key modal

Section: AppComponent >> key modal
• has an input field to enter the key
• has buttons to save and to test the key
• buttons are initially disabled
• buttons reacts to presence of input
• test button has status indicators that are initially hidden
• test button reveals only spinner when API operation is in progress
• test button reveals only success indicator when API reports success
• test button reveals only failure indicator when API reports failure
• test button is disabled while test operation is in progress
• test button is NOT disabled when test operation completes

3.8. The Base Modal Component

Both the modal for the API key and the modal for the detail image view are using the same component, the BaseModalComponent. This component opens a dialog that appears to be on top of the main window, greys out the contents of that main window, and only allows actions within the dialog.

The inherent behaviors of this modal component available to any consumer component are covered by these tests:

Section: BaseModalComponent
• modal opens on demand
• modal can be closed on demand
• modal closes itself upon pressing "escape"
• modal does NOT close upon pressing keys other than "escape"
• modal closes itself when clicking the background
• modal does NOT close when clicking deeper inside the modal body
• modal does NOT close when clicking the modal body

3.9. State Management

I am using @ngxs/store for the first time with this project, having previously been an avid @ngrx/store user. Both of these provide a powerful state management system based on Redux for use in Angular applications.

I came across ngxs recently in my search for a way to do reduce the boilerplate needed by ngrx. You can compare and contrast the two systems on npmcompare.com and npmtrends.com. They both work on the same principles: you dispatch an action, that action causes side effects, the store gets updated from those side effects, and ultimately subscribers watching specific portions of the store render responses to those state changes in the UI.

There are two actions used by this simple application:

SetSearchString is called every time the user modifies the search string at the top of the application window (after the debounce period, discussed earlier). It clears out the prior results, if any, and then stores the new string in the state. The ViewPhotosComponent watches for changes to that particular piece of the state. When it sees a new value, it invokes the next action, FetchPhotos.

Store actions >> SetSearchString action
• resets page counter to zero
• puts search string into state
• resets photos to empty list
• resets total to zero

FetchPhotos is invoked by either the presence of a new search string initiated by SetSearchString or by the user scrolling to the last photo of the prior results. Either way, it liaises with the ImageService to fetch more data from the Pexels API. Once that new data is received, it determines whether it is the final batch or not (to set the end flag), and it adds a relative index number to each received photo so that can be displayed along with the photo in the UI.

Store actions >> FetchPhotos action
• end-of-input
  • end of input reflects in state with value 'true'
  • end of input reflects in state with value 'false'
• annotates photos with sequence number
• stores new photos in state
• stores total in state
• increments page number with each subsequent fetch
Store actions >> FetchPhotos action (error return)
• resets photos to empty list upon error
• resets total to zero upon error

4. More On Managing Search Results

The heart of this application is managing a potentially huge collection of images. Searching for cat, for example, yields 8000 results. The idea is to only fetch a small batch at a time, and as the user scrolls down, another batch is fetched, then another, etc. The user can thus scroll through those 8000 images seamlessly. This is the notion of infinite scrolling.

But if all those images were kept in memory, batch after batch, the browser would start to bog down and exhibit poor performance. So the app then needs virtual scrolling as well. With a virtual scrolling container, the app renders just the very small number of items that happen to be on screen at a given moment (plus a few above and below that to allow for smooth transitions).

There are numerous scrolling libraries available that support either or both of those -- but mostly for a simple, one-dimensional list. For this app, we need a two-dimensional grid of images that can be used with virtual infinite scrolling. Because the grid has multiple tiles across the page, it is important for the app to also support responsive design, meaning it adjusts the layout to accommodate whatever screen size and resolution the user is using. Practically speaking, it should adjust the number of images across the page depending on the screen width. And that is not just at page load time--if the user changes the window size, the layout should react accordingly.

With those aspirations in mind, I went looking for an off-the-shelf component:

  • Angular's own <cdk-virtual-scroll-viewport>, detailed in this great article from Zoaib Khan. Main problem with this approach: supports list only, not a grid.

  • ngx-infinite-scroll directive, detailed here by Christian Nwamba. Main problem: infinite but not virtual scroll, so it keeps adding to the DOM.

  • <virtual-scroller> (ngx-virtual-scroller) from Rinto Jose. Main problem: not responsive to window size changes.

  • <od-virtualscroll> from Onur DoÄźangönĂĽl. Main problem: difficulty getting it running on my machine

During my search for a good solution I had some useful conversations with some of the folks who have worked on this problem. A special kudos in particular to Zoaib Khan, who critiqued and tweaked code to help me get things going. Khan provided yet further assistance from his article, Fastest way to make a responsive card grid with CSS, revealing an elegant CSS technique to add responsiveness to existing layout!

I finally settled on using the <virtual-scroller> component (incorporating with Khan's tweaks for responsiveness). This particular component had the added benefit of being able to handle non-uniform sizing of images--not every image in the grid has to be the same dimensions!

5. Accessibility

Even in a small application such as this, there are a number of required items to conform to WCAG accessibility requirements.

  • Associate a <label> element with each input field. Note that every input field should have a corresponding <label> -- even if the label is not visible on-screen, so screen readers can properly make sense of things. That is done by a handy bit of CSS I found for the task.

  • Add an alt attribute to each image.

  • Add context landmarks (header, main).

  • Define tab-navigable elements with tabindex attributes. So, while it may be more convenient to use a mouse to scroll through the "infinite" scroll region, it is also possible to do so with the keyboard using successive presses of the tab key to accommodate those users unable to use a mouse.

  • Set appropriate color contrast. In this case, when I initially added sequence numbers to each image, they were just white characters on whatever background the image proffered. But that made some of them harder to discern, so I added a small background shading around the character with sufficient contrast to make them always readable.

By the way, I use ANDI (recommended by the GSA's own Section508.gov) as well as the very useful axe dev tools to identify such accessibility issues.

6. User Experience

First, one UX touch point in detail, to illustrate that some seemingly trivial things are... not so trivial to implement. That is followed by a list of other UX highlights.

6.1. Input Validation In Detail

Input fields both have validation and provide validation feedback to the user. There are only two simple input fields in this application, one for the search string and one for the API key. It is sufficient that, if they are non-empty, they should be considered valid. And they should only be acted upon if they are valid. As an example, the buttons acting upon the API key are disabled until the input is valid, which is to say, non-empty. But even more than that, the input fields themselves provide visual feedback on the valid/invalid state of the input. In this kineograph, notice that the marker appears and turns green when input is valid, but turns red when not. Note that you can use a required attribute to do all the work of checking empty or non-empty. But that is not the best UX! In the last frame you see that if the user has typed only whitespace, that is invalid, too. Just takes a bit more handling to cover that case.

input validation feedback

To make those red/green bars I adapted a clever bit of CSS from Angular's own development guide, which you can see in app.component.css. One useful change I made is that the field is not marked red until the user actually touches the field; good UX dictates that one should not signal "hey, this is wrong!" when the user just arrives on the page and has not done anything yet.

6.2. Other UX Touch Points

Here are some of the UX considerations applied to this application to provide the best possible UX. (Some of these have already been mentioned throughout this narrative.)

  • Clear visual imagery for the non-happy path, which is to say any result other than a batch of images returned (see "Variations on a Result").

  • Buttons are labeled with well-known icons rather than words for both brevity and universality.

  • A spinner pops up during various operations to indicate the application is doing something.

  • A clear end marker pops up when all images for a given search have been retrieved.

  • Images have sequence numbers visible so the user can maintain a sense of where they are when scrolling back and forth.

  • A total number of matches for the given search string is reported immediately, giving the user a sense of how much there is to scroll through.

  • Tooltips are used whenever there is additional helpful information to share with the user.

  • The API key modal test button shows both a spinner while testing and then positive or negative feedback right on the button when a result is determined.

6.3. Developer Experience, Too

UX is not just for users--developer experience (DX) is important, too! I strive to make code welcoming in a sense, so people who want to either read the code or just understand how the application works, can easily dig in.

Some examples:

  • A thorough read-me file is supplied ( this one :-).

  • A table of contents is included (easy to maintain with the "markdown TOC" extension for VS Code).

  • The documentation includes appropriate illustrations rather than trying to describe with lots and lots of words.

  • Where do things come from? Borrowed or adapted code snippets include source references (search for "from http" or "see http" in the code base to see examples).

  • Comments explain where other types of things come from. For instance, I derived the bits in Photo.ts from some data conversions. But rather than just provide the bits, I included comments in that file explaining where they came from.

  • More on comments: some comments are vital, as in the previous bullet. But care must be taken to use comments appropriately and only when necessary, because too many comments can often be worse than not enough. I wrote at length about this in Fighting Evil in Your Code: Comments on Comments

  • CI checks help minimize the developer burden, automatically running checks on different browsers, and even a markdownlint check, too.

7. Performance

Firstly (as already discussed) displaying a large, growing collection of objects can quickly bog down the UI if implemented naively. The virtual scrolling component largely takes care of this, keeping what is actually being rendered to a very small, essentially constant number of items--just what is in the current viewport. That keeps the UI feeling very brisk and peppy to the user.

Secondly, the fact that this large collection of objects is a set of images is a concern. Image files are much larger than text files, of course, so having to download a lot of them as the user scrolls back and forth might introduce noticeable lag times. Caching of these images is thus vital. But the way I am using images in the app makes this issue moot: the browser is in control of fetching the images as needed (through what is rendered in the HTML template) and, by default, browsers cache those image fetches. The data I am manually fetching with each new search or each new batch is just image metadata (author, URL, etc.), which is relatively quite small so not a performance concern.

8. Further Points On Maintainable Design

8.1. Clean Unit Tests

I wanted the unit tests to be "short and sweet" and follow the useful Arrange-Act-Assert pattern; ideally just a couple lines for each. As many of my tests were peeking at the DOM, I crafted a set of helpers in queryHelper.ts to facilitate this.

In this example, you can see how the findAs and findAllAs generic helper functions make the test almost trivial. I first do a sanity check that the input field is empty. The standard DOM query returns a generic HTMLElement, which does not have a value property. So instead of just doing a find I use findAs to cast it to an HTMLInputElement in order to access its value. I then do a findAllAs to find a set of buttons and confirm they are all disabled (which similarly needs each object to be an HTMLButtonElement).

    it('buttons are initially disabled', () => {
      fixture.detectChanges();
      expect(findAs<HTMLInputElement>('.control-bar input').value).toBe('');

      findAllAs<HTMLButtonElement>('.control-bar button')
        .forEach(button => {
          expect(button.disabled).toBeTrue();
        });
    })

8.2. Entities Are Treated As Entities, Not Strings

Often when writing a program we need to deal with a thing that has a well-defined structure: a bit of HTML or XML, a URL, an entry in a log file, etc. Since each of those is typically supplied as a string it is only natural that the developer treats them as strings. But that can lead to what I call the "subtle syntax catastrophe" which I talk about in my article Code Smells: Raw Strings and the Subtle Syntax Catastrophe. For the case at hand, I had a need to extract the file name out of a URL. It is fairly straightforward to find the piece of string between the first virgule and question mark, and know that that is the path to the file, so the name of the file is the last piece on that path. But that is dealing with the URL as a raw string, and there is no benefit to that. Rather, treat it as a URL: create a first-class URL object and then just access the pathname property to get that file path. You can see this in action in view-photos.component.ts::getFileName().

8.3. Unit Tests Are Phantom-Aware

Perhaps there is a industry term for this, but I have coined the phrase "phantom tests" in my article The Phantom Menace in Unit Testing. A phantom test is one that proves nothing because if the code under test did nothing, the result would be the same. I give a mention to this in base-modal.component.spec.ts.

8.4. CSS Constants

Native CSS does not support reusable variables or constants: if you want to reuse a number in multiple places, you just repeat that hard-coded value over and over and... Fortunately, SASS does support constants (except they call them variables ;-). The few global values I needed are recorded only once in _variables.scss and those named constants can then be used freely without any maintenance burden.

8.5. On File Downloads

Such a simple notion; such a challenge to do in an in-browser application!

In the early days, in-browser code could not interact with the filesystem for security reasons. With the advent of HTML 5, however, a simple mechanism was introduced to allow downloading a file, using the download attribute in an anchor element:

    <a href='TARGET_URL' download='image.jpg'>Download</a>

Initially that worked, but then--again for security reasons--the download attribute became ignored for cross-origin URLs (reference: https://developers.google.com/web/updates/2018/02/chrome-65-deprecations#block_cross-origin_a_download). Patterns emerged to download the file in question asynchronously, then feed it to an anchor element so as not to violate the CORS constraint. One way is using the built-in fetch method (from https://dev.to/sbodi10/download-images-using-javascript-51a9):

  const image = await fetch(remoteUrl)
  const imageBlog = await image.blob()
  const imageURL = URL.createObjectURL(imageBlog)
  // ... pass it to a generated anchor element

Alternately, the axios library provides a slightly more convenient way to retrieve the remote data (from https://www.delftstack.com/howto/javascript/javascript-download/#use-axios-library-to-download-files):

  axios({
        url: remoteUrl,
        method: 'GET',
        responseType: 'blob'
  })
  .then((response) => {
    // ... pass it to a generated anchor element
  }

Either way, after retrieving the data, it would be fed to an anchor element and one would then manually trigger the link by invoking its click method. That one has to trigger the browser to do a file download via the DOM rather than writing code to do it directly seemed rather peculiar to me. Looking further it turns out that a new native file system API was very recently developed--but alas, at the time of writing (September 2021) it is so new that it is barely supported (references: Can I Use data at https://caniuse.com/native-filesystem-api and Draft Report on File System Access dated August 2021 at https://wicg.github.io/file-system-access/).

As all the workarounds seemed somewhat kludgy I opted to just use the file-saver library, which could do the whole operation in a single line of code ( see view-photos.component.ts::download() ).

8.6. More On File Downloads

Yes, there is still more to say on this topic. My initial thought was, like a desktop application, when the user selects to download something, they are then presented with a file picker dialog, allowing specifying the directory path and the file name through OS-specific design standards. It turns out that displaying such a file picker though is again a large challenge; the self-same native file system API mentioned above will eventually allow that. Today, it is so much simpler to just let the browser "do its thing": when you select something to download it puts it in the "downloads" folder. If a file of the same name already exists, the browser intelligently starts adding sequence numbers. On a Mac, for instance, Chrome saves "image.jpg", "image (1).jpg", "image (2).jpg", etc., whereas Firefox does the same thing sans the space character: "image.jpg", "image(1).jpg", "image(2).jpg", etc.

8.7. Beyond Breadth: Unit Tests Have Depth

Code coverage is a great metric for unit tests. One should always strive for a high percentage; just what that percentage should be (80, 90, 100??) is open for debate, but picking any reasonably high number is better than not. However, that metric only covers breadth of tests. Just as important is the aspect of depth--that is all about the two-pronged approach of boundary-value analysis and equivalence class partitioning. I talk about those in detail in the Test Quality section of my article Go Unit Tests: Tips from the Trenches. As mentioned there, data-driven tests provide a good way to achieve depth. One example of that approach to deep tests is in the "normalizing input" section in app.component.spec.ts. But test depth can also come from a series of related tests covering the same topic. If you look above at the set of tests under the heading "fetching more photos during scrolling" you will find one test that checks for when more photos are fetched, plus several related tests where more photos are not fetched. You can peruse all the depth coverage tests in this test suite by searching for "(DEPTH COVERAGE)".

8.8. Strict Mode And Other Checks

I am an ardent believer in using safety nets whenever possible--let the machine do the work of checking rote things! First and foremost, the TypeScript compiler offers a strict mode (in tsconfig.json) which is a composite of several important checks. I am firmly of the opinion that one should never build a project without it. The cost of finding errors grows exponentially as a project develops so if one can flip a switch to catch errors earlier, that is a definite win!

I further rely on whatever linters I can find: one for TypeScript, of course, but there is even a linter for markdown files (such as the one you are reading) called Markdown Lint, which helped me construct this file properly.

I use a spell-checker for my code, too (Code Spell Checker). That not only helps prevent embarrassing typos being shown to all your users, but it also occasionally resolves code bugs--there are almost always bits of an application that rely on data names that the compiler just cannot validate a priori, and if you mistype one you have a problem.

8.9. TypeScript Type Traps

To say that TypeScript boosts JavaScript development to great heights cannot be overstated. But strong-typing is not a panacea; it is a tool that must be used with care. Sometimes it can even fool you: my canonical example is that if you use type assertions incorrectly you actually get weaker code! See my slide deck TypeScript Type Traps where I explain that and a few other vagaries of the language.

8.10. Unit Tests Test Behavior Rather Than Implementation

Testing behavior in unit tests is much more robust and much more interesting than testing implementation. It is more robust because testing behavior allows you to change the implementation if desired and still have all the tests pass without having to rewrite any. And it is much more interesting because behavior-oriented tests describe to any interested code readers something tangible, some actual behavior that the code performs, as opposed to, say, showing that some internal function with inputs x and y returns z. Exclusively focusing on behavior-oriented tests allows me to do much more with unit tests than is traditionally the norm; it can even supplant a portion of end-to-end testing. in fact, for this application, I decided not to invest in end-to-end tests because my unit tests are sufficiently rich and robust. (On the other hand, end-to-end tests alone would not be sufficient, because they do not have the resolution to exercise all the nuances that unit tests do.)

8.11. Test Environments And Continuous Integration

A generated Angular skeleton project by default sets up the Karma test runner to run tests on Chrome. Theoretically it should not matter which browser is targeted when running unit tests: every browser should conform to the specs for JavaScript, the DOM, etc. But in the real world differences crop up either when (a) you least expect them or (b) they can be the most insidious. So it is extremely useful to flag such differences as early as possible. As such, I have provided support for the triumvirate of browsers at the top of the food chain: Chrome, Firefox and Edge. Running tests on each browser is as simple as npm run test:chrome, npm run test:firefox, or npm run test:edge. Note that you have to already have the target browser installed on your system. You can see the recipes for these commands in the scripts section of package.json.

I am fairly fastidious when it comes to running unit tests continuously while I am writing code. But I will admit I find it burdensome to remember to run the suite on different browsers. Invoking that best practice of being a lazy programmer (see for example lazyprogrammer), I set up a continuous integration (CI) environment using GitHub Actions to automatically run tests against all three browsers for every pull request. Thus, even if I forget to do it, the machine will do it for me. A powerful feature of GitHub Actions is that you can easily specify whatever target operating system you need, so while I want my default environment to be Linux, it was simpler to run the tests for Edge on a Windows server. The recipes for these CI checks are in actions.yaml.

9. Acknowledgements

As the expression goes, none of us is an island. I would not have the acumen and skills of my craft honed as sharply today without having worked beside a number of keen and passionate individuals including (in alphabetical order): Erin Carver, Jamie Degnan, Jay Mundrawala, Joe Dumoulin, John Knuth, Kaleb Pederson, Kathie Drake, Kimberly Garmoe, Lance Finfrock, Larry Kippenham, Maggie Walker, Mary Jinglewski, Matt Peck, May Pinyo, Nezar Hussein, Ofira Varga, Rick Kurtz, Salim Afiune, Scott McCabe, Sergei Knezdov, Shadae Holmes, Stephan Renatus, Steven Danna, Susan Evans, Tara Black, Tina Saunders

(Apologies to others I am undoubtedly forgetting.)

10. Default Generated Docs For This Project

This project was generated with Angular CLI version 11.2.6.

10.1. Development Server

Run ng serve for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.

10.2. Code Scaffolding

Run ng generate component component-name to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module.

10.3. Build

Run ng build to build the project. The build artifacts will be stored in the dist/ directory. Use the --prod flag for a production build.

10.4. Running Unit Tests

Run ng test to execute the unit tests via Karma.

10.5. Running End-to-end Tests

Run ng e2e to execute the end-to-end tests via Protractor.

10.6. Further Help

To get more help on the Angular CLI use ng help or go check out the Angular CLI Overview and Command Reference page.

About

This cloud image viewer is a mini-showcase for my engineering skills

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published