I am facing some troubles with my GraphQL optimistic rendering. I'm using Apollo Client, such as:
const history = useHistory();
const [addItem] = useMutation(ADD_ITEM_QUERY);
const onClick = () => {
addItem({
variables: {
id: '16b1119a-9d96-4bc8-96a3-12df349a0c4d',
name: 'Foo Bar'
},
optimisticResponse: {
addItem {
id: '16b1119a-9d96-4bc8-96a3-12df349a0c4d',
name: 'Foo Bar',
__typename: 'Item'
}
},
update: () => {
// Update item cached queries
history.push(`/items);
}
});
};
The issue comes from the redirect. As far as I understand it, the update function is called twice: a first time with the optimisticResponse, and a second time with the network response (which should normally be the same).
However, let's consider the following scenario:
I create a new item,
I receive the optimistic response, and I'm redirected to the list of items,
I click on "Add an item" to be redirected to the form
I receive the server response, hence I'm redirected again to the list.
What is the correct way to prevent this double redirection?
I thought checking the current cache value. If the value is already the latest one, I won't apply the redirect. Not sure if there is a better way? How do you proceed such a scenario?
You should call history.push outside of the update function. You can do
addItem({...}).then(() => history.push('/items')).
Related
I have a short question.
Does inertia render asynch?
I realized, as soon I delete a DB - Entry, while I connect to new Nav-Link direct afterwards (Inertia.onStart)- which redirects me on another Page, the changes (onSuccess) wont be showed up.
Inertia.post('data-delete', {
id: this.meeeh.data[index].id,
}, {
preserveScroll: true,
onBefore: () => {
window.Toast.confirm('Delete?');
},
onStart: (visit) => {
window.Toast.load('Delete...');
},
onSuccess: (page) => {
return Promise.all([
window.Toast.success(page.props.toast),
/** Wont show after click another Link in Navbar */
])
},
onError: (errors) => {
window.Toast.error(errors);
}
});
How does it come, I have to wait until the process is Finished - otherwise my Page is not working correctly?
Not sure if I understand what you're looking for.
onSuccess runs immediately after the post request has finished AND in successful. It is completely separated from other links and it's purpose (if your returning a Promise from it) is to delay the execution of the onFinish handler.
From the docs:
It's also possible to return a promise from the onSuccess() and
onError() callbacks. This will delay the "finish" event until the
promise has resolved.
I also believe there's some problem in your code: Promise.all should receive an array os Promises and I'm pretty sure window.Toast.success(page.props.toast) isn't returning one, is it?
So... chances are that your Promise.all is never resolving.
In the view_submission type I set ack to clear the stack like this:
await submissionAck({ response_action: 'clear' } as any)
First question - why do I have to cast it to any? Without it code throws error
Argument of type '{ response_action: "clear"; }' is not assignable to parameter of type '(ViewUpdateResponseAction & void) | (ViewPushResponseAction & void) | (ViewClearResponseAction & void) | (ViewErrorsResponseAction & void) | undefined'.Type '{ response_action: "clear"; }' is not assignable to type 'ViewClearResponseAction & void'.
Type '{ response_action: "clear"; }' is not assignable to type 'void'.
Second question - the stack seems not to be cleared. When I submit modal for the first time it's okay, but if I try next time it throws:
[ERROR] bolt-app { Error: The receiver's `ack` function was called multiple times.
at ack (/home/ec2-user/metrics/node_modules/#slack/bolt/src/ExpressReceiver.ts:147:17)
at /home/ec2-user/metrics/app/actions.ts:43:17
at Generator.next (<anonymous>)
at /home/ec2-user/metrics/app/actions.ts:11:71
at new Promise (<anonymous>)
at __awaiter (/home/ec2-user/metrics/app/actions.ts:7:12)
at app.view (/home/ec2-user/metrics/app/actions.ts:40:70)
at process_1.processMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/App.ts:660:19)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:36:12)
at next (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:28:21)
at Array.<anonymous> (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/builtin.ts:201:11)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:27:47)
at next (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:28:21)
at Array.exports.onlyViewActions (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/builtin.ts:110:11)
at invokeMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:27:47)
at Object.processMiddleware (/home/ec2-user/metrics/node_modules/#slack/bolt/src/middleware/process.ts:39:10) code: 'slack_bolt_receiver_ack_multiple_error' }
Any ideas? That's how I call these views: (by the way 3rd question - why do I have to cast body to BlockAction? Otherwise it throws error that trigger_id doesn't exists)
app.action('modify', async ({ body, ack }) => {
await ack()
await authenticate(body.team.id, async (customer: Customer) => {
await app.client.views.open({
trigger_id: (body as BlockAction).trigger_id,
token: 'token',
view: modificationModal,
})
app.view(
{
type: 'view_submission',
callback_id: 'yay',
},
async ({ body: submissionBody, ack: submissionAck, view }) => {
const receivedValues = submissionBody.view.state.values
await submissionAck({ response_action: 'clear' } as any)
},
)
})
})
I know that in the documentation stands:
view() requires a callback_id of type string or RegExp.
but that doesn't tell me much. What is that string? Is that a function? What should it do?
Sorry for noobish question and thanks for help!
I'll try to answer these in the reverse order, because I think that might make the most sense.
What is that string? Is that a function? What should it do? (referring to app.view())
When you create a Modal, you typically create it with a callback_id. You can see a description for that property in the documentation for a view payload.
That sentence is trying to say this is how you'd listen for a view submission for a view that was created with callback_id set to "some_callback_id":
app.view('some_callback_id', async () => {
/* listener logic goes here */
})
Note: You could also use a regular expression if you wanted the same function to handle view submissions for many views - views whose callback_ids all follow the same pattern. But the regular expression is a pretty advanced case that I don't think we should worry about it for now.
To create a Modal, you use the views.open method, and that's where you'll set the callback_id in the first place. I'm going to suggest an improvement. All Web API methods are available inside a listener as methods on the client argument. Then you don't need to worry about adding the token. Here's an example of using this:
// Add the `client` argument
app.action('modify', async ({ body, ack, client }) => {
await ack()
await authenticate(body.team.id, async (customer: Customer) => {
// Remove `app.`
await client.views.open({
// Let's come back to this cast later
trigger_id: (body as BlockAction).trigger_id,
// Not sure what's in modificationModal, but to illustrate, I used a literal
view: {
// *** Setting the callback_id ***
callback_id: 'modify_submission',
title: {
type: 'plain_text',
text: 'Modify something'
},
blocks: [{ /* add your blocks here */ }],
},
})
})
})
Next, don't handle a view submission inside another listener. When you do that, each time the outer listener runs, you're (re)registering the view submission listener to run. So the first time it will run once, the second time it will run twice, the third time it will run three times. This explains why the stacktrace is telling you that ack() was called multiple times. Instead, just handle the view submission outside that listener. The "linking" information between the two interactions is the callback_id. Building off the previous example:
// Further down in the same file, at the same level as the previous code
// *** Using the callback_id we set previously ***
app.view('modify_submission', async ({ body, ack, view }) => {
const receivedValues = body.view.state.values
// Let's come back to this cast later
await ack({ response_action: 'clear' } as any)
})
Okay this should all work, but now let's talk about the casts. When you handle an action with app.action(), the body argument is typed as BlockAction | InteractiveMessage | DialogSubmitAction. Within these interfaces, BlockAction and InteractiveMessage do have a trigger_id property, but DialogSubmitAction does not. So as far as TypeScript is concerned, it can't be sure the property body.trigger_id exists. You might know that the action you're handling is a BlockAction (let's assume it is), but TypeScript does not! However, Bolt was built to allow users to give TypeScript some more information by using a generic parameter. Here's a part of the first example, modified to use the generic parameter.
import { BlockAction } from '#slack/bolt';
app.action<BlockAction>('modify', async ({ body, ack, client }) => {
// `body` is now typed as a BlockAction, and therefore body.trigger_id is a string
});
It's a pretty similar story for app.view() and generic parameters. This time, the body argument is of type ViewSubmitAction | ViewClosedAction. Using a clear response action doesn't make sense for a ViewClosedAction, so we need to constrain the type once again. That's right, the generic parameter is hooked up to more than just the type of body, it can actually change (constrain) any of the listener arguments! In this case, the generic parameter changes the type of ack().
import { ViewSubmitAction } from '#slack/bolt';
app.view<ViewSubmitAction>('modify_submission', async ({ body, ack, view }) => {
// No errors now
await ack({ response_action: 'clear' });
});
Final note: The way you wrote the view submission handler with a constraints object ({ type: 'view_submission', callback_id: 'yay' } does seem like you've given TypeScript enough information to constrain the types of the listener arguments. That actually would work for app.action({ type: 'block_actions', ... }, ...) because we defined ActionConstraints to be generic. This is an area where Bolt could be improved and all it would take is making ViewConstraints generic in the same way.
We're trying to get some events/messages to post when a user exits a chatbot window (or the site) (or a welcome message), but so far the events are not firing.
I can see within Inspector tools:
Screen Shot 2020-02-18 at 3 15 39 PM
Various activities/conversations are created, the chatbot works, but no welcome/exit events are triggered.
The code we're using is nearly if not identical to documentation code here: https://github.com/microsoft/BotFramework-WebChat/blob/master/docs/WELCOME_MESSAGE.md
and here: How to handle user leaving conversation
I have a function that fires when the window is closed, as follows:
const store = window.WebChat.createStore( {}, ( { dispatch } ) => next => async action => {
return next( action );});
window.addEventListener( 'sendEventActivity', ( { data } ) => {
store.dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'user_event',
value: {
name: 'end_conversation',
value: 'user ended conversation'
},
text: 'The user has left the conversation.'
}
})
});
function exitEvent(){
const eventSendActivity = new Event( 'sendEventActivity' );
eventSendActivity.data = 'User left conversation';
window.dispatchEvent( eventSendActivity );
console.log('Exit Event Submitted (hopefully)');
}
exitEvent();
I have tried other variations, defining the store earlier, above render chat, after render chat, sending welcome messages from various locations and at various times but can't seem to get it to send.
We are using https://cdn.botframework.com/botframework-webchat/latest/webchat.js
Any idea what the issue might be? Not sure where we are going wrong or why it's not firing - copying in theory known to be working code straight into our code doesn't seem to do the trick.
Thanks in advance and please let me know if I have failed to include any necessary details- new to chatbot and do not post much on github. Many thanks,
EDIT:
I was able to marry the aforementioned code and code from here: https://github.com/microsoft/BotFramework-WebChat/issues/2120#issuecomment-516056614 in order to achieve what I wanted. I'll post below in case it helps anyone else...
const store = window.WebChat.createStore({}, ({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join'
}
});
}
return next(action);
});
window.addEventListener( 'sendEventActivity', ( { data } ) => {
store.dispatch( {
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/exit'
}
} );
} );
document.getElementById("action_menu_btn").addEventListener( 'click', function() {
const eventSendActivity = new Event( 'sendEventActivity' );
eventSendActivity.data = 'User left conversation';
window.dispatchEvent( eventSendActivity );
console.log('End Converstaion Event Fired');
});
Cheers!
I failed to mention this in the other post (I'll update it), but the reason the code works is because of the window.onbeforeunload() function. Without it, the window closes before any code can finish executing. The result being no event is created, is caught by a listener, nor is sent via the Web Chat store to the bot.
Here, using the above, refreshing the page produces the "User left conversation" activity.
Also, something to note, any function you create and pass thru like you have with exitEvent() is going to run as soon as the page loads. Take the following code which gets the user's location via the browser (placed just before the closing </script> tag). As you can see, it's loading even before Web Chat. If you are wanting a function to run according to some activity passed from the bot, then utilize either the store's actions (i.e. DIRECT_LINE/INCOMING_ACTIVITY, or some other) or via the available middleware.
let geoLoc = async () => {
await navigator.geolocation.getCurrentPosition(position => {
console.log('Latitude: ', position.coords.latitude);
console.log('Longitude: ', position.coords.longitude);
});
}
geoLoc();
Regarding a welcome message, you have two options. Either send as an activity from your bot (reference this sample) or initiate an event on your page after some initial activity is received (reference this sample).
Lastly, I would recommend getting the code working as-is before tinkering with it. This usually trips me up, so thought I'd pass it along.
Hope of help!
I have stuck with Cypress fixtures. Can't intercept an XHR request with SSR and navigation routing.
cypress/integration/page.js:
const fetch = require("unfetch")
describe("/about", () => {
beforeEach(() => {
cy.visit("/", { // Visit home page to trigger SSR
onBeforeLoad (win) {
win.fetch = fetch // replace fetch with xhr implementation
},
})
})
it("Has a correct title", () => {
cy.server()
cy.fixture("about").then(about => {
// about object is correct here, like {title: "About+"}
cy.route("GET", "http://localhost:8080/api/documents/url", about) // Not sure where .route should be
cy.get(".main > :nth-child(1) > a").click() // Navigate to the /about page
cy.route("GET", "http://localhost:8080/api/documents/url", about) // Tried both ways
// This hits my server API without stubbing, getting {title: "About"}
cy.title().should("eq", "About+") // About != About+
})
})
})
cypress/fixtures/about.json:
{"title": "About+"}
I see an XHR request (type=xhr) in Dev Tools and it doesn't use the above about stub object but hits real API instead. Why? Double checked URL and method – 100% the same. Can it be that route is coupled to visit and ignores click-based routing?!
Rechecking this once again, I've found a solution. Let me share the details for everyone interested:
1) I use Next.js which is an excellent tool for SSR but it doesn't allow you to disable server-side rendering (yet) according to this and this issues.
2) You can use Cypress with SSR pages but, in this way, you're limited to testing real HTML. Which means you have to either couple tests to real data (not good in most cases) or stub the database itself (slow). In general, you want to stub HTTP requests.
3) Cypress can't stub fetch requests and mocking fetch with XHR-based implementation was trickier than I thought.
First you need to:
// cypress/integration/your-test.js
Cypress.on('window:before:load', (win) => {
delete win.fetch
})
Then:
// pages/your-page.js
Entry.getInitialProps = async function() {
window.fetch = require("unfetch").default
...
}
Other combinations of delete & update code lines I tried didn't yield positive results. For example, when I had window.fetch = line in the test file it didn't work and fetch.toString() gave "native code". Not sure why, no time to explore further.
Axios solves the above but I don't like to bloat my bundle with extra stuff. You can inject XHR-based fetch for tests only.
4) The most important missing piece. You need to wait for route.
it("Has a correct title", () => {
cy.visit("/")
cy.server()
cy.route("GET", "http://localhost:8080/api/documents/url/about", {title: "About+"}).as("about")
cy.get("[href='/about']").click()
cy.wait("#about") // !!!
cy.get("h1").contains("About+")
})
In Cypress, it is well-documented that you can alias specific network requests, which you can then "wait" on. This is especially helpful if you want to do something in Cypress after a specific network request has fired and finished.
Example below from Cypress documentation:
cy.server()
cy.route('POST', '**/users').as('postUser') // ALIASING OCCURS HERE
cy.visit('/users')
cy.get('#first-name').type('Julius{enter}')
cy.wait('#postUser')
However, since I'm using GraphQL in my app, aliasing no longer becomes a straightforward affair. This is because all GraphQL queries share one endpoint /graphql.
Despite it not being possible to differentiate between different graphQL queries using the url endpoint alone, it is possible to differentiate graphQL queries using operationName (refer to following image).
Having dug through the documentation, there doesn't appear to be a way to alias graphQL endpoints using operationName from the request body. I'm also returning the operationName (yellow arrow) as a custom property in my response header; however, I haven't managed to find a way to use it to alias specific graphQL queries either.
FAILED METHOD 1: This method attempts to use the purple arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
onResponse(reqObj) {
if (reqObj.request.body.operationName === 'editIpo') {
cy.wrap('editIpo').as('graphqlEditIpo');
}
},
});
cy.wait('#graphqlEditIpo');
This method doesn't work since the graphqlEditIpo alias is registered at runtime and as such, the error I receive is as follows.
CypressError: cy.wait() could not find a registered alias for: '#graphqlEditIpo'. Available aliases are: 'ipoInitial, graphql'.
FAILED METHOD 2: This method attempts to use the yellow arrow shown in image.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
headers: {
'operation-name': 'editIpo',
},
}).as('graphql');
cy.wait('graphql');
This method doesn't work because the headers property in the options object for cy.route is actually meant to accept response headers for stubbed routes per the docs. Here, I'm trying to use it to identify my specific graphQL query, which obviously won't work.
Which leads me to my question: How can I alias specific graphQL queries/mutations in Cypress? Have I missed something?
The intercept API introduced in 6.0.0 supports this via the request handler function. I used it in my code like so:
cy.intercept('POST', '/graphql', req => {
if (req.body.operationName === 'queryName') {
req.alias = 'queryName';
} else if (req.body.operationName === 'mutationName') {
req.alias = 'mutationName';
} else if (...) {
...
}
});
Where queryName and mutationName are the names of your GQL operations. You can add an additional condition for each request that you would like to alias. You can then wait for them like so:
// Wait on single request
cy.wait('#mutationName');
// Wait on multiple requests.
// Useful if several requests are fired at once, for example on page load.
cy.wait(['#queryName, #mutationName',...]);
The docs have a similar example here: https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-requests.
This works for me!
Cypress.Commands.add('waitForGraph', operationName => {
const GRAPH_URL = '/api/v2/graph/';
cy.route('POST', GRAPH_URL).as("graphqlRequest");
//This will capture every request
cy.wait('#graphqlRequest').then(({ request }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (request.body.operationName !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
Just remember to write your queries with unique names as posible, because the operation name relies on it.
If 'waiting' and not 'aliasing' in itself is the main purpose, the easiest way to do this, as I've encountered thus far, is by aliasing the general graphql requests and then making a recursive function call to 'wait' targeting the newly created alias until you find the specific graphql operation you were looking for.
e.g.
Cypress.Commands.add('waitFor', operationName => {
cy.wait('#graphqlRequest').then(({ request }) => {
if (request.body.operationName !== operationName) {
return cy.waitFor(operationName)
}
})
})
This of course have its caveats and may or may not work in your context. But it works for us.
I hope Cypress enables this in a less hacky way in the future.
PS. I want to give credit to where I got the inspiration to this from, but it seemt to be lost in cyberspace.
Since I was having the same issue and I did not find a real solution for this problem I combined different options and created a workaround that solves my problem. Hopefully this can help someone else too.
I do not really 'wait' for the request to be happen but I catch them all, based on **/graphql url and match the operationName in the request. On a match a function will be executed with the data as parameter. In this function the tests can be defined.
graphQLResponse.js
export const onGraphQLResponse = (resolvers, args) => {
resolvers.forEach((n) => {
const operationName = Object.keys(n).shift();
const nextFn = n[operationName];
if (args.request.body.operationName === operationName) {
handleGraphQLResponse(nextFn)(args.response)(operationName);
}
});
};
const handleGraphQLResponse = (next) => {
return (response) => {
const responseBody = Cypress._.get(response, "body");
return async (alias) => {
await Cypress.Blob.blobToBase64String(responseBody)
.then((blobResponse) => atob(blobResponse))
.then((jsonString) => JSON.parse(jsonString))
.then((jsonResponse) => {
Cypress.log({
name: "wait blob",
displayName: `Wait ${alias}`,
consoleProps: () => {
return jsonResponse.data;
}
}).end();
return jsonResponse.data;
})
.then((data) => {
next(data);
});
};
};
};
In a test file
Bind an array with objects where the key is the operationName and the value is the resolve function.
import { onGraphQLResponse } from "./util/graphQLResponse";
describe("Foo and Bar", function() {
it("Should be able to test GraphQL response data", () => {
cy.server();
cy.route({
method: "POST",
url: "**/graphql",
onResponse: onGraphQLResponse.bind(null, [
{"some operationName": testResponse},
{"some other operationName": testOtherResponse}
])
}).as("graphql");
cy.visit("");
function testResponse(result) {
const foo = result.foo;
expect(foo.label).to.equal("Foo label");
}
function testOtherResponse(result) {
const bar = result.bar;
expect(bar.label).to.equal("Bar label");
}
});
}
Credits
Used the blob command from glebbahmutov.com
This is what you're looking for (New in Cypress 5.6.0):
cy.route2('POST', '/graphql', (req) => {
if (req.body.includes('operationName')) {
req.alias = 'gqlMutation'
}
})
// assert that a matching request has been made
cy.wait('#gqlMutation')
Documentation:
https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request
I hope that this helps!
I used some of these code examples but had to change it slightly to add the onRequest param to the cy.route and also add the date.Now (could add any auto incrementer, open to other solutions on this) to allow multiple calls to the same GraphQL operation name in the same test. Thanks for pointing me in the right direction!
Cypress.Commands.add('waitForGraph', (operationName) => {
const now = Date.now()
let operationNameFromRequest
cy.route({
method: 'POST',
url: '**graphql',
onRequest: (xhr) => {
operationNameFromRequest = xhr.request.body.operationName
},
}).as(`graphqlRequest${now}`)
//This will capture every request
cy.wait(`#graphqlRequest${now}`).then(({ xhr }) => {
// If the captured request doesn't match the operation name of your query
// it will wait again for the next one until it gets matched.
if (operationNameFromRequest !== operationName) {
return cy.waitForGraph(operationName)
}
})
})
to use:
cy.waitForGraph('QueryAllOrganizations').then((xhr) => { ...
This is how I managed to differentiate each GraphQL request. We use cypress-cucumber-preprocessor so we have a common.js file in /cypress/integration/common/ where we can call a before and beforeEach hook which are called before any feature file.
I tried the solutions here, but couldn't come up with something stable since, in our application, many GraphQL requests are triggered at the same time for some actions.
I ended up storing every GraphQL requests in a global object called graphql_accumulator with a timestamp for each occurence.
It was then easier to manage individual request with cypress command should.
common.js:
beforeEach(() => {
for (const query in graphql_accumulator) {
delete graphql_accumulator[query];
}
cy.server();
cy.route({
method: 'POST',
url: '**/graphql',
onResponse(xhr) {
const queryName = xhr.requestBody.get('query').trim().split(/[({ ]/)[1];
if (!(queryName in graphql_accumulator)) graphql_accumulator[queryName] = [];
graphql_accumulator[queryName].push({timeStamp: nowStamp('HHmmssSS'), data: xhr.responseBody.data})
}
});
});
I have to extract the queryName from the FormData since we don't have (yet) the key operationName in the request header, but this would be where you would use this key.
commands.js
Cypress.Commands.add('waitGraphQL', {prevSubject:false}, (queryName) => {
Cypress.log({
displayName: 'wait gql',
consoleProps() {
return {
'graphQL Accumulator': graphql_accumulator
}
}
});
const timeMark = nowStamp('HHmmssSS');
cy.wrap(graphql_accumulator, {log:false}).should('have.property', queryName)
.and("satisfy", responses => responses.some(response => response['timeStamp'] >= timeMark));
});
It's also important to allow cypress to manage GraphQL requests by adding these settings in /cypress/support/index.js:
Cypress.on('window:before:load', win => {
// unfilters incoming GraphQL requests in cypress so we can see them in the UI
// and track them with cy.server; cy.route
win.fetch = null;
win.Blob = null; // Avoid Blob format for GraphQL responses
});
I use it like this:
cy.waitGraphQL('QueryChannelConfigs');
cy.get(button_edit_market).click();
cy.waitGraphQL will wait for the latest target request, the one that will be stored after the call.
Hope this helps.
Somewhere else this method was suggested.
Btw it all becomes a bit easier once you migrate to Cypress v5.x and make use of the new route (route2) method.
Our use case involved multiple GraphQL calls on one page. We had to use a modified version of the responses from above:
Cypress.Commands.add('createGql', operation => {
cy.route({
method: 'POST',
url: '**/graphql',
}).as(operation);
});
Cypress.Commands.add('waitForGql', (operation, nextOperation) => {
cy.wait(`#${operation}`).then(({ request }) => {
if (request.body.operationName !== operation) {
return cy.waitForGql(operation);
}
cy.route({
method: 'POST',
url: '**/graphql',
}).as(nextOperation || 'gqlRequest');
});
});
The issue is that ALL GraphQL requests share the same URL, so once you create a cy.route() for one GraphQL query, Cypress will match all the following GraphQL queries to that. After it matches, we set cy.route() to just a default label of gqlRequest or the next query.
Our test:
cy.get(someSelector)
.should('be.visible')
.type(someText)
.createGql('gqlOperation1')
.waitForGql('gqlOperation1', 'gqlOperation2') // Create next cy.route() for the next query, or it won't match
.get(someSelector2)
.should('be.visible')
.click();
cy.waitForGql('gqlOperation2')
.get(someSelector3)
.should('be.visible')
.click();