ftmocks-utils 1.3.4 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/package.json +1 -1
- package/src/common-utils.js +79 -0
- package/src/file-utils.js +39 -5
- package/src/match-utils.js +120 -1
- package/src/playwright-utils.js +29 -5
package/README.md
CHANGED
|
@@ -1,2 +1,89 @@
|
|
|
1
1
|
# ftmocks-utils
|
|
2
|
+
|
|
2
3
|
Util functions for FtMocks
|
|
4
|
+
|
|
5
|
+
## Usage: initiatePlaywrightRoutes
|
|
6
|
+
|
|
7
|
+
`initiatePlaywrightRoutes` sets up Playwright network route mocks for your tests.
|
|
8
|
+
|
|
9
|
+
### Example Test
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { test, expect } from "@playwright/test";
|
|
13
|
+
import { initiatePlaywrightRoutes } from "ftmocks-utils";
|
|
14
|
+
|
|
15
|
+
test("Sample test case", async ({ page }) => {
|
|
16
|
+
// Initiate Playwright routes with custom directories and patterns
|
|
17
|
+
await initiatePlaywrightRoutes(
|
|
18
|
+
page,
|
|
19
|
+
{
|
|
20
|
+
MOCK_DIR: "../ftmocks",
|
|
21
|
+
FALLBACK_DIR: "../public",
|
|
22
|
+
},
|
|
23
|
+
"Sample test case",
|
|
24
|
+
"**/*" // Pattern(s) to intercept; you can use a string or array of patterns
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
await page.goto("https://example-test.com/");
|
|
28
|
+
|
|
29
|
+
// Now your requests will be mocked as per your ftmocks setup
|
|
30
|
+
// Add your test steps and assertions here
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Parameters**:
|
|
35
|
+
|
|
36
|
+
- `page`: Playwright page object.
|
|
37
|
+
- `options`: Object with configuration. At minimum, provide `MOCK_DIR` (required). `FALLBACK_DIR` is optional.
|
|
38
|
+
- `testName`: (string) Name of this test, so ftmocks can find the right mock data.
|
|
39
|
+
- `patterns`: (string or array) Glob patterns for requests to intercept.
|
|
40
|
+
|
|
41
|
+
Make sure your `MOCK_DIR` points to the directory where your FtMocks records are saved.
|
|
42
|
+
|
|
43
|
+
See more API documentation at [ftmocks.com](https://ftmocks.com) or in the main FtMocks repository.
|
|
44
|
+
|
|
45
|
+
## Usage: recordPlaywrightRoutes
|
|
46
|
+
|
|
47
|
+
`recordPlaywrightRoutes` allows you to record network requests and responses from a Playwright test session and save them as FtMocks mocks. This is useful for setting up new mocks or updating existing ones with actual traffic.
|
|
48
|
+
|
|
49
|
+
### Example Usage
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { test } from "@playwright/test";
|
|
53
|
+
import { recordPlaywrightRoutes } from "ftmocks-utils";
|
|
54
|
+
|
|
55
|
+
test("Record API interactions", async ({ page }) => {
|
|
56
|
+
await recordPlaywrightRoutes(
|
|
57
|
+
page,
|
|
58
|
+
{
|
|
59
|
+
MOCK_DIR: "../ftmocks",
|
|
60
|
+
FALLBACK_DIR: "../public",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
testName: "Recorded test",
|
|
64
|
+
mockPath: "**/*", // Intercept all requests by default
|
|
65
|
+
pattern: "^/api/.*", // Only record requests matching this regex pattern (e.g., API endpoints)
|
|
66
|
+
avoidDuplicatesInTheTest: true, // Skip duplicates within a single test recording
|
|
67
|
+
avoidDuplicatesWithDefaultMocks: true, // Skip duplicates with default mocks
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await page.goto("https://your-app-under-test.com/");
|
|
72
|
+
// Interact with your page as needed; API requests will be recorded
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Parameters**:
|
|
77
|
+
|
|
78
|
+
- `page`: Playwright page object.
|
|
79
|
+
- `ftmocksConifg`: Object, must contain at minimum `MOCK_DIR`. `FALLBACK_DIR` is optional.
|
|
80
|
+
- `config`: Object containing recording options:
|
|
81
|
+
- `testName`: (string) Name of the test, used for saving the mock data.
|
|
82
|
+
- `mockPath`: (string|array) Glob pattern(s) for requests to intercept.
|
|
83
|
+
- `pattern`: (string) Regex string; only requests matching this will be recorded.
|
|
84
|
+
- `avoidDuplicatesInTheTest`: (boolean) Skip duplicate entries during this run.
|
|
85
|
+
- `avoidDuplicatesWithDefaultMocks`: (boolean) Skip recording if identical default mocks are present.
|
|
86
|
+
|
|
87
|
+
After running the test, FtMocks-compatible mock files will be saved to the specified folder for easy reuse.
|
|
88
|
+
|
|
89
|
+
See more API documentation and advanced usage at [ftmocks.com](https://ftmocks.com).
|
package/package.json
CHANGED
package/src/common-utils.js
CHANGED
|
@@ -134,6 +134,82 @@ const getTestByName = async (ftmocksConifg, testName) => {
|
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Creates a map from an array of objects using `id` property as key.
|
|
139
|
+
* @param {Array<object>} arr - The array of objects to map.
|
|
140
|
+
* @returns {Object} Map of id -> object
|
|
141
|
+
*/
|
|
142
|
+
function createIdMap(arr) {
|
|
143
|
+
if (!Array.isArray(arr)) return {};
|
|
144
|
+
return arr.reduce((acc, obj) => {
|
|
145
|
+
if (obj && obj.id !== undefined && obj.id !== null) {
|
|
146
|
+
acc[obj.id] = obj;
|
|
147
|
+
}
|
|
148
|
+
return acc;
|
|
149
|
+
}, {});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a map where the key is in the format '[method]-[pathname]' and the value is the mock's id.
|
|
154
|
+
* @param {Array<object>} mocks - Array of mock objects. Each should have fileContent with method and url, and an id.
|
|
155
|
+
* @returns {Object} Map of '[method]-[pathname]' -> mock.id
|
|
156
|
+
*/
|
|
157
|
+
function createMethodPathnameIdMap(mocks) {
|
|
158
|
+
if (!Array.isArray(mocks)) return {};
|
|
159
|
+
const map = {};
|
|
160
|
+
for (const mock of mocks) {
|
|
161
|
+
if (
|
|
162
|
+
mock &&
|
|
163
|
+
mock.id !== undefined &&
|
|
164
|
+
mock.fileContent &&
|
|
165
|
+
typeof mock.fileContent.method === "string" &&
|
|
166
|
+
typeof mock.fileContent.url === "string"
|
|
167
|
+
) {
|
|
168
|
+
const urlObj = (() => {
|
|
169
|
+
try {
|
|
170
|
+
return new URL(mock.fileContent.url, "http://localhost");
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// fallback: treat as path only
|
|
173
|
+
return { pathname: mock.fileContent.url };
|
|
174
|
+
}
|
|
175
|
+
})();
|
|
176
|
+
const key = `${mock.fileContent.method.toUpperCase()}-${urlObj.pathname}`;
|
|
177
|
+
if (!map[key]) {
|
|
178
|
+
map[key] = [mock.id];
|
|
179
|
+
} else {
|
|
180
|
+
map[key].push(mock.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return map;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Returns a unique key for the given mock in the format '[METHOD]-[pathname]'.
|
|
189
|
+
* If the URL can't be parsed, uses the raw URL as pathname.
|
|
190
|
+
* @param {object} mock - The mock object with at least fileContent.method and fileContent.url
|
|
191
|
+
* @returns {string|null} The key as '[METHOD]-[pathname]', or null if invalid input
|
|
192
|
+
*/
|
|
193
|
+
function getMockKey(options) {
|
|
194
|
+
if (
|
|
195
|
+
!options ||
|
|
196
|
+
!options.method ||
|
|
197
|
+
!options.url ||
|
|
198
|
+
typeof options.method !== "string" ||
|
|
199
|
+
typeof options.url !== "string"
|
|
200
|
+
) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
let pathname;
|
|
204
|
+
try {
|
|
205
|
+
const urlObj = new URL(options.url, "http://localhost");
|
|
206
|
+
pathname = urlObj.pathname;
|
|
207
|
+
} catch (_) {
|
|
208
|
+
pathname = options.url;
|
|
209
|
+
}
|
|
210
|
+
return `${options.method.toUpperCase()}-${pathname}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
137
213
|
module.exports = {
|
|
138
214
|
charDifference,
|
|
139
215
|
nameToFolder,
|
|
@@ -146,4 +222,7 @@ module.exports = {
|
|
|
146
222
|
getHeaders,
|
|
147
223
|
countFilesInDirectory,
|
|
148
224
|
getTestByName,
|
|
225
|
+
createIdMap,
|
|
226
|
+
createMethodPathnameIdMap,
|
|
227
|
+
getMockKey,
|
|
149
228
|
};
|
package/src/file-utils.js
CHANGED
|
@@ -7,8 +7,43 @@ const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
|
|
|
7
7
|
|
|
8
8
|
// Check if URL contains file extension like .js, .png, .css etc
|
|
9
9
|
const fileExtMatch = urlObj.pathname.match(/\.[a-zA-Z0-9]+$/);
|
|
10
|
-
if
|
|
11
|
-
|
|
10
|
+
// Check mime type if extension is not present
|
|
11
|
+
let fileExt = null;
|
|
12
|
+
if (!fileExtMatch) {
|
|
13
|
+
// Try to get extension from content-type header
|
|
14
|
+
const response = await route.fetch();
|
|
15
|
+
const contentType = response.headers()["content-type"];
|
|
16
|
+
if (contentType) {
|
|
17
|
+
// Map common mime types to extensions
|
|
18
|
+
const mimeToExt = {
|
|
19
|
+
"image/png": ".png",
|
|
20
|
+
"image/jpeg": ".jpg",
|
|
21
|
+
"image/jpg": ".jpg",
|
|
22
|
+
"image/gif": ".gif",
|
|
23
|
+
"image/webp": ".webp",
|
|
24
|
+
"image/svg+xml": ".svg",
|
|
25
|
+
"application/javascript": ".js",
|
|
26
|
+
"application/x-javascript": ".js",
|
|
27
|
+
"text/javascript": ".js",
|
|
28
|
+
"text/css": ".css",
|
|
29
|
+
"font/woff": ".woff",
|
|
30
|
+
"font/woff2": ".woff2",
|
|
31
|
+
"font/ttf": ".ttf",
|
|
32
|
+
"audio/mpeg": ".mp3",
|
|
33
|
+
"audio/wav": ".wav",
|
|
34
|
+
"video/mp4": ".mp4",
|
|
35
|
+
"application/pdf": ".pdf",
|
|
36
|
+
};
|
|
37
|
+
// Remove any charset, etc.
|
|
38
|
+
const mime = contentType.split(";")[0].trim();
|
|
39
|
+
if (mimeToExt[mime]) {
|
|
40
|
+
fileExt = mimeToExt[mime];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
fileExt = fileExtMatch[0];
|
|
45
|
+
}
|
|
46
|
+
if (fileExt) {
|
|
12
47
|
// Create directory path matching URL structure
|
|
13
48
|
const dirPath = path.join(
|
|
14
49
|
getMockDir(ftmocksConifg),
|
|
@@ -21,15 +56,14 @@ const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
|
|
|
21
56
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
22
57
|
|
|
23
58
|
// Save file with original name
|
|
24
|
-
const fileName =
|
|
59
|
+
const fileName = `${id}${fileExt}`;
|
|
25
60
|
const filePath = path.join(dirPath, fileName);
|
|
26
61
|
|
|
27
62
|
const response = await route.fetch();
|
|
28
63
|
const buffer = await response.body();
|
|
29
64
|
fs.writeFileSync(filePath, buffer);
|
|
30
65
|
|
|
31
|
-
|
|
32
|
-
return true;
|
|
66
|
+
return fileName;
|
|
33
67
|
}
|
|
34
68
|
return false;
|
|
35
69
|
};
|
package/src/match-utils.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
const { compareMockToFetchRequest } = require("./compare-utils");
|
|
2
2
|
const { getCompareRankMockToFetchRequest } = require("./rank-compare-utils");
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
getMockDir,
|
|
5
|
+
nameToFolder,
|
|
6
|
+
createIdMap,
|
|
7
|
+
createMethodPathnameIdMap,
|
|
8
|
+
getMockKey,
|
|
9
|
+
} = require("./common-utils");
|
|
4
10
|
const path = require("path");
|
|
5
11
|
const fs = require("fs");
|
|
6
12
|
|
|
@@ -14,11 +20,23 @@ function getMatchingMockData({
|
|
|
14
20
|
mode,
|
|
15
21
|
}) {
|
|
16
22
|
let served = false;
|
|
23
|
+
const testMockIdMap = createIdMap(testMockData);
|
|
17
24
|
let matchedMocks =
|
|
18
25
|
testMockData?.filter((mock) => {
|
|
19
26
|
if (mock.fileContent.waitForPrevious && !served) {
|
|
20
27
|
return false;
|
|
21
28
|
}
|
|
29
|
+
if (mock.fileContent.waitFor) {
|
|
30
|
+
const waitForMocks = mock.fileContent.waitFor.filter(
|
|
31
|
+
(waitForMockId) => {
|
|
32
|
+
const waitForMock = testMockIdMap[waitForMockId];
|
|
33
|
+
return waitForMock && !waitForMock.fileContent.served;
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
if (waitForMocks.length > 0) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
served = mock.fileContent.served;
|
|
23
41
|
return compareMockToFetchRequest(mock, { url, options });
|
|
24
42
|
}) || [];
|
|
@@ -89,6 +107,107 @@ function getMatchingMockData({
|
|
|
89
107
|
return foundMock ? foundMock.fileContent : null;
|
|
90
108
|
}
|
|
91
109
|
|
|
110
|
+
function getMatchingMockDataV2({
|
|
111
|
+
testMockData,
|
|
112
|
+
defaultMockData,
|
|
113
|
+
url,
|
|
114
|
+
options,
|
|
115
|
+
testConfig,
|
|
116
|
+
testName,
|
|
117
|
+
mode,
|
|
118
|
+
}) {
|
|
119
|
+
const testMockIdMap = createIdMap(testMockData);
|
|
120
|
+
const testMockMethodPathnameMap = createMethodPathnameIdMap(testMockData);
|
|
121
|
+
const defaultMockIdMap = createIdMap(defaultMockData);
|
|
122
|
+
const defaultMockMethodPathnameMap =
|
|
123
|
+
createMethodPathnameIdMap(defaultMockData);
|
|
124
|
+
const key = getMockKey(options);
|
|
125
|
+
let matchedMocks = testMockMethodPathnameMap[key] || [];
|
|
126
|
+
let defaultMatchedMocks = [];
|
|
127
|
+
const nonWaitForMocks = matchedMocks.filter((mockId) => {
|
|
128
|
+
const mock = testMockIdMap[mockId];
|
|
129
|
+
if (mock.fileContent.waitFor) {
|
|
130
|
+
const waitForMocks = mock.fileContent.waitFor.filter((waitForMockId) => {
|
|
131
|
+
const waitForMock = testMockIdMap[waitForMockId];
|
|
132
|
+
return waitForMock && !waitForMock.fileContent.served;
|
|
133
|
+
});
|
|
134
|
+
if (waitForMocks.length > 0) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
const nonServedMocks = nonWaitForMocks.filter((mockId) => {
|
|
141
|
+
const mock = testMockIdMap[mockId];
|
|
142
|
+
return !mock.fileContent.served;
|
|
143
|
+
});
|
|
144
|
+
let foundMock = null;
|
|
145
|
+
if (nonServedMocks.length > 0) {
|
|
146
|
+
foundMock = testMockIdMap[nonServedMocks[0]];
|
|
147
|
+
} else if (nonWaitForMocks.length > 0) {
|
|
148
|
+
foundMock = testMockIdMap[nonWaitForMocks[nonWaitForMocks.length - 1]];
|
|
149
|
+
}
|
|
150
|
+
if (!foundMock) {
|
|
151
|
+
defaultMatchedMocks = defaultMockMethodPathnameMap[key] || [];
|
|
152
|
+
if (defaultMatchedMocks.length > 0) {
|
|
153
|
+
foundMock = defaultMockIdMap[defaultMatchedMocks[0]];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!foundMock && mode !== "strict") {
|
|
157
|
+
const mockRanks = {};
|
|
158
|
+
matchedMocks.forEach((mockId) => {
|
|
159
|
+
const mock = testMockIdMap[mockId];
|
|
160
|
+
const rank = getCompareRankMockToFetchRequest(mock, {
|
|
161
|
+
url,
|
|
162
|
+
options,
|
|
163
|
+
});
|
|
164
|
+
if (rank > 0) {
|
|
165
|
+
mockRanks[mock.id] = rank;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
defaultMatchedMocks.forEach((mockId) => {
|
|
169
|
+
const mock = defaultMockIdMap[mockId];
|
|
170
|
+
const rank = getCompareRankMockToFetchRequest(mock, {
|
|
171
|
+
url,
|
|
172
|
+
options,
|
|
173
|
+
});
|
|
174
|
+
if (rank > 0) {
|
|
175
|
+
mockRanks[mock.id] = rank;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// Sort by rank to find the best match
|
|
179
|
+
const sortedRanks = Object.entries(mockRanks).sort((a, b) => a[1] - b[1]);
|
|
180
|
+
if (sortedRanks.length > 0) {
|
|
181
|
+
const bestMockId = sortedRanks?.[0]?.[0];
|
|
182
|
+
if (bestMockId) {
|
|
183
|
+
foundMock = testMockIdMap[bestMockId] || defaultMockIdMap[bestMockId];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// updating stats to mock file
|
|
188
|
+
if (foundMock) {
|
|
189
|
+
let mockFilePath = path.join(
|
|
190
|
+
getMockDir(testConfig),
|
|
191
|
+
`test_${nameToFolder(testName)}`,
|
|
192
|
+
`mock_${foundMock.id}.json`
|
|
193
|
+
);
|
|
194
|
+
if (!fs.existsSync(mockFilePath)) {
|
|
195
|
+
mockFilePath = path.join(
|
|
196
|
+
getMockDir(testConfig),
|
|
197
|
+
"defaultMocks",
|
|
198
|
+
`mock_${foundMock.id}.json`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
foundMock.fileContent.served = true;
|
|
202
|
+
fs.writeFileSync(
|
|
203
|
+
mockFilePath,
|
|
204
|
+
JSON.stringify(foundMock.fileContent, null, 2)
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return foundMock ? foundMock.fileContent : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
92
210
|
module.exports = {
|
|
93
211
|
getMatchingMockData,
|
|
212
|
+
getMatchingMockDataV2,
|
|
94
213
|
};
|
package/src/playwright-utils.js
CHANGED
|
@@ -14,6 +14,8 @@ const crypto = require("crypto");
|
|
|
14
14
|
const fs = require("fs");
|
|
15
15
|
|
|
16
16
|
let logger = null;
|
|
17
|
+
const DEFAULT_EXCLUDED_HEADERS =
|
|
18
|
+
"cookie,set-cookie,authorization,www-authenticate";
|
|
17
19
|
|
|
18
20
|
async function initiatePlaywrightRoutes(
|
|
19
21
|
page,
|
|
@@ -204,6 +206,22 @@ async function initiatePlaywrightRoutes(
|
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
const excludeHeaders = (headers, ftmocksConifg) => {
|
|
210
|
+
const excludedHeaders =
|
|
211
|
+
ftmocksConifg.EXCLUDED_HEADERS || DEFAULT_EXCLUDED_HEADERS;
|
|
212
|
+
if (!excludedHeaders) {
|
|
213
|
+
return headers;
|
|
214
|
+
}
|
|
215
|
+
excludedHeaders.split(",").forEach((header) => {
|
|
216
|
+
Object.keys(headers).forEach((key) => {
|
|
217
|
+
if (key.toLowerCase() === header.toLowerCase()) {
|
|
218
|
+
delete headers[key];
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
return headers;
|
|
223
|
+
};
|
|
224
|
+
|
|
207
225
|
async function recordPlaywrightRoutes(
|
|
208
226
|
page,
|
|
209
227
|
ftmocksConifg,
|
|
@@ -231,16 +249,21 @@ async function recordPlaywrightRoutes(
|
|
|
231
249
|
await createTest(ftmocksConifg, config.testName);
|
|
232
250
|
}
|
|
233
251
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
252
|
+
const fileName = await saveIfItIsFile(
|
|
253
|
+
route,
|
|
254
|
+
config.testName,
|
|
255
|
+
ftmocksConifg
|
|
256
|
+
);
|
|
237
257
|
|
|
238
258
|
const mockData = {
|
|
239
259
|
url: urlObj.pathname + urlObj.search,
|
|
240
260
|
time: new Date().toString(),
|
|
241
261
|
method: route.request().method(),
|
|
242
262
|
request: {
|
|
243
|
-
headers:
|
|
263
|
+
headers: excludeHeaders(
|
|
264
|
+
await route.request().headers(),
|
|
265
|
+
ftmocksConifg
|
|
266
|
+
),
|
|
244
267
|
queryString: Array.from(urlObj.searchParams.entries()).map(
|
|
245
268
|
([name, value]) => ({
|
|
246
269
|
name,
|
|
@@ -255,9 +278,10 @@ async function recordPlaywrightRoutes(
|
|
|
255
278
|
: null,
|
|
256
279
|
},
|
|
257
280
|
response: {
|
|
281
|
+
file: fileName,
|
|
258
282
|
status: (await route.fetch()).status(),
|
|
259
283
|
headers: (await route.fetch()).headers(),
|
|
260
|
-
content: await (await route.fetch()).text(),
|
|
284
|
+
content: fileName ? null : await (await route.fetch()).text(),
|
|
261
285
|
},
|
|
262
286
|
id: crypto.randomUUID(),
|
|
263
287
|
served: false,
|