Network Needs
- do not log / hide requests that match a certain URL, criteria, etc
- gain access to the request in order to...
- assert on the request URL
- assert on the request headers
- assert on the request body
- modify the request URL?
- modify the request headers (add / remove headers)
- modify the request body
- control the response in order to…
- delay / simulate delay
- pause the response until a condition is met
- pass through the real ‘origins’ response
- modify the real origins response
- control the response entirely at the cypress proxy layer
- match a response with a particular file or fixture
- repeat the same things above but also for websockets / server sent events
- wait on specific frame requests
- act as the server for sending frame responses
- modify origin server frame responses
- programmatically access…
- some particular requests / all requests
- some particular responses / responses
- routes to dynamically add or remove them
- all things like graphql to be easy to stub / modify / accommodate
- block / throttle requests made that match a certain criteria… domain, host, etc
Cypress Needs
- wait in cypress for the request to be made
- wait in cypress for the response to finish
- flush all paused responses
- create routes on a per test basis
- reset routes before each test
- coordinate routes + fixtures + aliases + commands
Visual Needs
- Understand which requests have been made
- When responses have been finished
- Which requests match things like aliases
- Which routes have been entered
- Why a request did or didn’t match a route
- The URL, status, response, etc
- When requests pass / fail
- Errors in callback handling per request
Power Features
- Record / export requests as fixtures
- Route / code generation for requests
Discussions
- What to do with existing
blacklistHosts
? - Whether to mimic / expose a similar API for
cy.visit()
- How exactly to implement the
next()
pattern
// global defaults for all network traffic
Cypress.Network.defaults({
hide: '...',
ignore: '...',
})
// alias or fixture
cy.route('/login', '@someAlias')
cy.route('/login', 'fixture:some/user.json')
// default is now to match all method names
cy.route('/login', { some: 'response' })
cy.route('ALL', '/login', { some: 'response' })
// all of these are equivalent to modify the server response
cy.route('POST', '/login', { some: 'response' })
cy.route('POST', '/login', (req) => {
req.reply(200)
req.reply(200, { some: 'response' })
req.reply(200, { some: 'response' }, { 'x-new-headers': 'from-server' })
req.reply('fixture:some/user.json')
req.reply({
some: 'response'
})
req.reply({
status: 200,
body: {
some: 'response'
},
headers: {
'x-new-headers': 'from-server'
}
})
// (passthrough) not intercept the response
// (terminate) intercept and act as origin
// (passthrough + modify)
// use a function here to access the
// response that was passed through
// from the origin server...
req.reply((res) => {
// res.body
// res.status
// res.headers
res.status = 200
res.headers['x-new-headers'] = 'from-server'
res.body = {
some: 'response'
}
// dynamically alter the body
res.body = res.body.replace('window.top', 'window.self')
})
// redirect the request
req.redirect(...)
})
// we can also modify the original request
cy.route('/login', (req) => {
req.headers = {...} // or req.setHeader(...)
req.body = {...} // or req.setBody(...)
// delay could take two numbers or { client: X, server: Y } values
req.delay(100)
req.delay(500, 1000)
req.delay({
client: 100,
server: 200,
})
req.throttle('3G')
})
//cy.route(['@blacklistGoogle'])
// can blacklist still...
cy.route('*.google.com', (req) => {
req.reply(503, '').as('blacklistGoogle')
})
cy.route('ALL', '*.google.com', {
status: 503,
headers: {},
body: '',
})
// ...MIDDLEWARE PATTERN...
// what will happen when one or more routes are matched?
// what is the order of preference?
// do routes you define later have more priority?
// does the first one match?
route('*', (req, next)) // match any route
route('/foo', (req)) // is hit
route('/foo', (req))
route('/foo', (req)) // is never hit
// if we don't yield a second argument cypress will know to
// terminate the response here in this handler? probably, yes
route('localhost', (req, next) => {
req.headers = { }
if (req.body...) {
doSomething()
}
return next() // require a return or ?
}).as('l1')
route('localhost', (req, next) => {
next()
next() // what to do?
})
.as('l2')
// route all things based on general request type
route({ type: 'javascript' })
route({ type: 'json' })
route({ type: 'html' })
route({ type: 'image' })
route({ contentType: 'application/script' }, (req, next) => {
req.reply('fixture:pdfs/foo.pdf')
// possibly capable of implmenting but might
// not be performant at all
req.pipe((chunk, enc, cb) => {
})
req.reply((res) => {
res.body.replace('window.top', 'window.self')
// indicate we want to proceed to the next handler...?
// there's likely no way we can or should do this...
next(res)
})
})
// route('*.google.com', (req, next) => {
// req
// .filter(...) // could this automatically call next?
// .modify((url, body, headers) => {
// })
// .reply(200, ...)
// })
route('*', (req, next) => {
next() // what if there are none others?
})
// whenever a request matches a route we should visually indicate it
// and let the user know how many routes it matched and why (?)
// possibly also add an area that lets them test why the route didnt
// match the defined routes
// can route based on strings, regex, or globs
// that match the URL, or URI parsed objects
cy.route('/foo', (req) => {})
cy.route('/bar?baz=quux', (req) => {})
cy.route('http//:localhost:8080/do/the/thing', (req) => {})
cy.route('http//:localhost:8080/**/foo.json', (req) => {})
cy.route('*.github.com/oauth', (req) => {})
cy.route('github.com/login', (req) => {})
cy.route({
url: '*',
onRequest: (req) => {
}
})
cy.route({ protocol: 'https' }, (req) => {})
cy.route({ host: 'localhost', query: { foo: 'bar' } }, (req) => {})
cy.route({ pathname: '/'}, (req) => {
// THESE ARE FILTERING EXPERIMENTS BUT
// YOU CAN LARGELY IGNORE, THEY AREN'T NECESSARY
if (req.body) {
// bad because we don't know why or what you did..
}
// better because we know why something didn't match
req
.filter(someExpression === 'something...')
.set({
})
.
.filter((url, body, headers) => {
})
.filter({
// useful for graphql
body: (body) => {}
})
.filter({
url: {
pathname: {
}
}
})
req
.match({
url: '...',
body: '...',
headers: '...',
})
.reply(...)
.as('someStricterThing')
req
.filter({
})
.delay(..., ...)
.delay(true) // holds indefinitely
.reply((res) => {
})
// control how many times this applies
req
.times(3)
.reply(200, '@bobUser').as('bobUser')
.reply(200, '@sallyUser').as('sallyUser')
.pause()
.once()
.end()
.persist()
.record() // saves
.save('getUser.json') // records the thing
})
// we want to be able to get all the requests or responses for
// a particular route or just get at all of the available routes
// or delay / throttle / respond to all requests currently paused / delayed
cy.network('reply', '@getUsers')
cy.network('reply', ['@getUsers', '@getFoo'])
cy.network('throttle', '...')
cy.network('replyAll')
cy.network('reply', '@websockets', { ... })
cy.network((routes, requests, responses) => {
routes.remove(...)
routes.delete()
})
cy.network('routes', 'remove', '...')
cy.network('requests').should('have.length', '...')
cy.network('responses').should('have.length', '...')
cy.network('route', '@getUser').its('requests' | 'responses')
cy.network('@getUser', 'request')
cy.network('@getUser', 'response')
cy.network('@getUser', 'route')
cy.network((net) => {
net.routes
net.requests
net.responses
})
cy.network('holdAllRequest')
cy.route(...)
cy.visit(...)
cy.get('#loading')
cy.network('release')
cy.network((ret) => {
expect(ret.requests).to.be.gt(20)
})
// can programmatically alter the rules or
// access requests or responses
cy.network((rules) => {
rules.disable('@blacklistGoogle')
rules.enable('@blacklistGoogle')
})
// or keep cy.server(...) ?
cy.network((net) => {
// programmatically alter cy.route(...)
net.routes.add('...')
net.routes.remove('...')
})
cy.server((server) => {
server.routes.add('...')
server.routes.remove('...')
})
//cy.route('enable', '@getUser')
//cy.route('disable', '@getUser')
// WEBSOCKET HANDLING...
cy.route('websockets', '/ws', (ws) => {
ws.onRequestMessage((msg) => {
})
ws.onResponseMessage((msg) => {
})
ws.encode((frame) => {
return engineio.encode(frame)
})
ws.decode((msg) => {
return engineio.decode(msg)
})
ws.onFrameReceived((frame) => {
})
})
cy.route('websockets', '/ws', (ws) => {
ws
.filter((msg, direction = 'sent | received') => {
return /something/test(msg)
})
.on('request', (frame) => {
})
.on('response', (frame) => {
})
.reply(...)
.as('chatMessage')
})
// CY.VISIT() ONE-OFF HANDLING
// with a similar API as above
cy.visit('....', (req) => {
req.whatever().reply()
})
cy.visit({
url: '...',
method: 'POST',
headers: { ... },
reply: (res) => { ... },
})
cy.visit('POST', '...', (req) => {
req.headers
req.body
req.whatever().reply()
req.reply((res) => {
})
})
// REQUEST RECORDING / PLAYBACK
cy.server((server) => {
server.save()
server.record()
server.record(false)
server.load(...)
server.replay(...)
})
// we can also add something in the UI that records
// all the requests (like a button when pressed)
----
// import gqlMocks from 'cypress-graphql-mocks'
// const mockApi = gqlMocks.configure({
// method: 'POST',
// url: '/graphql',
// name: 'api',
// schema: '...',
// mocks: '...',
// })
import mockApi from '../helpers/graphql'
Cypress.Network.defaults({
rules: [
{
}
]
})
// TODO: support array of routes
const GetMe = mockApi('GetMe', {
currentUser: {
firstName: 'brian',
lastName: 'mann',
}
})
GetMe.asRoute((req) => req.cy.get('#something'))
cy.visit()
it('gives us a current user', () => {
})
cy.route([
mockApi.getMe,
mockApi.users
])
cy.route(mockApi('GetMe', {
currentUser: {
firstName: 'brian',
lastName: 'mann',
}
}))
// cy.route('POST', '/graphql', (req, next) => {
// const { body } = req
// if (!body) {
// return next()
// }
// const { operationName, query, variables } = payload
// })
// cypress-graphql-mocks
export const configure = (options) => {
const schema = makeExecutableSchema({
typeDefs: schemaAsSDL(options.schema)
})
return (alias, operation) => {
return {
url: options.url,
onRequest: (req, next) => {
const { body } = req
if (!body) {
return next()
}
const { operationName, query, variables } = body
req.alias = alias
// req.reply(200, {
// })
return graphql({
schema,
source: query,
variableValues: variables,
operationName,
rootValue: getRootValue<AllOperations>(
operation,
operationName,
variables
)
})
.then(req.reply)
}
}
}
}
-------------------------------------------
import gqlMocks from 'cypress-graphql-mocks'
const mockApi = gqlMocks.configure({
method: 'POST',
url: '/graphql',
name: 'api',
schema: '...',
mocks: '...',
})
// mockApi.getMe =
export const mockApi
-------------------------------------
/// <reference types="cypress" />
import { graphql, IntrospectionQuery } from "graphql";
import { buildClientSchema, printSchema } from "graphql";
import {
makeExecutableSchema,
addMockFunctionsToSchema,
IMocks
} from "graphql-tools";
interface MockGraphQLOptions<AllOperations extends Record<string, any>> {
schema: string | string[] | IntrospectionQuery;
name?: string;
mocks?: IMocks;
endpoint?: string;
operations?: Partial<AllOperations>;
}
interface SetOperationsOpts<AllOperations> {
name?: string;
endpoint?: string;
operations?: Partial<AllOperations>;
}
interface GQLRequestPayload<AllOperations extends Record<string, any>> {
operationName: Extract<keyof AllOperations, string>;
query: string;
variables: any;
}
declare global {
namespace Cypress {
interface Chainable {
mockGraphql<AllOperations = any>(
options?: MockGraphQLOptions<AllOperations>
): Cypress.Chainable;
mockGraphqlOps<AllOperations = any>(
options?: SetOperationsOpts<AllOperations>
): Cypress.Chainable;
}
}
}
/**
* Adds a .mockGraphql() and .mockGraphqlOps() methods to the cypress chain.
*
* The .mockGraphql should be called in the cypress "before" or "beforeEach" block
* config to setup the server.
*
* By default, it will use the /graphql endpoint, but this can be changed
* depending on the server implementation
*
* It takes an "operations" object, representing the named operations
* of the GraphQL server. This is combined with the "mocks" option,
* to modify the output behavior per test.
*
* The .mockGraphqlOps() allows you to configure the mock responses at a
* more granular level
*
* For example, if we has a query called "UserQuery" and wanted to
* explicitly force a state where a viewer is null (logged out), it would
* look something like:
*
* .mockGraphqlOps({
* operations: {
* UserQuery: {
* viewer: null
* }
* }
* })
*/
Cypress.Commands.add(
"mockGraphql",
<AllOperations extends Record<string, any>>(
options: MockGraphQLOptions<AllOperations>
) => {
const { endpoint = "/graphql", operations = {}, mocks = {} } = options;
const schema = makeExecutableSchema({
typeDefs: schemaAsSDL(options.schema)
});
addMockFunctionsToSchema({
schema,
mocks
});
let currentOps = operations;
cy.on("window:before:load", win => {
const originalFetch = win.fetch;
function fetch(input: RequestInfo, init?: RequestInit) {
if (typeof input !== "string") {
throw new Error(
"Currently only support fetch(url, options), saw fetch(Request)"
);
}
if (input.indexOf(endpoint) !== -1 && init && init.method === "POST") {
const payload: GQLRequestPayload<AllOperations> = JSON.parse(
init.body as string
);
const { operationName, query, variables } = payload;
return graphql({
schema,
source: query,
variableValues: variables,
operationName,
rootValue: getRootValue<AllOperations>(
currentOps,
operationName,
variables
)
}).then((data: any) => new Response(JSON.stringify(data)));
}
return originalFetch(input, init);
}
cy.stub(win, "fetch", fetch).as("fetchStub");
});
//
cy.wrap({
setOperations: (newOperations: Partial<AllOperations>) => {
currentOps = {
...(operations as object),
...(newOperations as object)
};
}
}).as(getAlias(options));
}
);
Cypress.Commands.add(
"mockGraphqlOps",
<AllOperations extends Record<string, any>>(
options: SetOperationsOpts<AllOperations>
) => {
cy.get(`@${getAlias(options)}`).invoke(
"setOperations" as any,
options.operations || {}
);
}
);
const getAlias = ({ name, endpoint }: { name?: string; endpoint?: string }) => {
if (name || endpoint) {
return `mockGraphqlOps:${name || endpoint}`;
}
return "mockGraphqlOps";
};
// Takes the schema either as the full .graphql file (string) or
// the introspection object.
function schemaAsSDL(schema: string | string[] | IntrospectionQuery) {
if (typeof schema === "string" || Array.isArray(schema)) {
return schema;
}
return printSchema(buildClientSchema(schema));
}
function getRootValue<AllOperations>(
operations: Partial<AllOperations>,
operationName: Extract<keyof AllOperations, string>,
variables: any
) {
if (!operationName || !operations[operationName]) {
return {};
}
const op = operations[operationName];
if (typeof op === "function") {
return op(variables);
}
return op;
}
Interop with RxJs would be nice for websockets: