В Cypress именно хорошо задокументированы можно назначать псевдонимом для определенных сетевых запросов, которые затем можно «подождать». Это особенно полезно, если вы хотите что-то сделать в Cypress после того, как определенный сетевой запрос был запущен и завершен.
Пример ниже из документации Cypress:
cy.server()
cy.route('POST', '**/users').as('postUser') // ALIASING OCCURS HERE
cy.visit('/users')
cy.get('#first-name').type('Julius{enter}')
cy.wait('@postUser')
Однако, поскольку я использую GraphQL в своем приложении, создание псевдонимов больше не становится простой задачей. Это связано с тем, что все запросы GraphQL используют одну конечную точку /graphql.
Несмотря на то, что невозможно различить разные запросы graphQL, используя только конечную точку url, является позволяет различать запросы graphQL с помощью operationName (см. Следующее изображение).
Покопавшись в документации, похоже, не существует способа псевдонима конечных точек graphQL с использованием operationName из тела запроса. Я также возвращаю operationName (желтая стрелка) в качестве настраиваемого свойства в заголовке ответа; однако мне также не удалось найти способ использовать его для псевдонима конкретных запросов graphQL.
НЕПРАВИЛЬНЫЙ СПОСОБ 1: Этот метод пытается использовать фиолетовую стрелку, показанную на изображении.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
onResponse(reqObj) {
if (reqObj.request.body.operationName === 'editIpo') {
cy.wrap('editIpo').as('graphqlEditIpo');
}
},
});
cy.wait('@graphqlEditIpo');
Этот метод не работает, поскольку псевдоним graphqlEditIpo зарегистрирован во время выполнения, и поэтому я получаю следующую ошибку.
CypressError: cy.wait() could not find a registered alias for: '@graphqlEditIpo'. Available aliases are: 'ipoInitial, graphql'.
НЕПРАВИЛЬНЫЙ СПОСОБ 2: Этот метод пытается использовать желтую стрелку, показанную на изображении.
cy.server();
cy.route({
method: 'POST',
url: '/graphql',
headers: {
'operation-name': 'editIpo',
},
}).as('graphql');
cy.wait('graphql');
Этот метод не работает, потому что свойство headers в объекте параметров для cy.route фактически предназначено для приема заголовков ответов для заглушенных маршрутов на документы. Здесь я пытаюсь использовать его для идентификации моего конкретного запроса graphQL, который, очевидно, не сработает.
Это приводит меня к моему вопросу: как я могу использовать псевдонимы для конкретных запросов / мутаций graphQL в Cypress? Я что-то упустил?
У Cypress теперь есть документация по поддержке graphql, включая некоторые служебные функции: [ссылка] docs.cypress.io/guides/testing-strategies/…


Если «ожидание», а не «псевдоним» само по себе является основной целью, самый простой способ сделать это, как я уже встречал, - это сопоставление общих запросов graphql с последующим вызовом рекурсивной функции для нацеливания на «ожидание». только что созданный псевдоним, пока вы не найдете конкретную операцию graphql, которую искали. например
Cypress.Commands.add('waitFor', operationName => {
cy.wait('@graphqlRequest').then(({ request }) => {
if (request.body.operationName !== operationName) {
return cy.waitFor(operationName)
}
})
})
Это, конечно, имеет свои недостатки и может работать, а может и не работать в вашем контексте. Но у нас это работает.
Я надеюсь, что Cypress позволит в будущем сделать это менее опасным способом.
PS. Я хочу отдать должное тому, откуда я черпал вдохновение, но, похоже, это потеряно в киберпространстве.
Я думаю, что важно объяснить, как вы использовали псевдоним для общих запросов graphql, я пробовал это без успеха, возможно, cy.intercept('POST', '/graphql').as('gqlRequest') о том, где это сделать.
Поскольку у меня была такая же проблема, и я не нашел реального решения для этой проблемы, я объединил разные варианты и создал обходной путь, который решает мою проблему. Надеюсь, это поможет и кому-то другому.
На самом деле я не «жду» выполнения запроса, но я ловлю их всех на основе URL-адреса **/graphql и сопоставляю имя операции в запросе. При совпадении будет выполнена функция с данными в качестве параметра. В этой функции можно определить тесты.
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);
});
};
};
};
В тестовом файле
Свяжите массив с объектами, где ключ - это имя_операции, а значение - функция разрешения.
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");
}
});
}
Кредиты
Использовал команду blob из glebbahmutov.com
Можно ли еще улучшить использование насмешек? например только если operationName = "OurOperation", то response: ourFixture.json, в противном случае вернуть немокационный ответ от API? @Stan
У меня это работает!
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)
}
})
})
Просто не забудьте писать свои запросы с уникальными именами, насколько это возможно, потому что имя операции зависит от этого.
Поскольку метод route () устарел. В cypress 6.8.0 я заменил его на: cy.intercept('POST', '/graphql').as('gqlRequest'), но он ждет только один раз. Любая подсказка, почему? Он вызывает рекурсивно, но во второй раз возвращается.
Я использовал некоторые из этих примеров кода, но мне пришлось немного изменить его, чтобы добавить параметр onRequest в cy.route, а также добавить дату. Теперь (можно добавить любой автоматический инкрементер, открытый для других решений по этому поводу), чтобы разрешить несколько вызовов то же имя операции GraphQL в том же тесте. Спасибо, что указали мне правильное направление!
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)
}
})
})
использовать:
cy.waitForGraph('QueryAllOrganizations').then((xhr) => { ...
Вот как мне удалось различить каждый запрос GraphQL. Мы используем кипарис-огурец-препроцессор, поэтому у нас есть файл common.js в / кипарисовик / интеграция / общий /, где мы можем вызвать ловушки до и beforeEach, которые вызываются перед любым файлом функций.
Я пробовал решения здесь, но не смог придумать что-то стабильное, поскольку в нашем приложении многие запросы GraphQL запускаются одновременно для некоторых действий.
В итоге я хранил все запросы GraphQL в глобальном объекте под названием graphql_accumulator с отметкой времени для каждого случая.
Тогда было проще управлять индивидуальным запросом с помощью команды cypress должен.
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})
}
});
});
Мне нужно извлечь queryName из FormData, поскольку у нас (пока) нет ключа operationName в заголовке запроса, но вы могли бы использовать этот ключ именно здесь.
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));
});
Также важно разрешить cypress управлять запросами GraphQL, добавив эти настройки в /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
});
Я использую это так:
cy.waitGraphQL('QueryChannelConfigs');
cy.get(button_edit_market).click();
cy.waitGraphQL будет ждать последнего целевого запроса, который будет сохранен после вызова.
Надеюсь это поможет.
win.Blob = null отлично работал при извлечении данных. Спасибо!
Наш вариант использования включал несколько вызовов GraphQL на одной странице. Нам пришлось использовать модифицированную версию ответов сверху:
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');
});
});
Проблема в том, что ВСЕ запросы GraphQL используют один и тот же URL-адрес, поэтому, как только вы создадите cy.route() для одного запроса GraphQL, Cypress сопоставит с ним все следующие запросы GraphQL. После совпадения мы устанавливаем для cy.route() только метку по умолчанию gqlRequest или следующий запрос.
Наш тест:
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();
Где-то еще этот метод был предложен.
Между прочим, все станет немного проще, если вы воспользуетесь методом перейти на Cypress v5.x и использовать новый маршрут (route2).
Это то, что вы ищете (новое в 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')
Документация: https://docs.cypress.io/api/commands/route2.html#Waiting-on-a-request
Надеюсь, это поможет!
Привет, я пробовал этот подход, но у меня он не работает. Это не срабатывает с сообщением, что я никогда не использовал псевдоним "gqlMutation". Я использую тот же код, что и вы, моя версия для кипариса - 5.2. Не могли бы вы помочь, пожалуйста
@HayaD вам нужно перейти на cypress 6 и использовать команду cy.intercept. Оформить заказ cypress docs
API intercept, представленный в 6.0.0, поддерживает это через функцию обработчика запросов. Я использовал это в своем коде так:
cy.intercept('POST', '/graphql', req => {
if (req.body.operationName === 'queryName') {
req.alias = 'queryName';
} else if (req.body.operationName === 'mutationName') {
req.alias = 'mutationName';
} else if (...) {
...
}
});
Где queryName и mutationName - названия ваших операций GQL. Вы можете добавить дополнительное условие для каждого запроса, которому вы хотите присвоить псевдоним. Затем вы можете подождать их так:
// 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',...]);
В документации есть аналогичный пример здесь: https://docs.cypress.io/api/commands/intercept.html#Aliasing-individual-requests.
Вопрос. Мне это нравится, и я использовал именно то, что указано выше. Я вижу, что мои три запроса / graphql "завершены", хотя мое ожидание всегда истекает. Я делаю cy.wait('@operationName').its('req.body.operationName').should('include', 'library'); Вот как бы я это делал? Я не хочу заглушать / издеваться, просто убедитесь, что то, что я получаю от api, соответствует пользовательскому интерфейсу, но сначала мне нужно получить правильный ответ graphql, и я застрял даже на этом этапе. Я пробовал примеры из документации и вашей, но безрезультатно.
@ Jo-Anne нет необходимости утверждать на req.body.operationName, если вы создали псевдоним запроса на основе operationName. Например, предположим, что ваш запрос GQL называется library, ваш обработчик intercept будет иметь условие: if (req.body.operationName === 'library') { req.alias = 'library' }, и тогда вы можете дождаться выполнения этого конкретного запроса с помощью cy.wait('@library').
А, хорошо, а потом, я полагаю, я могу использовать этот псевдоним @library в этом случае, чтобы утверждать в теле ответа, чтобы сравнить с тем, что я вижу в DOM теперь, когда у меня есть правильный?
Например, я хотел бы получить часть этого ответа от library, а затем сделать что-то вроде этого const sportName = response.body.data.library.sports.name, а затем утверждать на основе этого.
@ Jo-Anne, которая работала бы как cy.wait('@library').its('response.body.data.library.sports.name').should(...).
Я не понимаю, как мы можем ждать нескольких запросов, если этот перехват выполняется только один раз.
Есть ли решение с использованием библиотеки засовывать в качестве альтернативы
cy.route(), о чем говорил Глеб Бахмутов здесь. По сути, какой-то перехватчик решит множество проблем при тестировании сетевых запросов.