Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gql mock server #1071

Merged
merged 19 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ jobs:
# skip git, yarn, pixel test script and HAR file, and all i18n resources.
# Also, the a11y test file has a false positive and the ignore list does not work
# see https://github.com/opentripplanner/otp-react-redux/pull/436/checks?check_run_id=3369380014
skip: ./.git,yarn.lock,./a11y/a11y.test.js,./a11y/mocks,./percy/percy.test.js,./percy/mock.har,./i18n,./__tests__/mocks
skip: ./.git,yarn.lock,./a11y/a11y.test.js,./a11y/mocks,./percy/percy.test.js,./percy/mock.har,./i18n,./__tests__/mocks,otpSchema.json,./percy/mocks/*.json
2 changes: 1 addition & 1 deletion lib/actions/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ export function findStopTimesForStop(params) {
const { date, stopId, ...otherParams } = params
let datePath = ''
if (date) {
const dateWithoutDashes = date.replace('-', '')
const dateWithoutDashes = date.replace(/-/g, '')
datePath = `/${dateWithoutDashes}`
}

Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"semantic-release": "semantic-release",
"start": "craco start",
"percy-serve": "serve",
"percy-har-express": "har-express"
"percy-har-express": "har-express",
"percy-combined-mock-server": "node ./percy/mock-server.js"
},
"standard": {
"parser": "babel-eslint"
Expand Down Expand Up @@ -50,8 +51,8 @@
"@opentripplanner/location-icon": "^1.4.1",
"@opentripplanner/map-popup": "2.0.7-alpha.1",
"@opentripplanner/otp2-tile-overlay": "1.0.7-alpha.2",
"@opentripplanner/printable-itinerary": "2.0.10-alpha.4",
"@opentripplanner/park-and-ride-overlay": "^2.0.6",
"@opentripplanner/printable-itinerary": "2.0.10-alpha.4",
"@opentripplanner/route-viewer-overlay": "^2.0.14",
"@opentripplanner/stop-viewer-overlay": "^2.0.7",
"@opentripplanner/stops-overlay": "^5.1.2",
Expand Down Expand Up @@ -131,6 +132,7 @@
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/preset-typescript": "^7.15.0",
"@craco/craco": "^6.3.0",
"@graphql-tools/schema": "^10.0.0",
"@jackwilsdon/craco-use-babelrc": "^1.0.0",
"@opentripplanner/scripts": "^1.2.0",
"@opentripplanner/types": "^6.1.1",
Expand Down
44 changes: 26 additions & 18 deletions percy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ PERCY_OTP_CONFIG_OVERRIDE=<path-to-yml> npx percy exec -- npx jest percy/percy.t
## Mock OTP Server and HAR file

Percy tests are rely on a mock OTP server that returns preset responses to given URLs.
The mock server is powered by [har-express](https://github.com/toutpt/har-express).
The mock server is powered by an express.js server, which forwards REST requests to a HAR server and handles GraphQL requests with a GraphQL server.

The mock server uses a [HAR (HTTP Archive) file](https://en.wikipedia.org/wiki/HAR_(file_format)),
`mock.har`, as input.
The HAR server uses a [HAR (HTTP Archive) file](https://en.wikipedia.org/wiki/HAR_(file_format)),
`mock.har`, as input. The GraphQL server uses high level resolvers for each request type, processes some important arguments, and then returns
objects from static JSON files sourced from real OTP queries.

The HAR file is checked in this repo and contains a predefined lists of web queries and their corresponding responses.
We use these predefined responses, which are snapshots of responses from a live OTP server,
Expand All @@ -24,22 +25,29 @@ This avoids failed screenshot comparisons caused by external factors such as tra

At this time, only the OTP server is mocked. The same `mock.har` file is (re)used for all the UI tests.

## Duplication of responses in `mock.har`
In all cases, `mock.har` needs to contain the mock responses for all REST queries the UI is expected to make,
or the screenshot comparison will fail

You will notice that `mock.har` contains a number of repeated or duplicate query/response sets.
The position of these queries are also noted in the test script.
This is because har-express only compares the URL of the request (including query parameters),
but ignores the request body and headers completely. For GraphQL queries especially, the URL is always `http://localhost:9999/otp2/routers/default/graphql`, but the GraphQL content sent by the UI is ignored.
The GraphQL requests are handled by the GraphQL.js library, which uses a schema from OTP in `otpSchema.json`.
This file comes from the introspection query against an OTP GraphQL server.
GraphQL queries are handled by resolvers in `graphql-mocks.js`, which contains a standard GraphQL handler provided by the GraphQL library.
Objects for the various queries are provided in the `mocks` folder.

When a given URL is sent to har-express again, har-express will work in a **cycle**
and return the response for that URL that follows the response previously given for that URL.
When all the responses to a URL in the HAR file have been exhausted, the first one is sent again.
Some gotchas experienced while setting up these mocks:
- The frontend expects the server to return the correct stop ID when it sends a request for stop information. That means that if you send a request for stop ID X, and the server returns data for stop ID Y, the frontend will not work correctly. For the purposes of a screenshot test it may not matter, but for OTP-RR, the ID needs to match. I solved this by adding a handler that checks the `id` argument passed with the request, and returns one of two mocks depending on the ID.
- When getting data from the mocks, the easiest way is to look at the request OTP-RR makes against a real server and copy the response into the mock JSON file. However, when items in the query are renamed, e.g. `id: gtfsId`, you need to rename those back to the original name for the mock, otherwise the shape of the data won't match the schema. You will be warned about this in the logs when the query is made, though, so pay special attention to those to figure out what needs to be corrected.

This feature or bug is advantageous because it forces us to keep track of **all** expected web requests
made by the OTP-RR UI in `mock.har`, in the order they are made:
- If a web request is introduced/removed,
it will disrupt the order of the responses sent by the mock server.
- If the UI makes unnecessary web requests, those requests still have to be included in `mock.har`.
There is also another HAR file and mock server used for the geocoder, this one without an express server sitting in front.

## Running and debugging the Percy tests
You can run the Percy tests locally with this command:
```OTP_RR_UI_MODE=normal npx percy exec -- npx jest percy/percy.test.js```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is env needed before the env variables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for me. I think because this is a Bash command, it's okay. I am not sure about Windows.

or this for calltaker:
```OTP_RR_UI_MODE=calltaker npx percy exec -- npx jest percy/percy.test.js```

This will run the mock servers and then start the tests in headless mode by default.

You can disable headless mode by setting `headless: false` in the Puppeteer launch settings in `percy.test.js`. We left a line commented out which you can uncomment to achieve this.

You can also debug the tests by creating a JavaScript Debug Terminal, then running the above commands with breakpoints set in the editor.

In all cases, `mock.har` needs to contain the mock responses for all queries the UI is expected to make,
or the screenshot comparison will fail.
83 changes: 83 additions & 0 deletions percy/graphql-mocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const PlanResponseBike = require('./mocks/PlanResponseBike.json').data.plan
const PlanResponseWalk = require('./mocks/PlanResponseWalk.json').data.plan
const PlanResponseBusSubwayTram =
require('./mocks/PlanResponseBusSubwayTram.json').data.plan
const TripResponse = require('./mocks/TripResponse.json').data.trip
const NearestResponse = require('./mocks/NearbyResponse.json').data.nearest
const Stop114900Response = require('./mocks/Stop114900Response.json').data.stop
const Stop803Response = require('./mocks/Stop803Response.json').data.stop
const StopsByRadiusResponse = require('./mocks/StopsByRadiusResponse.json').data
.stopsByRadius
const ServiceTimeRangeResponse =
require('./mocks/ServiceTimeRangeResponse.json').data.serviceTimeRange
const RoutesResponse = require('./mocks/Routes.json').data.routes
const IndividualRouteResponse = require('./mocks/IndividualRoute.json').data
.route

function getPlanResponseMock(transportModes) {
const transportModesString = transportModes
.map((tm) => tm.mode)
.sort()
.join('')
switch (transportModesString) {
case 'BICYCLEBUSSUBWAYTRAM':
case 'BUSSUBWAY':
return PlanResponseBusSubwayTram
case 'BICYCLE':
return PlanResponseBike
case 'WALK':
return PlanResponseWalk
default:
return PlanResponseBike
}
}

function getStopResponseMock(stopId) {
switch (stopId) {
case 'MARTA:803':
return Stop803Response
case 'MARTA:114900':
default:
return Stop114900Response
}
}

const increment = (obj, key) => (obj[key] ? obj[key]++ : (obj[key] = 0))

const mocks = (callCount) => ({
QueryType: {
nearest() {
increment(callCount, 'nearest')
return NearestResponse
},
plan(obj, { transportModes }) {
increment(callCount, 'plan')
return getPlanResponseMock(transportModes)
},
route() {
increment(callCount, 'route')
return IndividualRouteResponse
},
routes() {
increment(callCount, 'routes')
return RoutesResponse
},
serviceTimeRange() {
increment(callCount, 'serviceTimeRange')
return ServiceTimeRangeResponse
},
stop(obj, { id }) {
increment(callCount, 'stop')
return getStopResponseMock(id)
},
stopsByRadius() {
increment(callCount, 'stopsByRoute')
return StopsByRadiusResponse
},
trip() {
increment(callCount, 'trip')
return TripResponse
}
}
})
module.exports = mocks
Loading
Loading