如何在Cypress中为特定的GraphQL请求添加别名?

如何在Cypress中为特定的GraphQL请求添加别名?,graphql,cypress,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('@po

在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

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

翻阅了文档之后,似乎没有一种方法可以使用请求正文中的
operationName
来别名graphQL端点。我还将
操作名
(黄色箭头)作为响应标题中的自定义属性返回;但是,我也没有找到一种方法来使用它来别名特定的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');
cy.server();
cy.route({
    method: 'POST',
    url: '/graphql',
    headers: {
        'operation-name': 'editIpo',
    },
}).as('graphql');
cy.wait('graphql');
此方法不起作用,因为
graphqlEditIpo
别名是在运行时注册的,因此,我收到的错误如下

CypressError:cy.wait()找不到“@graphqlEditIpo”的注册别名。可用别名为:“ipoInitial,graphql”

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

cy.server();
cy.route({
    method: 'POST',
    url: '/graphql',
    onResponse(reqObj) {
        if (reqObj.request.body.operationName === 'editIpo') {
            cy.wrap('editIpo').as('graphqlEditIpo');
        }
    },
});
cy.wait('@graphqlEditIpo');
cy.server();
cy.route({
    method: 'POST',
    url: '/graphql',
    headers: {
        'operation-name': 'editIpo',
    },
}).as('graphql');
cy.wait('graphql');
此方法不起作用,因为cy.route的options对象中的headers属性实际上意味着根据接受存根路由的响应头。在这里,我试图用它来识别我的特定graphQL查询,这显然是行不通的


这就引出了我的问题:如何在Cypress中别名特定的graphQL查询/突变?我错过了什么吗?

如果“等待”而不是“别名”本身是主要目的,这是我迄今为止遇到的最简单的方法,是通过对常规graphql请求使用别名,然后针对新创建的别名进行递归函数调用“wait”,直到找到要查找的特定graphql操作。 e、 g

这当然有它的警告,在您的环境中可能有效,也可能无效。但它对我们有用

我希望Cypress将来能够以一种不那么烦人的方式实现这一点


注:我想感谢我从何处获得灵感,但它似乎在网络空间中消失了。

因为我遇到了同样的问题,但我没有找到解决这个问题的真正方法,所以我结合了不同的选择,创建了一个解决我问题的变通方法。希望这也能帮助其他人

我并不是真的“等待”请求发生,而是基于
**/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);
                });
        };
    };
};
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));
});
在测试文件中

使用对象绑定数组,其中键是operationName,值是resolve函数

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

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);
                });
        };
    };
};
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));
});
使用了来自

的blob命令这对我来说很有用

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

请记住尽可能使用唯一的名称编写查询,因为操作名称依赖于它。

我使用了其中一些代码示例,但不得不稍微更改它,以便将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请求的方法。我们使用cypress Cumber预处理器,因此在/cypress/integration/common/中有一个common.js文件,我们可以在任何特征文件之前调用beforebeforeach钩子

我在这里尝试了这些解决方案,但无法找到稳定的解决方案,因为在我们的应用程序中,许多GraphQL请求在某些操作的同时被触发

最后,我将每个GraphQL请求存储在一个名为GraphQL_accumulator的全局对象中,每个请求都有一个时间戳

然后,使用cypress命令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})
    }
  });
});
我必须从FormData中提取queryName,因为我们在请求头中还没有键operationName,但这将是使用该键的地方

commands.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);
                });
        };
    };
};
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通过在/cypress/support/index.js中添加这些设置来管理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将等待最新的目标请求,该请求将在调用后存储


希望这能有所帮助。

我们的用例涉及一个页面上的多个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()
,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 5.6.0中新增的):

文件:


我希望这有帮助

6.0.0中引入的
intercept
API通过请求处理函数支持这一点。我曾经