Playwright 1.45.0 使用记录的 HAR 文件发布多部分 FormData 时出现问题

问题描述 投票:0回答:1

我们使用

routeFromHAR(har, {update: true})
来记录网络流量,同时测试进行一系列 REST 调用(GET 和 POST)的应用程序。我们正在进行记录,以便稍后使用模拟数据离线运行测试(即
{update: false}
)。

除了使用 multipart/form-data 的 POST,即通过使用 method:POST 和 body:formData 调用 fetch() 来创建的 POST 之外,这一切都很好。

录制时没有问题,实际上录制的HAR文件包含解码后的FormData,类似于

"postData": {
  "mimeType": "multipart/form-data; boundary=----WebKitFormBoundaryodbswUmZkXgG8qgY",
  "text": "------WebKitFormBoundaryodbswUmZkXgG8qgY\r\nContent-Disposition: form-data; name=\"Param1\"\r\n\r\nValue1\r\n------WebKitFormBoundaryodbswUmZkXgG8qgY--\r\n",
  ...
}

这似乎是正确的。

对记录的 HAR 文件运行相同的测试时会出现问题(即

routeFromHAR(har, {update: false}
)。 在这种情况下,似乎 Playwright 在 HAR 文件中找不到匹配的条目,导致测试失败。

我认为问题在于 Playwright 没有考虑到浏览器每次都会生成一个新的“边界”这一事实。 也就是说,针对记录的 HAR 文件运行时使用的边界将与记录 HAR 文件时使用的边界不同,因此 postData 将不会严格匹配

为了向自己证明这一点,我修改了传递给 fetch() 的数据,以便浏览器生成的边界与记录的 HAR 文件中的边界相匹配。 当我这样做时,Playwright 会在 HAR 文件中找到该条目,并且测试成功。

例如:

// Normally we would fetch() with this:
let request = new Request(url, {
  'method': 'POST',
  'body': formData,
  ...
});

// But now, rebuild the request using boundary stored in recorded HAR file
let body: string = new TextDecoder('utf-8').decode(await request.arrayBuffer());
const boundary = body.substring(2, 40); // get boundary from browser
const boundaryInHar = '----WebKitFormBoundaryodbswUmZkXgG8qgY'; // copied from HAR
body = body.replaceAll(boundary, boundaryInHar);

request = new Request(url, {
  'method': 'POST',
  'body': body,
  'headers': {
    'Content-Type': `multipart/form-data; boundary=${boundaryInHar}`,
  },
  ...
});

Playwright 团队能否确认使用记录的 HAR 文件发布多部分 FormData 存在问题?

更新 - 最小测试用例

  1. 从 playwright 示例 https://playwright.dev/docs/intro 开始,修改 tests/example.spec.ts 如下:

import {
  test,
  expect
} from '@playwright/test';

test('upload file', async({
  page
}) => {

  // When POSTing to this url, get response from test.har
  await page.context().routeFromHAR('test.har', {
    url: 'https://httpbin.org/post',
    update: false,
    updateContent: 'embed'
  });

  // Run the test (upload a file and click submit)
  await page.goto('http://localhost:80/');
  const [fileChooser] = await Promise.all([
    page.waitForEvent('filechooser'),
    page.locator('#file').click()
  ]);
  await fileChooser.setFiles('sample.txt');
  await page.locator('button').click();

  // If the response is served from the test.har file,
  // we should get a "success" response.
  await expect(page.locator('#status')).toContainText('success');

});

  1. 将sample.txt和test.har放在顶级目录中。

{
  "log": {
    "version": "1.2",
    "creator": {
      "name": "Playwright",
      "version": "1.45.0"
    },
    "browser": {
      "name": "chromium",
      "version": "127.0.6533.17"
    },
    "entries": [
      {
        "startedDateTime": "2024-06-27T17:25:48.829Z",
        "time": 2.478,
        "request": {
          "method": "POST",
          "url": "https://httpbin.org/post",
          "httpVersion": "HTTP/2.0",
          "cookies": [],
          "headers": [
            { "name": ":authority", "value": "httpbin.org" },
            { "name": ":method", "value": "POST" },
            { "name": ":path", "value": "/post" },
            { "name": ":scheme", "value": "https" },
            { "name": "accept", "value": "*/*" },
            { "name": "accept-encoding", "value": "gzip, deflate, br, zstd" },
            { "name": "accept-language", "value": "en-US" },
            { "name": "content-length", "value": "44" },
            { "name": "content-type", "value": "multipart/form-data; boundary=----WebKitFormBoundary0123456789ABCDEF" },
            { "name": "origin", "value": "http://localhost" },
            { "name": "priority", "value": "u=1, i" },
            { "name": "referer", "value": "http://localhost/" },
            { "name": "sec-ch-ua", "value": "\"Not)A;Brand\";v=\"99\", \"HeadlessChrome\";v=\"127\", \"Chromium\";v=\"127\"" },
            { "name": "sec-ch-ua-mobile", "value": "?0" },
            { "name": "sec-ch-ua-platform", "value": "\"Windows\"" },
            { "name": "sec-fetch-dest", "value": "empty" },
            { "name": "sec-fetch-mode", "value": "cors" },
            { "name": "sec-fetch-site", "value": "cross-site" },
            { "name": "user-agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.17 Safari/537.36" }
          ],
          "queryString": [],
          "headersSize": -1,
          "bodySize": -1,
          "postData": {
            "mimeType": "multipart/form-data; boundary=----WebKitFormBoundary0123456789ABCDEF",
            "text": "------WebKitFormBoundary0123456789ABCDEF--\r\n",
            "params": []
          }
        },
        "response": {
          "status": 200,
          "statusText": "",
          "httpVersion": "HTTP/2.0",
          "cookies": [],
          "headers": [
            { "name": "access-control-allow-credentials", "value": "true" },
            { "name": "access-control-allow-origin", "value": "http://localhost" },
            { "name": "content-length", "value": "1000" },
            { "name": "content-type", "value": "application/json" },
            { "name": "date", "value": "Thu, 27 Jun 2024 17:25:49 GMT" },
            { "name": "server", "value": "gunicorn/19.9.0" }
          ],
          "content": {
            "size": -1,
            "mimeType": "application/json",
            "text": "{\n  \"success\": {} }\n"
          },
          "headersSize": -1,
          "bodySize": -1,
          "redirectURL": ""
        },
        "cache": {},
        "timings": { "send": -1, "wait": -1, "receive": 2.478 }
      }
    ]
  }
}

  1. 在 localhost:80 处,放置此 index.html 文件:

<html>

<body>
  <div class="container">
    <h1>Multipart File Upload</h1>
    <form id="form" enctype="multipart/form-data">
      <div class="input-group">
        <label for="files">Select files</label>
        <input id="file" type="file" multiple />
      </div>
      <button class="submit-btn" type="submit">Upload</button>
    </form>
    <div id="status"></div>
  </div>
  <script type="text/javascript">
    const form = document.querySelector('form');

    form.addEventListener('submit', async(event) => {
      event.preventDefault();

      // This is the request we would normally use.  But it does not
      // work when running against the test.har file, because Playwright
      // will not find a match in that file -- due to the fact that the
      // boundaries won't match (fetch uses random boundaries each time).
      let request = new Request('https://httpbin.org/post', {
        method: 'POST',
        body: new FormData(form)
      });

      // ### BEGIN WORKAROUND
      // As a clunky workaround, rebuild request using a fixed boundary
      // To demonstrate the Playwright bug, commenet out this workaround
      const body = new TextDecoder('utf-8').decode(await request.arrayBuffer());
      const currBoundary = body.substring(2, 40);
      const newBoundary = '----WebKitFormBoundary0123456789ABCDEF';
      request = new Request(request, {
        headers: {
          'Content-Type': `multipart/form-data; boundary=${newBoundary}`,
        },
        body: body.replaceAll(currBoundary, newBoundary)
      });
      // ### END WORKAROUND

      // Now make the request
      const response = await fetch(request);
      const json = await response.json();
      document.querySelector('#status').innerHTML = JSON.stringify(json, null, 2);
    });
  </script>
</body>

</html>

  1. 运行测试 (
    npx playwright test tests/example.spec.ts
    )。

如果没有在index.html 中对边界进行硬编码的“WORKAROUND”,测试将会失败。 如果我们让浏览器选择自己的(随机)边界,那么在针对 test.har 文件运行时测试将始终失败。

post mocking multipartform-data playwright har
1个回答
-1
投票

我也面临类似的问题。你有解决这个问题吗?

© www.soinside.com 2019 - 2024. All rights reserved.