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.

After adding a new reusable component into the shared directory, ensure to update the reusable-components section of the handbook with the new updates.

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 8 Cypress test scenarios namely:

  • Forms
  • Gate Access
  • Guest Invitation
  • Manual Gate Access
  • Payment
  • Properties
  • Tasks
  • Timesheet

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,
      :module,
      :role_id,
      permissions: [],
    ).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.
7 cy.visitUserProfile(userName) Visits the 'Users' menu item, selects the user name passed in the argument and clicks the card. It asserts that the passed user has access to the Users menu list and can see their profile.
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.

Sometimes, tests will pass locally but fail on the CI. A possible cause is that the view port on the CI could be a different size, so some elements end up being covered and thus not visible as far as cypress is concerned. You can access the videos from the CI build to see the problem. To download the videos, go to Gitlab and in the platform project, navigate to CI/CD -> pipelines. This lists all recent builds. Check the failing build for your tests and click on the icon on the right to download the cypress artifacts. See image below for more context.

Cypress_Artifacts

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.

How To Resolve Vulnerability Issues

Below are the most common categories of vulnerability issues we usually encounter.

  • Generic Object Injection: This happens when you use the square bracket notation to access an array element or an object value, or you add an element to an array or add an item to an object using the square bracket notation. We couldn’t find a good way of fixing this, so we resulted to creating two helper functions(objectAccessor and setObjectValue) to handle this logic and re-use them whenever there’s a need to use the square bracket notation. Examples:
  • objectAccessor
  • setObjectValue

  • Dependency Upgrade: This happens when a security issue is discovered in one of our packages. What we do in this case is to upgrade to a secured newer version or find an alternative package that can be used instead. If the affected package is a sub-dependency, then we upgrade the parent package to a version that makes use of a secured version of the sub-dependency. Here is an example of this kind of upgrade.

  • PII Disclosure: At times, our security scanners discover what might look like a personal identifiable information, e.g credit card numbers. What we do in this case is to study the pattern and rewrite it so it doesn’t look like a PII. Here is an example.

  • Possible SQL Injection: This is usually introduced through raw SQL queries. We fix this by sanitizing the query or re-write using ActiveRecord. Here is an example.

  • Hardcoded Username: This happens when setting an object string value using the dot notation. We fix this by using the spread operator or Object.assign(). Here is an example.

Important Notes about MUI Upgrade from v4 to v5

  • Button's default color is now "primary", nothing like "default" color again. It's either you explicitly pass a valid color or assume "primary" will be used instead.
  • Note: Make sure you always pass a valid color prop to Button, "default" is now invalid in v5, passing "default" or any other invalid color will result into an "undefined method main of ownerState" (something along that line) in your test and console.
  • The <Hidden /> component is now deprecated, you should now use useMediaQuery instead. Using still works but throws a "deprecation warning" in the console. There's one problem with useMediaQuery though, as raised in this Gitlab issue, useMediaQuery uses CSS's "display: none" under the hood, and as result it does not do proper hiding that does. This has an effect on our tests because the supposed hidden parts of our components are still visible to Jest, so you may run into a situation where multiple elements would be found when you're actually expecting one, then you have to use getAllByTestId('your-id')[0] or getAllByText('your-txt')[0] as the case may be.
  • The internal structure of <TextField /> has also slightly changed. In some situations which we have not properly figured out, if you give a "data-testid" to your text-field, it creates another span with the same "data-testid" and this is visible through browser-inspect, therefore Jest sees both of them when you run the test. You should also fix this with the same method above, getAllByTestId('your-id')[0] or getAllByText('your-txt')[0] as the case may be.
  • In case you run your test and you get an error related to "theme", something along the line "Cannot read property 'breakpoints' of null" or "Cannot read property 'palette' of null", it's an indication that you need to wrap your component inside <MockedThemeProvider />. This wasn't strict in v4 but now very strict in v5.
  • The "underline" prop of the <Link /> component now defaults to "always". If you want underline to only show up on hover, then pass underline='hover'
  • <TextField /> with "outlined" variant must now have a label prop, otherwise the top border would not show.
  • <IconButton /> now has a default size of 40px. To get the old default size of 48px, use size='large'.

How To Fix Jest Errors and Warnings

At times, due to the way we have written our tests or the way we have written our React components, we do get some warnings in the log when we run our front-end tests. Some of these warnings also show up in the browser's console for components that have been written incorrectly, such as passing incorrect props, specifying the wrong prop type, etc.

Below are the common ones we have come across with explanations on how we have been resolving them:

  • Failed prop type: This happens if an invalid prop is passed to a component. For example, a component receives a string instead of an object. This should be fixed by providing an appropriate prop. It can also happen if a component expects a required prop but it's not passed into it. For example, a missing "children" prop.

  • Missing Fields in GraphQL Queries: When testing a component that makes use of a GraphQL query(i.e fetches some results from the backend), a mock structure of the query is usually provided, if a field is missing in the query provided, this warning is thrown. All you have to do is make sure the structure of the query used in the component tallies with the mock used in the test.

  • Out-of-range Value for Select Component: This basically means "the value of a select input should be the value of one of the options of the select input". This warning is usually raised if you are rendering the options of a "Select" with a result of a GraphQL query(which is async) and you specify null or an empty string as the default value of the select. The best way to fix this is not to render the select component until the data result(to be used as options) is ready.

  • Update Not Wrapped in act(): In most cases, this means you need to wait before doing assertions in your test. It could also mean that you need to use the right queries, i.e making sure the queries used in the test tally with the ones in the component.

  • TypeError: activeElement.attachEvent is not a function: No solution found yet.

  • Can't perform React update on an unmounted component: No solution found yet.