我们使用
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 存在问题?
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');
});
{
"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 }
}
]
}
}
<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>
npx playwright test tests/example.spec.ts
)。如果没有在index.html 中对边界进行硬编码的“WORKAROUND”,测试将会失败。 如果我们让浏览器选择自己的(随机)边界,那么在针对 test.har 文件运行时测试将始终失败。
我也面临类似的问题。你有解决这个问题吗?