ftmocks-utils 1.3.3 → 1.3.5

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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftmocks-utils",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Util functions for FtMocks",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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
  };
@@ -0,0 +1,73 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+ const { getMockDir, nameToFolder } = require("./common-utils");
4
+
5
+ const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
6
+ const urlObj = new URL(route.request().url());
7
+
8
+ // Check if URL contains file extension like .js, .png, .css etc
9
+ const fileExtMatch = urlObj.pathname.match(/\.[a-zA-Z0-9]+$/);
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) {
47
+ // Create directory path matching URL structure
48
+ const dirPath = path.join(
49
+ getMockDir(ftmocksConifg),
50
+ `test_${nameToFolder(testName)}`,
51
+ "_files",
52
+ path.dirname(urlObj.pathname)
53
+ );
54
+
55
+ // Create directories if they don't exist
56
+ fs.mkdirSync(dirPath, { recursive: true });
57
+
58
+ // Save file with original name
59
+ const fileName = `${id}${fileExt}`;
60
+ const filePath = path.join(dirPath, fileName);
61
+
62
+ const response = await route.fetch();
63
+ const buffer = await response.body();
64
+ fs.writeFileSync(filePath, buffer);
65
+
66
+ return fileName;
67
+ }
68
+ return false;
69
+ };
70
+
71
+ module.exports = {
72
+ saveIfItIsFile,
73
+ };