Skip to content

General Engineering Standards

Most of the notes here are documented here because we have already been using them in the application and it is better to be consistent, with that said feel free to make suggestions on what we can improve, what we remove from here and what can be added.

General

Naming We prefer using underscore for compound worded values, this is great because it nicely matches with rails convention of underscore.

// Bad
const type = 'user-type'
const clienttype = 'prospective-client'

// Good
const type = 'user_type'
const clientType = 'prospective_client'
Eslint will warn you when naming a variable with an underscore so variable prefer to use camelCase.

Linters

We use Eslint for Javascript files and Rubocop for ruby files, this have their own rules try not to disable the rule set, Discuss with the team if you think there is a rule that needs to change.

Always make sure you clean up the code by re-enabling rules that might have been disabled, This is highly encourage because we iteratively add rules.

Shared Components

When working on a feature, first think of reusability:

- Can other engineers make use of it  
- Has another engineer created something similar before.  
- How better can it be improved.

After consider the above points, if the utility can be shared, make sure you put it under shared directory and communicate to other engineers via slack.

Modules

Modules in our codebase are like a representation of high level community features, these are meant to make the codebase easy to manage.

  • It is okay for a module to have a submodule
  • Module should contain all the files it needs:
    • GraphQL related files
    • All routes for the module
    • Tests
    • Components
    • Utilities
    • Settings
  • Do as much to reduce a module inteferring with another module

Directory structure of a module

  • app/javascript/src/modules/|module name|

    • index.js : file indicating how to load the javascript side of the module - contains menus convention.
    • Component: Directory containing module specific ReactJS components.

    • tests : Directory containing the tests for the reactJS components.

    • GraphQL : Directory containing the Queries and Mutations used by the module.
    • Dashboard/fragments: Directory containing the ReactJS Fragments to load the module dashboard display.
    • User/fragments: Directory containing the ReactJS Fragments to load the module User Details display.
    • Settings/fragments: Directory containing the ReactJS Fragments to load the module Community Settings display.
    • app/policies/policy/|module name| : Rails directory containing permissions for the module. (affects backend and frontend)
    • app/graphql/|module name| Rails handling of GraphQL Queries. (not there yet)
    • app/models/|module name| Rails Models by module
    • app/lib/|module name| Rails lib by module

Common Components

Please add shared common components to the following directories:

  • Javascript:
    • app/javascript/src/shared/
  • Rails:
    • app/lib/shared?

Legacy Components (DO NOT ADD COMPONENTS THERE):

Javascript: app/javascript/src/components/

Javascript Tests

When writing test for react, we highly recommend you use react-testing-library because we are incrementally removing enzyme in our codebase to make our tests easy to write and giving us the ability to test what the user sees rather than the implementation.

  • Avoid duplicate test titles
  • Make your components small so that they are easier to tests
  • When testing components with GraphQL mutations or queries:
    • Make sure the variables in the component and in your test files are exactly of the same type and value
    • Make sure you provide all arguments needed
    • Returned query should match what's declared to avoid warnings in the console
    • Make sure to assert right, avoid expect(value).toBeTruthy instead check the exact value you expect
  • Avoid using wrong names for mocks. e.g: errorMock when it is not an error

Note: To run jest test outside docker which should be fast, we recommend using these extensions: - jest
- vscode-jest
- jest-runner

End to End Testing

We use Cypress for our end-to-end testing due to the many benefits it provides and its excellent documentation. As of this writing, we have 3 Cypress test scenarios (Gate Access, Timesheet, and Payment) and we hope to add more as we progress. This integration is included in our Gitlab CI pipelines and the tests are run when deploying to Staging, this gives us the confidence to go ahead with the production push in case of no Cypress failure.

How To Add a New Cypress Test
  • Create a new file inside the cypress/integration directory. E.g cypress/integration/timesheet.js
  • Write your code. We can group the code into four parts as shown in the example below:
describe('Time Sheet', () => {
  it('allows custodian to record time shift', () => {
    // First part
    cy.factory('community', { name: 'Nkwashi' }).then((communityResponse) => {
      cy.factory('store_custodian', {
        name: 'Mr Custodian',
        phone_number: '2348167740149',
        email: '[email protected]',
        state: 'valid',
        community_id: communityResponse.body.id
      })
      cy.factory('security_guard', {
        name: 'A Guard',
        phone_number: '2347065834175',
        email: '[email protected]',
        community_id: communityResponse.body.id,
      })
    })

    // Second part
    cy.login('2348167740149')

    // Third part
    cy.visit('/search')
    cy.get('.user-search-input').type('A Guard').type("{enter}")
    cy.wait(2000)
    cy.get('.user-search-result').click()
    cy.wait(1000)
    cy.get('#closeBtn').click()
    cy.get('.start-shift-btn').click()
    cy.wait(20000)
    cy.get('.end-shift-btn').click()

    // Fourth part
    cy.visit('/timesheet')
    cy.get('.shift-user-name').should('contain', 'A Guard')
  })
})

The first part is the factory part that allows you to seed the test DB with the necessary data you are going to be needing. In this example, we created three records; community and two user records (a custodian and a security guard). Note that the first argument supplied to cy.factory() must be a valid factory name, i.e store_custodian and security_guard can be found in spec/factories/user/user.rb and community can be found in spec/factories/community.rb.

The way cy.factory() command works is that it makes a request to the factories_controller using the second argument as the body params. As a result of this, we should always make sure we require the necessary factory files at the top of the controller file. Below is what it currently looks like:

unless Rails.env.production?
  require Rails.root.join('spec/factories/community.rb')
  require Rails.root.join('spec/factories/users/user.rb')
  require Rails.root.join('spec/factories/properties/account.rb')
  require Rails.root.join('spec/factories/properties/land_parcel_account.rb')
  require Rails.root.join('spec/factories/properties/land_parcel.rb')
end

At the same time, we should ensure we add new body params as part of the permitted attributes in the factory_attributes method of the same controller. Here's what it currently looks like:

  def factory_attributes
    params.fetch(:attributes).permit(
      :name,
      :phone_number,
      :email,
      :state,
      :community_id,
      :parcel_number,
      :user_id,
      :land_parcel_id,
      :account_id,
    ).to_h
  end

The second part is the login part, you basically pass in the phone-number of the user you want to log in to cy.login(). Note that the phone-number must be that of an already created user.

The third part is where you mimic user interaction on the app, such as visiting a URL, clicking a button, typing into an input field, etc. You can check the documentation here for a better understanding and of this part.

The fourth part is where you do assertions based on the third part you've written, such as; verifying that a div contains a certain value, a button is enabled/disabled, etc.

Cypress Commands Reusability

Cypress provides a simple API for creating custom commands. Using this API, we've handy custom commands you can re-use when writing your own tests.

Custom commands ensure the tests are DRY. We have commands that cover things like - general navigation, performing repeated actions such as Log In, adding a payment plan, creating a custom form and more.

See table below:

SN Command Description
1 cy.login (phoneNumber) Log In a user with a phone number. Cypress receives a phone token, and completes the login step
2 cy.visitMainMenu (menuItem) Toggles the Left Main menu, and opens a menu item. The menuitem args could be provided as a class name or data-testid.
3 cy.myProfile() Visits the current user's profile page.
4 cy.visitUserMenu (menuItem) Opens the right menu under the user's profile page, and visits the menu item. The menuitem args could be provided as a class name or data-testid. It would typically work if you're already on the user's profile page using cy.myProfile() command.
5 cy.addNewPaymentPlanFromUserProfile(object)

{
duration: "Duration of payment plant",
amount: "Payment plan amount",
type: "Payment plan type",
plot: "Plot associated with plan",
coOwner (optional): "Associated co-owner"
}
Creates a new Payment plan from the user profile page with provided data.
6 cy.addFormProperty(fieldName, fieldType, isRequired, options) Creates a form property under a category in a custom form.

For e.g cy.addFormProperty('TextField', 'text', true), will create a required textfield. The options argument is optional. If provided, it contains a list of options for a radio or checkbox field type.
How To Run Cypress

Run sh ./bin/integration_tests.sh Note that this takes some time to run as compilation usually takes a few minutes.

If you rebuild before running the tests, probably because you have some new gems, you should pass an argument to the command. E.g sh ./bin/integration_tests.sh 'ci'. This will build and then run the tests.

How To Debug Cypress Failure

Cypress errors are usually self-explanatory. In case of confusing error messages, you can check recorded videos to see what's going on. Every time you run Cypress tests, it automatically records a video for each test file and saves them in tmp/cypress/videos, you can go in there, download the file and watch.

### Storybook

As Engineer, you should strive to always create components that can be reused in the application for similar purposes that it serves, this way we reduce the amount of work needed to be done and it also helps us be consistent in how we do things. Every reusable component should be documented in storybook, You can find its documentation here http://storybook.js.org.

Example of how a simple story is written

Say you have Button in components/Button.js

export default Button({isDisabled}){
  return <button disabled={isDisabled}></button>
}

Button.propTypes = {
  /**
   * This determines whether the button is clickable or disabled
  */
  isDisabled: PropTypes.bool.isRequired,
}
In the Stories directory you create a file with the name of your component.stories.js in our example it would be button.stories.js

// import the file in stories directory from the components directory
import Button  from '../app/javascript/src/components/Button'

export default {
   // Add the story to components category or a matching cateogory depending on what it is.
    title: 'Components/Button',
    component: Button,
}
// If the component has arguments which is likely to be the case
// then define different versions of that component with the props

const ButtonTemplate = (args) => <Button {...args} />;

export const SimpleActiveButton = ButtonTemplate.bind({});

SimpleActiveButton.args = {
  isDisabled: true',
};

export const DeactivatedButton = ButtonTemplate.bind({});

DeactivatedButton.args = {
  isDisabled: false',
};

After saving your changes, navigate to http:localhost:6006 and you should be able to see the newly created component reflect in storybook

Engineers must read the Storybook tutorial: https://www.learnstorybook.com/intro-to-storybook

The DoubleGDP Storybook documentation can be found here: https://doublegdp.gitlab.io/app/

App Navigation

When working on a ticket that requires a new route or to change existing - Ensure there is backward compatibility if there is route change
- Routes should follow a predictable naming standards:
- e.g: /tasks /tasks/:id, /tasks/:id?type=edit, /tasks/:id?type=new
- If it is a compound, routes should use underscores. e.g: /log_book, /event_logs

GraphQL

Migration

Migrations should be generated every time database table schema is changed. When pushed to staging they are run automatically.

BE CAREFUL not to drop a column/table, rename a column.

Rake Tasks

If the story you are working on requires to run a rake task, communicate this to other engineers and add the necessary label.