Как я могу создать псевдоним для конкретных запросов GraphQL в Cypress?

В 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 в Cypress?

Покопавшись в документации, похоже, не существует способа псевдонима конечных точек 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? Я что-то упустил?

Есть ли решение с использованием библиотеки засовывать в качестве альтернативы cy.route(), о чем говорил Глеб Бахмутов здесь. По сути, какой-то перехватчик решит множество проблем при тестировании сетевых запросов.

Richard Matsen 19.12.2018 22:11

У Cypress теперь есть документация по поддержке graphql, включая некоторые служебные функции: [ссылка] docs.cypress.io/guides/testing-strategies/…

Darren G 27.07.2021 21:13
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
19
2
6 664
9

Ответы 9

Если «ожидание», а не «псевдоним» само по себе является основной целью, самый простой способ сделать это, как я уже встречал, - это сопоставление общих запросов 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') о том, где это сделать.

Diego Ortiz 12.05.2021 06:08

Поскольку у меня была такая же проблема, и я не нашел реального решения для этой проблемы, я объединил разные варианты и создал обходной путь, который решает мою проблему. Надеюсь, это поможет и кому-то другому.

На самом деле я не «жду» выполнения запроса, но я ловлю их всех на основе 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

HRVHackers 12.03.2020 00:16

У меня это работает!

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'), но он ждет только один раз. Любая подсказка, почему? Он вызывает рекурсивно, но во второй раз возвращается.

Diego Ortiz 12.05.2021 06:24

Я использовал некоторые из этих примеров кода, но мне пришлось немного изменить его, чтобы добавить параметр 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 отлично работал при извлечении данных. Спасибо!
M. Lee 27.02.2020 18:17

Наш вариант использования включал несколько вызовов 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. Не могли бы вы помочь, пожалуйста

Haya D 17.12.2020 14:05

@HayaD вам нужно перейти на cypress 6 и использовать команду cy.intercept. Оформить заказ cypress docs

Antony Fuentes 18.12.2020 07:24

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').shou‌​ld('include', 'library'); Вот как бы я это делал? Я не хочу заглушать / издеваться, просто убедитесь, что то, что я получаю от api, соответствует пользовательскому интерфейсу, но сначала мне нужно получить правильный ответ graphql, и я застрял даже на этом этапе. Я пробовал примеры из документации и вашей, но безрезультатно.

Jo-Anne 25.11.2020 17:27

@ Jo-Anne нет необходимости утверждать на req.body.operationName, если вы создали псевдоним запроса на основе operationName. Например, предположим, что ваш запрос GQL называется library, ваш обработчик intercept будет иметь условие: if (req.body.operationName === 'library') { req.alias = 'library' }, и тогда вы можете дождаться выполнения этого конкретного запроса с помощью cy.wait('@library').

Travis Lloyd 25.11.2020 19:36

А, хорошо, а потом, я полагаю, я могу использовать этот псевдоним @library в этом случае, чтобы утверждать в теле ответа, чтобы сравнить с тем, что я вижу в DOM теперь, когда у меня есть правильный?

Jo-Anne 25.11.2020 19:47

Например, я хотел бы получить часть этого ответа от library, а затем сделать что-то вроде этого const sportName = response.body.data.library.sports.name, а затем утверждать на основе этого.

Jo-Anne 25.11.2020 19:57

@ Jo-Anne, которая работала бы как cy.wait('@library').its('response.body.data.library.sports.n‌​ame').should(...).

Travis Lloyd 25.11.2020 20:19

Я не понимаю, как мы можем ждать нескольких запросов, если этот перехват выполняется только один раз.

Diego Ortiz 12.05.2021 06:27

Другие вопросы по теме