Help, I Crashed My Plane! is a website that assists pilots in navigating 49 CFR § 830 (a.k.a. NTSB 830), the regulations governing reporting incidents and accidents to the NTSB. Using those laws as a basis, it asks a series of questions to determine if the incident qualifies as a serious incident or accident, and then gives the user the appropriate reporting instructions they need to follow.
This website is written in TypeScript using Vue.js. State management is done with Vuex and localization is done using i18n-vue (though American English is currently the only supported locale).
Vue CLI is the toolchain used to develop this website. Most operations are
powered by vue-cli-service
via Yarn. The package.json
file has yarn
aliases for all common development and deployment tasks.
To run a copy of this website, simply check out this project into a directory,
then run yarn install
in that directory.
yarn serve
will compile the source with Webpack, and run a local development
server at http://localhost:8080. The development web server supports
hot-reloading.
This website has both unit tests (written using Mocha/Chai) and end-to-end tests
(run using Cypress). To run unit tests, run yarn test:unit
. Run
yarn test:e2e
to launch the Cypress test runner and run end-to-end tests.
HTML documentation for the code can be generated by running
yarn docs:generate
. Documentation will be in the docs
directory.
This website is hosted using GitHub Pages, and deployed automatically using GitHub Actions.
This website works with two main object types, Surveys and Responses. A Survey contains the data necessary to build a series of questions that can be presented to the user. A Response encodes the user's answers to the Survey questions. Because which questions get asked depend on how previous questions were answered, both Surveys and Responses are stored as trees, though in different formats.
Question refers to questions that are asked to the user, either single- or multiple-choice, and option refers to the available options for a question. Choice refers to which of the options the user selected.
An answer is an answer to a single question. A response is the collected answers that a user has given for a survey. A response is finished when all queries have been answered. A response is "effectively finished" when no possible future answers will change the outcome of the survey.
Incident level refers to whether an incident qualifies as non-serious (regular incident), a serious incident, or an accident. Leveling refers to marking a particular level as having been achieved (e.g., a user's answer qualifies the incident to be considered an accident). The final incident level is the highest of each answer's incident level.
Flag refers to information about the user which determines what queries to show. For example, queries regarding rotor blade damage will not be shown if the user was not flying a helicopter.
A {@link Survey} is a tree organizing the queries that make up the survey. When the user is given the survey, the tree is traversed depth-first, and the user's answers determine which paths are followed or skipped.
The tree consists of the following node types:
Name | Description | Connects to |
---|---|---|
{@link Question} | A question presented to the user. | One or more Option s |
{@link Option} | A possible option a user can choose for a Question. | One Action |
{@link Action} | What to do after the user selects an Option. Either present a follow-up Question, or set the incident level (accident, incident, etc.). | One Question , or null . |
Surveys are stored in JavaScript files under src/data
, and loaded at runtime
into a dictionary. They are retrieved by their identifier (e.g., "incident"
for the incident survey that is the raison d'etre of this website). The
data/surveys.ts
file contains an importable dictionary of all surveys, keyed
by their identifier. The data/surveyOrder.ts
file contains the order in which
the surveys should be presented to the user.
Questions about the survey are answered using {@link SurveyTraverser}, which traverses the tree in a depth-first manner and uses a Visitor paradigm to pass events to the consumer.
A {@link Response} is a tree organizing the user's answers to each of the queries. It consists of the following nodes representing answers:
{@link QuestionNode} | Contains a nodes array, where the selected options are represented by the presence of further nodes. |
In addition, the following joining nodes are used:
{@link ActionNode} | Joins QuestionNodes to their children. |
{@link endNode} | Marks the end of a path in the tree. |
These nodes are all interrelated in the following ways:
QuestionNode
contains a nodes
array. For each option the user selected, an
ActionNode
is present in the array at that index, linking to the next
QuestionNode
for that path, or EndNode
if that is the end of that path. For
each option the user did not select, the array contains undefined
.
Using ActionNode
allows us to differentiate between the situation where the
user did not choose an option (undefined
) and where the user did choose an
option but has not yet answered any follow-up questions resulting from that
option (ActionNode
linked to an EndNode
).
Response trees are accessed using answer paths, which are arrays of numbers.
Each element of the array is an index in a QuestionNode
's nodes
array to
follow. Answer paths must terminate in an end node; incomplete paths are not
allowed.
The {@link ResponseTraverser} class traverses a Survey and Response tree simultaneously. It is written nearly identically to the SurveyTraverser class, except the visitor callbacks yield corresponding nodes in both trees simultaneously.
User state management is done with Vuex. The root Vuex module contains a dictionary of Responses for each Survey the user has completed, keyed by the survey identifier. As the user answers questions, the answers are added to the appropriate Response tree in Vuex.
The root Vue is the {@link App} view, which contains a single element, the
{@link Container}. The App view creates the #app
element which positions the
content in the center of the page using a flexbox. The Container view handles
rendering subviews and animating between them.
Most layout is done using flexboxes. The main view is fixed in horizontal extent above the mobile break, and allowed to flow in vertical extent with a minimum height.
Both light and dark themes are supported based on OS settings.
Styling is written using SCSS. The src/assets/styles
directory contains global
CSS (global.css
) as well as a number of library functions and mixins used
throughout the app. View-specific CSS is placed in the <style>
tags within
Vue files.
The font "Quicksand" is used throughout the application. It is a variable font,
meaning that its weight (among other properties) can be varied fluidly. The
weights used in this application are defined in _fonts.scss
. The font is
loaded in font-faces.scss
.
Animations are used extensively throughout the app, powered by Vue transitions.
These animations are defined in transitions.scss
, which also contains a number
of mixins designed to make writing animations less tedious.
Responsive behavior, including the mobile break and scaling of font and margin
sizes, is done in _responsive.scss
.
Unit tests are powered by Mocha using Chai's expect
syntax. A fixtures.ts
file contains example survey responses that can be used for testing.
Cypress tests are written in TypeScript. To prevent Mocha/Chai's globals from
interfering with Cypress's globals, a custom tsconfig.json
and cypress.d.ts
have been placed in the test/e2e
directory to isolate the Cypress namespace.
test/e2e/plugins/index.js
has been modified to add a Webpack preprocessor that
makes module path resolution work the same in both the application and Cypress
environments. This way, application modules can be used in Cypress tests. This
was done because ResponseTraverser
is used by Cypress to run test flows from
fixture responses (genius!).