【问题标题】:How can I alias specific GraphQL requests in Cypress?如何在赛普拉斯中为特定的 GraphQL 请求设置别名?
【发布时间】:2020-11-25 00:26:25
【问题描述】:

在 Cypress 中,您可以通过 well-documented 为特定的网络请求设置别名,然后您可以“等待”这些请求。如果您想在特定网络请求触发并完成后在 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

尽管单独使用 url 端点无法区分不同的 graphQL 查询,但可以使用 operationName 区分 graphQL 查询(请参阅下图)。

仔细阅读文档后,似乎没有办法使用请求正文中的 operationName 为 graphQL 端点设置别名。我还将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() 找不到“@graphqlEditIpo”的注册别名。可用的别名有:'ipoInitial, graphql'。

失败的方法 2:此方法尝试使用图中所示的黄色箭头。

cy.server();
cy.route({
    method: 'POST',
    url: '/graphql',
    headers: {
        'operation-name': 'editIpo',
    },
}).as('graphql');
cy.wait('graphql');

此方法不起作用,因为 cy.route 的选项对象中的 headers 属性实际上是为了接受每个 docs 的存根路由的响应标头。在这里,我试图用它来识别我的特定 graphQL 查询,这显然是行不通的。

这让我想到了我的问题:如何在 Cypress 中为特定的 graphQL 查询/突变设置别名?我错过了什么吗?

【问题讨论】:

  • 如 Gleb Bahmutov here 所述,是否有使用 nock 库替代 cy.route() 的解决方案。本质上,某种拦截器可以在测试网络请求时解决很多问题。
  • 赛普拉斯现在有关于如何支持 graphql 的文档,包括一些实用功能:[链接]docs.cypress.io/guides/testing-strategies/…

标签: graphql cypress


【解决方案1】:

6.0.0 中引入的intercept API 通过请求处理函数支持这一点。我在我的代码中这样使用它:

cy.intercept('POST', '/graphql', req => {
  if (req.body.operationName === 'queryName') {
    req.alias = 'queryName';
  } else if (req.body.operationName === 'mutationName') {
    req.alias = 'mutationName';
  } else if (...) {
    ...
  }
});

其中queryNamemutationName 是您的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 获得的内容与 ui 匹配,但我需要首先获得正确的 graphql 响应,即使在这一点上我也会卡住。我已经尝试了文档和您的示例,但无济于事。
  • @Jo-Anne 如果您已根据operationName 为请求设置别名,则无需在req.body.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(...) 一样工作。
【解决方案2】:

这对我有用!

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() 方法已弃用在赛普拉斯 6.8.0 中,我将其替换为:cy.intercept('POST', '/graphql').as('gqlRequest'),但它只等待一次。任何线索为什么?它递归调用,但第二次超时。
【解决方案3】:

如果'等待'而不是'别名'本身是主要目的,那么最简单的方法,正如我迄今为止遇到的那样,是通过别名一般的graphql请求,然后对'进行递归函数调用等待'以新创建的别名为目标,直到找到您正在寻找的特定 graphql 操作。 例如

Cypress.Commands.add('waitFor', operationName => {
  cy.wait('@graphqlRequest').then(({ request }) => {
    if (request.body.operationName !== operationName) {
      return cy.waitFor(operationName)
    }
  })
})

这当然有一些注意事项,在您的情况下可能有效,也可能无效。但它对我们有用。

我希望赛普拉斯在未来以一种不那么老套的方式实现这一点。

PS。我想感谢我从哪里获得灵感,但它似乎在网络空间中消失了。

【讨论】:

  • 我认为解释你如何为一般的 graphql 请求起别名很重要,我试过这个但没有成功cy.intercept('POST', '/graphql').as('gqlRequest') 可能是关于在哪里做的。
【解决方案4】:

由于我遇到了同样的问题,但我没有找到真正解决这个问题的方法,所以我结合了不同的选项并创建了一个解决方法来解决我的问题。希望这也可以帮助其他人。

我并没有真正“等待”请求发生,但我根据**/graphql url 捕获了所有请求,并匹配请求中的 operationName。在匹配时,将使用数据作为参数执行函数。在这个函数中可以定义测试。

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");
        }
    });
}

学分

使用来自glebbahmutov.com的blob命令

【讨论】:

  • 可以进一步增强它以使用模拟吗?例如仅当 operationName = "OurOperation" 然后 response: ourFixture.json,否则从 API 返回非模拟响应? @斯坦
【解决方案5】:

这就是你要找的东西(赛普拉斯 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”的消息。我用和你一样的代码,我的cypress版本是5.2你能帮忙吗
  • @HayaD 你需要升级到 cypress 6 并使用cy.intercept 命令。结帐赛普拉斯文档
【解决方案6】:

我使用了其中的一些代码示例,但必须对其稍作更改以将 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) => { ...

【讨论】:

    【解决方案7】:

    这就是我设法区分每个 GraphQL 请求的方法。我们使用 cypress-cucumber-preprocessor,所以我们在 /cypress/integration/common/ 中有一个 common.js 文件,我们可以在其中调用 before 和 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})
        }
      });
    });
    

    我必须从 FormData 中提取 queryName,因为我们在请求标头中(还没有)键 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/support/index.js 中添加这些设置,允许 cypress 管理 GraphQL 请求也很重要:

    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 将等待最新的目标请求,即在调用后存储的请求。

    希望这会有所帮助。

    【讨论】:

      【解决方案8】:

      其他地方this method was suggested

      顺便说一句,一旦你使用migrate to Cypress v5.x and make use of the new route (route2) 方法,一切都会变得容易一些。

      【讨论】:

        【解决方案9】:

        我们的用例涉及一个页面上的多个 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,因此一旦您为一个 GraphQL 查询创建了cy.route(),赛普拉斯将匹配以下所有 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();
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-11-11
          • 2020-08-17
          • 2020-05-09
          • 2022-11-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多