@congminh1254/shopee-sdk 1.6.2 → 1.8.0
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 +9 -3
- package/lib/__tests__/errors.test.d.ts +1 -0
- package/lib/__tests__/errors.test.js +49 -0
- package/lib/__tests__/errors.test.js.map +1 -0
- package/lib/__tests__/fetch.test.d.ts +1 -0
- package/lib/__tests__/fetch.test.js +494 -0
- package/lib/__tests__/fetch.test.js.map +1 -0
- package/lib/__tests__/integration.test.d.ts +1 -0
- package/lib/__tests__/integration.test.js +201 -0
- package/lib/__tests__/integration.test.js.map +1 -0
- package/lib/__tests__/managers/account-health.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/account-health.manager.test.js +446 -0
- package/lib/__tests__/managers/account-health.manager.test.js.map +1 -0
- package/lib/__tests__/managers/add-on-deal.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/add-on-deal.manager.test.js +636 -0
- package/lib/__tests__/managers/add-on-deal.manager.test.js.map +1 -0
- package/lib/__tests__/managers/ads.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/ads.manager.test.js +1016 -0
- package/lib/__tests__/managers/ads.manager.test.js.map +1 -0
- package/lib/__tests__/managers/ams.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/ams.manager.test.js +1170 -0
- package/lib/__tests__/managers/ams.manager.test.js.map +1 -0
- package/lib/__tests__/managers/auth.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/auth.manager.test.js +193 -0
- package/lib/__tests__/managers/auth.manager.test.js.map +1 -0
- package/lib/__tests__/managers/bundle-deal.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/bundle-deal.manager.test.js +465 -0
- package/lib/__tests__/managers/bundle-deal.manager.test.js.map +1 -0
- package/lib/__tests__/managers/discount.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/discount.manager.test.js +615 -0
- package/lib/__tests__/managers/discount.manager.test.js.map +1 -0
- package/lib/__tests__/managers/fbs.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/fbs.manager.test.js +349 -0
- package/lib/__tests__/managers/fbs.manager.test.js.map +1 -0
- package/lib/__tests__/managers/first-mile.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/first-mile.manager.test.js +629 -0
- package/lib/__tests__/managers/first-mile.manager.test.js.map +1 -0
- package/lib/__tests__/managers/follow-prize.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/follow-prize.manager.test.js +408 -0
- package/lib/__tests__/managers/follow-prize.manager.test.js.map +1 -0
- package/lib/__tests__/managers/global-product.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/global-product.manager.test.js +1253 -0
- package/lib/__tests__/managers/global-product.manager.test.js.map +1 -0
- package/lib/__tests__/managers/livestream.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/livestream.manager.test.js +877 -0
- package/lib/__tests__/managers/livestream.manager.test.js.map +1 -0
- package/lib/__tests__/managers/logistics.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/logistics.manager.test.js +1125 -0
- package/lib/__tests__/managers/logistics.manager.test.js.map +1 -0
- package/lib/__tests__/managers/media-space.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/media-space.manager.test.js +556 -0
- package/lib/__tests__/managers/media-space.manager.test.js.map +1 -0
- package/lib/__tests__/managers/media.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/media.manager.test.js +681 -0
- package/lib/__tests__/managers/media.manager.test.js.map +1 -0
- package/lib/__tests__/managers/merchant.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/merchant.manager.test.js +328 -0
- package/lib/__tests__/managers/merchant.manager.test.js.map +1 -0
- package/lib/__tests__/managers/order.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/order.manager.test.js +937 -0
- package/lib/__tests__/managers/order.manager.test.js.map +1 -0
- package/lib/__tests__/managers/payment.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/payment.manager.test.js +1167 -0
- package/lib/__tests__/managers/payment.manager.test.js.map +1 -0
- package/lib/__tests__/managers/product.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/product.manager.test.js +2247 -0
- package/lib/__tests__/managers/product.manager.test.js.map +1 -0
- package/lib/__tests__/managers/public.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/public.manager.test.js +209 -0
- package/lib/__tests__/managers/public.manager.test.js.map +1 -0
- package/lib/__tests__/managers/push.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/push.manager.test.js +244 -0
- package/lib/__tests__/managers/push.manager.test.js.map +1 -0
- package/lib/__tests__/managers/returns.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/returns.manager.test.js +783 -0
- package/lib/__tests__/managers/returns.manager.test.js.map +1 -0
- package/lib/__tests__/managers/sbs.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/sbs.manager.test.js +492 -0
- package/lib/__tests__/managers/sbs.manager.test.js.map +1 -0
- package/lib/__tests__/managers/shop-category.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/shop-category.manager.test.js +425 -0
- package/lib/__tests__/managers/shop-category.manager.test.js.map +1 -0
- package/lib/__tests__/managers/shop-flash-sale.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/shop-flash-sale.manager.test.js +492 -0
- package/lib/__tests__/managers/shop-flash-sale.manager.test.js.map +1 -0
- package/lib/__tests__/managers/shop.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/shop.manager.test.js +637 -0
- package/lib/__tests__/managers/shop.manager.test.js.map +1 -0
- package/lib/__tests__/managers/top-picks.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/top-picks.manager.test.js +324 -0
- package/lib/__tests__/managers/top-picks.manager.test.js.map +1 -0
- package/lib/__tests__/managers/video.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/video.manager.test.js +1039 -0
- package/lib/__tests__/managers/video.manager.test.js.map +1 -0
- package/lib/__tests__/managers/voucher.manager.test.d.ts +1 -0
- package/lib/__tests__/managers/voucher.manager.test.js +442 -0
- package/lib/__tests__/managers/voucher.manager.test.js.map +1 -0
- package/lib/__tests__/sdk.test.d.ts +1 -0
- package/lib/__tests__/sdk.test.js +316 -0
- package/lib/__tests__/sdk.test.js.map +1 -0
- package/lib/__tests__/storage/custom-token-storage.test.d.ts +1 -0
- package/lib/__tests__/storage/custom-token-storage.test.js +337 -0
- package/lib/__tests__/storage/custom-token-storage.test.js.map +1 -0
- package/lib/__tests__/utils/signature.test.d.ts +1 -0
- package/lib/__tests__/utils/signature.test.js +38 -0
- package/lib/__tests__/utils/signature.test.js.map +1 -0
- package/lib/__tests__/utils/spec-audit.test.d.ts +1 -0
- package/lib/__tests__/utils/spec-audit.test.js +176 -0
- package/lib/__tests__/utils/spec-audit.test.js.map +1 -0
- package/lib/errors.js +2 -0
- package/lib/errors.js.map +1 -1
- package/lib/fetch.js +1 -1
- package/lib/fetch.js.map +1 -1
- package/lib/managers/ads.manager.d.ts +1 -41
- package/lib/managers/ads.manager.js +0 -54
- package/lib/managers/ads.manager.js.map +1 -1
- package/lib/managers/auth.manager.d.ts +2 -1
- package/lib/managers/auth.manager.js +2 -5
- package/lib/managers/auth.manager.js.map +1 -1
- package/lib/managers/base.manager.js +1 -0
- package/lib/managers/base.manager.js.map +1 -1
- package/lib/managers/media.manager.d.ts +36 -1
- package/lib/managers/media.manager.js +68 -0
- package/lib/managers/media.manager.js.map +1 -1
- package/lib/managers/order.manager.js +1 -0
- package/lib/managers/order.manager.js.map +1 -1
- package/lib/managers/payment.manager.d.ts +13 -1
- package/lib/managers/payment.manager.js +26 -0
- package/lib/managers/payment.manager.js.map +1 -1
- package/lib/managers/product.manager.js +1 -1
- package/lib/managers/product.manager.js.map +1 -1
- package/lib/schemas/ads.d.ts +2 -56
- package/lib/schemas/ams.d.ts +92 -0
- package/lib/schemas/base.d.ts +2 -0
- package/lib/schemas/discount.d.ts +4 -0
- package/lib/schemas/fetch.d.ts +1 -1
- package/lib/schemas/livestream.d.ts +14 -0
- package/lib/schemas/media.d.ts +107 -0
- package/lib/schemas/merchant.d.ts +2 -0
- package/lib/schemas/order.d.ts +22 -0
- package/lib/schemas/payment.d.ts +96 -0
- package/lib/schemas/product.d.ts +253 -15
- package/lib/schemas/public.d.ts +3 -0
- package/lib/schemas/region.d.ts +9 -2
- package/lib/schemas/region.js +9 -2
- package/lib/schemas/region.js.map +1 -1
- package/lib/schemas/returns.d.ts +38 -1
- package/lib/schemas/shop.d.ts +16 -0
- package/lib/schemas/video.d.ts +83 -37
- package/lib/schemas/voucher.d.ts +2 -0
- package/lib/sdk.d.ts +6 -1
- package/lib/sdk.js +53 -6
- package/lib/sdk.js.map +1 -1
- package/lib/storage/custom-token-storage.js +2 -0
- package/lib/storage/custom-token-storage.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
🎉 **The most complete, production-ready TypeScript SDK for Shopee Open API** 🎉
|
|
10
10
|
|
|
11
|
+
<a href="https://www.buymeacoffee.com/congminh1254" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" style="height: 40px !important;width: 150px !important;" ></a>
|
|
12
|
+
|
|
11
13
|
Build powerful Shopee integrations with confidence using our fully-featured SDK that covers **100% of Shopee's API endpoints**. Trusted by developers, built by the community.
|
|
12
14
|
|
|
13
15
|
## 📚 Documentation
|
|
@@ -32,6 +34,7 @@ Build powerful Shopee integrations with confidence using our fully-featured SDK
|
|
|
32
34
|
- [PushManager](./docs/managers/push.md) - Webhooks and notifications
|
|
33
35
|
- [PublicManager](./docs/managers/public.md) - Public API endpoints
|
|
34
36
|
- [AdsManager](./docs/managers/ads.md) - Advertising campaigns
|
|
37
|
+
- [AmsManager](./docs/managers/ams.md) - Affiliate Marketing Solutions (AMS)
|
|
35
38
|
- [AccountHealthManager](./docs/managers/account-health.md) - Performance metrics
|
|
36
39
|
- [ShopManager](./docs/managers/shop.md) - Shop information and settings
|
|
37
40
|
- [MediaManager](./docs/managers/media.md) - Image and video upload
|
|
@@ -50,6 +53,7 @@ Build powerful Shopee integrations with confidence using our fully-featured SDK
|
|
|
50
53
|
- [SbsManager](./docs/managers/sbs.md) - Shopee Business Services (SBS) warehouse inventory
|
|
51
54
|
- [FbsManager](./docs/managers/fbs.md) - Fulfillment by Shopee operations
|
|
52
55
|
- [LivestreamManager](./docs/managers/livestream.md) - Live streaming features
|
|
56
|
+
- [VideoManager](./docs/managers/video.md) - Shopee Video features and analytics
|
|
53
57
|
|
|
54
58
|
## Installation
|
|
55
59
|
|
|
@@ -60,7 +64,7 @@ npm install @congminh1254/shopee-sdk
|
|
|
60
64
|
**Requirements:** Node.js >= 20.0.0
|
|
61
65
|
|
|
62
66
|
### What You Get
|
|
63
|
-
✅ Complete TypeScript definitions for all
|
|
67
|
+
✅ Complete TypeScript definitions for all 29 API managers
|
|
64
68
|
✅ Automatic token refresh and management
|
|
65
69
|
✅ Built-in error handling and retry logic
|
|
66
70
|
✅ Zero dependencies (except node-fetch)
|
|
@@ -124,12 +128,12 @@ See the [Setup Guide](./docs/guides/setup.md) and [Authentication Guide](./docs/
|
|
|
124
128
|
## Why Choose This SDK?
|
|
125
129
|
|
|
126
130
|
### 🚀 Production-Ready & Battle-Tested
|
|
127
|
-
- **
|
|
131
|
+
- **95%+ test coverage** with 671 comprehensive tests - ensuring reliability in production
|
|
128
132
|
- **Zero compromises** - Every Shopee API endpoint is implemented and documented
|
|
129
133
|
- **Type-safe** - Full TypeScript definitions prevent errors before they happen
|
|
130
134
|
- **Actively maintained** - Regular updates to stay in sync with Shopee API changes
|
|
131
135
|
|
|
132
|
-
### 💪 Complete API Coverage - All
|
|
136
|
+
### 💪 Complete API Coverage - All 29 Managers Implemented
|
|
133
137
|
Unlike other SDKs with partial coverage, we provide **complete access** to every Shopee API:
|
|
134
138
|
|
|
135
139
|
**Core Commerce:**
|
|
@@ -146,6 +150,7 @@ Unlike other SDKs with partial coverage, we provide **complete access** to every
|
|
|
146
150
|
- ⚡ **ShopFlashSaleManager** - Flash sale campaigns
|
|
147
151
|
- 🎯 **FollowPrizeManager** - Follow prize activities
|
|
148
152
|
- ⭐ **TopPicksManager** - Top picks product collections
|
|
153
|
+
- 📣 **AmsManager** - Affiliate marketing solutions (AMS)
|
|
149
154
|
|
|
150
155
|
**Store Management:**
|
|
151
156
|
- 🏪 **ShopManager** - Shop information and profile management
|
|
@@ -165,6 +170,7 @@ Unlike other SDKs with partial coverage, we provide **complete access** to every
|
|
|
165
170
|
- 🏭 **SbsManager** - Shopee Business Services warehouse inventory
|
|
166
171
|
- 📦 **FbsManager** - Fulfillment by Shopee operations
|
|
167
172
|
- 📹 **LivestreamManager** - Live streaming features
|
|
173
|
+
- 🎬 **VideoManager** - Shopee Video features and analytics
|
|
168
174
|
|
|
169
175
|
### ✨ Developer Experience First
|
|
170
176
|
- **Intuitive API design** - Clean, consistent interfaces across all managers
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ShopeeSdkError, ShopeeApiError } from "../errors.js";
|
|
2
|
+
describe("ShopeeSdkError", () => {
|
|
3
|
+
it("should create an error with the correct name and message", () => {
|
|
4
|
+
const errorMessage = "Test error message";
|
|
5
|
+
const error = new ShopeeSdkError(errorMessage);
|
|
6
|
+
expect(error).toBeInstanceOf(Error);
|
|
7
|
+
expect(error).toBeInstanceOf(ShopeeSdkError);
|
|
8
|
+
expect(error.name).toBe("ShopeeSdkError");
|
|
9
|
+
expect(error.message).toBe(errorMessage);
|
|
10
|
+
});
|
|
11
|
+
it("should have a proper error stack", () => {
|
|
12
|
+
const error = new ShopeeSdkError("Test error");
|
|
13
|
+
expect(error.stack).toBeDefined();
|
|
14
|
+
expect(typeof error.stack).toBe("string");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("ShopeeApiError", () => {
|
|
18
|
+
it("should create an error with status and data", () => {
|
|
19
|
+
const status = 400;
|
|
20
|
+
const data = { error: "Invalid request", message: "Bad request" };
|
|
21
|
+
const error = new ShopeeApiError(status, data);
|
|
22
|
+
expect(error).toBeInstanceOf(Error);
|
|
23
|
+
expect(error).toBeInstanceOf(ShopeeApiError);
|
|
24
|
+
expect(error.name).toBe("ShopeeApiError");
|
|
25
|
+
expect(error.status).toBe(status);
|
|
26
|
+
expect(error.data).toBe(data);
|
|
27
|
+
expect(error.message).toContain(status.toString());
|
|
28
|
+
expect(error.message).toContain(JSON.stringify(data));
|
|
29
|
+
});
|
|
30
|
+
it("should handle different types of data", () => {
|
|
31
|
+
const testCases = [
|
|
32
|
+
{ status: 404, data: { error: "Not found" } },
|
|
33
|
+
{ status: 500, data: "Internal server error" },
|
|
34
|
+
{ status: 401, data: null },
|
|
35
|
+
{ status: 403, data: undefined },
|
|
36
|
+
];
|
|
37
|
+
testCases.forEach(({ status, data }) => {
|
|
38
|
+
const error = new ShopeeApiError(status, data);
|
|
39
|
+
expect(error.status).toBe(status);
|
|
40
|
+
expect(error.data).toBe(data);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it("should have a proper error stack", () => {
|
|
44
|
+
const error = new ShopeeApiError(500, { error: "Test error" });
|
|
45
|
+
expect(error.stack).toBeDefined();
|
|
46
|
+
expect(typeof error.stack).toBe("string");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=errors.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.js","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { jest, describe, beforeEach, it, expect } from "@jest/globals";
|
|
2
|
+
import { ShopeeRegion } from "../schemas/region.js";
|
|
3
|
+
import { ShopeeSdkError } from "../errors.js";
|
|
4
|
+
// Mock fetch function
|
|
5
|
+
const mockFetch = jest.fn();
|
|
6
|
+
// Mock the entire fetch module before importing ShopeeFetch
|
|
7
|
+
jest.unstable_mockModule("node-fetch", () => ({
|
|
8
|
+
default: mockFetch,
|
|
9
|
+
Blob: globalThis.Blob,
|
|
10
|
+
FormData: globalThis.FormData,
|
|
11
|
+
Headers: globalThis.Headers,
|
|
12
|
+
}));
|
|
13
|
+
// Import ShopeeFetch after mocking
|
|
14
|
+
const { ShopeeFetch } = await import("../fetch.js");
|
|
15
|
+
function getRequestContentType(url, options) {
|
|
16
|
+
return new Request(url, options).headers.get("content-type");
|
|
17
|
+
}
|
|
18
|
+
describe("ShopeeFetch", () => {
|
|
19
|
+
let mockConfig;
|
|
20
|
+
let mockSdk;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
mockSdk = {
|
|
24
|
+
getAuthToken: jest.fn(),
|
|
25
|
+
refreshToken: jest.fn(),
|
|
26
|
+
};
|
|
27
|
+
mockConfig = {
|
|
28
|
+
partner_id: 12345,
|
|
29
|
+
partner_key: "test_partner_key",
|
|
30
|
+
shop_id: 67890,
|
|
31
|
+
region: ShopeeRegion.GLOBAL,
|
|
32
|
+
base_url: "https://partner.test-stable.shopeemobile.com/api/v2",
|
|
33
|
+
sdk: mockSdk,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
describe("fetch method", () => {
|
|
37
|
+
it("should make successful GET request without auth", async () => {
|
|
38
|
+
const mockResponse = {
|
|
39
|
+
request_id: "test-request-id",
|
|
40
|
+
error: "",
|
|
41
|
+
message: "",
|
|
42
|
+
response: { data: "test data" },
|
|
43
|
+
};
|
|
44
|
+
mockFetch.mockResolvedValueOnce({
|
|
45
|
+
status: 200,
|
|
46
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
47
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
48
|
+
});
|
|
49
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint");
|
|
50
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(result).toEqual(mockResponse);
|
|
52
|
+
});
|
|
53
|
+
it("should make successful POST request with body", async () => {
|
|
54
|
+
const mockResponse = { success: true };
|
|
55
|
+
const requestBody = { key: "value" };
|
|
56
|
+
mockFetch.mockResolvedValueOnce({
|
|
57
|
+
status: 200,
|
|
58
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
59
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
60
|
+
});
|
|
61
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: requestBody,
|
|
64
|
+
});
|
|
65
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
66
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
67
|
+
expect(options).toBeDefined();
|
|
68
|
+
expect(options.method).toBe("POST");
|
|
69
|
+
expect(options.body).toBe(JSON.stringify(requestBody));
|
|
70
|
+
expect(result).toEqual(mockResponse);
|
|
71
|
+
});
|
|
72
|
+
it("should serialize multipart bodies when the payload contains binary fields", async () => {
|
|
73
|
+
const mockResponse = { success: true };
|
|
74
|
+
mockFetch.mockResolvedValue({
|
|
75
|
+
status: 200,
|
|
76
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
77
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
78
|
+
});
|
|
79
|
+
const image = Buffer.from("image-data");
|
|
80
|
+
await ShopeeFetch.fetch(mockConfig, "/media_space/upload_image", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: {
|
|
83
|
+
scene: "normal",
|
|
84
|
+
image,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
88
|
+
expect(options).toBeDefined();
|
|
89
|
+
const formData = options.body;
|
|
90
|
+
expect(formData).toBeInstanceOf(FormData);
|
|
91
|
+
expect(formData.get("scene")).toBe("normal");
|
|
92
|
+
expect(formData.get("image")).toBeInstanceOf(Blob);
|
|
93
|
+
expect(options.headers.get("content-type")).toBeNull();
|
|
94
|
+
expect(getRequestContentType(`${mockConfig.base_url}/media_space/upload_image`, options)).toMatch(/^multipart\/form-data; boundary=/);
|
|
95
|
+
});
|
|
96
|
+
it("should serialize multipart bodies when the payload contains binary arrays", async () => {
|
|
97
|
+
const mockResponse = { success: true };
|
|
98
|
+
mockFetch.mockResolvedValue({
|
|
99
|
+
status: 200,
|
|
100
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
101
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
102
|
+
});
|
|
103
|
+
const image1 = Buffer.from("image-1");
|
|
104
|
+
const image2 = Buffer.from("image-2");
|
|
105
|
+
await ShopeeFetch.fetch(mockConfig, "/media/upload_image", {
|
|
106
|
+
method: "POST",
|
|
107
|
+
body: {
|
|
108
|
+
business: 2,
|
|
109
|
+
scene: 1,
|
|
110
|
+
images: [image1, image2],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
114
|
+
expect(options).toBeDefined();
|
|
115
|
+
const formData = options.body;
|
|
116
|
+
const images = formData.getAll("images");
|
|
117
|
+
expect(formData).toBeInstanceOf(FormData);
|
|
118
|
+
expect(formData.get("business")).toBe("2");
|
|
119
|
+
expect(formData.get("scene")).toBe("1");
|
|
120
|
+
expect(images).toHaveLength(2);
|
|
121
|
+
images.forEach((image) => expect(image).toBeInstanceOf(Blob));
|
|
122
|
+
expect(options.headers.get("content-type")).toBeNull();
|
|
123
|
+
expect(getRequestContentType(`${mockConfig.base_url}/media/upload_image`, options)).toMatch(/^multipart\/form-data; boundary=/);
|
|
124
|
+
});
|
|
125
|
+
it("should pass through FormData bodies without forcing JSON content type", async () => {
|
|
126
|
+
const mockResponse = { success: true };
|
|
127
|
+
mockFetch.mockResolvedValue({
|
|
128
|
+
status: 200,
|
|
129
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
130
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
131
|
+
});
|
|
132
|
+
const formData = new FormData();
|
|
133
|
+
formData.append("video_upload_id", "upload-id");
|
|
134
|
+
formData.append("part_content", new Blob([Buffer.from("video-part")]), "part.bin");
|
|
135
|
+
await ShopeeFetch.fetch(mockConfig, "/media_space/upload_video_part", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: formData,
|
|
138
|
+
});
|
|
139
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
140
|
+
expect(options).toBeDefined();
|
|
141
|
+
expect(options.body).toBe(formData);
|
|
142
|
+
expect(options.headers.get("content-type")).toBeNull();
|
|
143
|
+
expect(getRequestContentType(`${mockConfig.base_url}/media_space/upload_video_part`, options)).toMatch(/^multipart\/form-data; boundary=/);
|
|
144
|
+
});
|
|
145
|
+
it("should serialize multipart bodies with null, undefined, named/unnamed Blobs, and nested objects", async () => {
|
|
146
|
+
const mockResponse = { success: true };
|
|
147
|
+
mockFetch.mockResolvedValue({
|
|
148
|
+
status: 200,
|
|
149
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
150
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
151
|
+
});
|
|
152
|
+
const fileWithFilename = new Blob([Buffer.from("file-1")]);
|
|
153
|
+
Object.defineProperty(fileWithFilename, "name", {
|
|
154
|
+
value: "custom-file.txt",
|
|
155
|
+
writable: false,
|
|
156
|
+
});
|
|
157
|
+
const fileWithoutFilename = new Blob([Buffer.from("file-2")]);
|
|
158
|
+
await ShopeeFetch.fetch(mockConfig, "/media_space/upload_image", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: {
|
|
161
|
+
image: Buffer.from("image-data"),
|
|
162
|
+
nullField: null,
|
|
163
|
+
undefinedField: undefined,
|
|
164
|
+
arrayField: ["string", null, undefined],
|
|
165
|
+
namedBlob: fileWithFilename,
|
|
166
|
+
unnamedBlob: fileWithoutFilename,
|
|
167
|
+
nestedObject: { key: "nested" },
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
171
|
+
expect(options).toBeDefined();
|
|
172
|
+
const formData = options.body;
|
|
173
|
+
expect(formData).toBeInstanceOf(FormData);
|
|
174
|
+
expect(formData.get("image")).toBeInstanceOf(Blob);
|
|
175
|
+
expect(formData.get("nullField")).toBeNull();
|
|
176
|
+
expect(formData.get("undefinedField")).toBeNull();
|
|
177
|
+
expect(formData.getAll("arrayField")).toEqual(["string"]);
|
|
178
|
+
expect(formData.get("namedBlob")).toBeInstanceOf(Blob);
|
|
179
|
+
expect(formData.get("unnamedBlob")).toBeInstanceOf(Blob);
|
|
180
|
+
expect(formData.get("nestedObject")).toBe(JSON.stringify({ key: "nested" }));
|
|
181
|
+
});
|
|
182
|
+
it("should make authenticated request with valid token", async () => {
|
|
183
|
+
const mockToken = {
|
|
184
|
+
access_token: "test_access_token",
|
|
185
|
+
refresh_token: "test_refresh_token",
|
|
186
|
+
expire_in: 3600,
|
|
187
|
+
expired_at: Date.now() + 3600000,
|
|
188
|
+
shop_id: 67890,
|
|
189
|
+
request_id: "test-request-id",
|
|
190
|
+
error: "",
|
|
191
|
+
message: "",
|
|
192
|
+
};
|
|
193
|
+
const mockResponse = { data: "authenticated data" };
|
|
194
|
+
mockSdk.getAuthToken.mockResolvedValue(mockToken);
|
|
195
|
+
mockFetch.mockResolvedValueOnce({
|
|
196
|
+
status: 200,
|
|
197
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
198
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
199
|
+
});
|
|
200
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
201
|
+
auth: true,
|
|
202
|
+
});
|
|
203
|
+
expect(mockSdk.getAuthToken).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
205
|
+
const [url] = mockFetch.mock.calls[0];
|
|
206
|
+
expect(url).toContain("access_token=test_access_token");
|
|
207
|
+
expect(url).toContain("shop_id=67890");
|
|
208
|
+
expect(result).toEqual(mockResponse);
|
|
209
|
+
});
|
|
210
|
+
it("should refresh token when expired and retry request", async () => {
|
|
211
|
+
const expiredToken = {
|
|
212
|
+
access_token: "expired_token",
|
|
213
|
+
refresh_token: "test_refresh_token",
|
|
214
|
+
expire_in: 3600,
|
|
215
|
+
expired_at: Date.now() - 1000, // Expired
|
|
216
|
+
shop_id: 67890,
|
|
217
|
+
request_id: "test-request-id",
|
|
218
|
+
error: "",
|
|
219
|
+
message: "",
|
|
220
|
+
};
|
|
221
|
+
const newToken = {
|
|
222
|
+
access_token: "new_access_token",
|
|
223
|
+
refresh_token: "new_refresh_token",
|
|
224
|
+
expire_in: 3600,
|
|
225
|
+
expired_at: Date.now() + 3600000,
|
|
226
|
+
shop_id: 67890,
|
|
227
|
+
request_id: "test-request-id",
|
|
228
|
+
error: "",
|
|
229
|
+
message: "",
|
|
230
|
+
};
|
|
231
|
+
const mockResponse = { data: "authenticated data" };
|
|
232
|
+
mockSdk.getAuthToken.mockResolvedValue(expiredToken);
|
|
233
|
+
mockSdk.refreshToken.mockResolvedValue(newToken);
|
|
234
|
+
mockFetch.mockResolvedValueOnce({
|
|
235
|
+
status: 200,
|
|
236
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
237
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
238
|
+
});
|
|
239
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
240
|
+
auth: true,
|
|
241
|
+
});
|
|
242
|
+
expect(mockSdk.getAuthToken).toHaveBeenCalledTimes(1);
|
|
243
|
+
expect(mockSdk.refreshToken).toHaveBeenCalledTimes(1);
|
|
244
|
+
expect(result).toEqual(mockResponse);
|
|
245
|
+
});
|
|
246
|
+
it("should throw error when no access token found", async () => {
|
|
247
|
+
mockSdk.getAuthToken.mockResolvedValue(null);
|
|
248
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint", { auth: true })).rejects.toThrow(ShopeeSdkError);
|
|
249
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint", { auth: true })).rejects.toThrow("No access token found");
|
|
250
|
+
});
|
|
251
|
+
it("should handle invalid access token error and retry after refresh", async () => {
|
|
252
|
+
const invalidTokenResponse = {
|
|
253
|
+
error: "invalid_acceess_token",
|
|
254
|
+
message: "Invalid access token",
|
|
255
|
+
request_id: "test-request-id",
|
|
256
|
+
};
|
|
257
|
+
const successResponse = { data: "success after refresh" };
|
|
258
|
+
const mockToken = {
|
|
259
|
+
access_token: "old_token",
|
|
260
|
+
refresh_token: "refresh_token",
|
|
261
|
+
expire_in: 3600,
|
|
262
|
+
expired_at: Date.now() + 3600000,
|
|
263
|
+
shop_id: 67890,
|
|
264
|
+
request_id: "test-request-id",
|
|
265
|
+
error: "",
|
|
266
|
+
message: "",
|
|
267
|
+
};
|
|
268
|
+
mockSdk.getAuthToken.mockResolvedValue(mockToken);
|
|
269
|
+
mockSdk.refreshToken.mockResolvedValue(mockToken);
|
|
270
|
+
// First call returns invalid token error, second call succeeds
|
|
271
|
+
mockFetch
|
|
272
|
+
.mockResolvedValueOnce({
|
|
273
|
+
status: 401,
|
|
274
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
275
|
+
json: jest.fn(() => Promise.resolve(invalidTokenResponse)),
|
|
276
|
+
})
|
|
277
|
+
.mockResolvedValueOnce({
|
|
278
|
+
status: 200,
|
|
279
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
280
|
+
json: jest.fn(() => Promise.resolve(successResponse)),
|
|
281
|
+
});
|
|
282
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
283
|
+
auth: true,
|
|
284
|
+
});
|
|
285
|
+
expect(mockSdk.refreshToken).toHaveBeenCalledTimes(1);
|
|
286
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
287
|
+
expect(result).toEqual(successResponse);
|
|
288
|
+
});
|
|
289
|
+
it("should include custom headers in request", async () => {
|
|
290
|
+
const mockResponse = { data: "test" };
|
|
291
|
+
const customHeaders = {
|
|
292
|
+
"X-Custom-Header": "custom-value",
|
|
293
|
+
};
|
|
294
|
+
mockFetch.mockResolvedValueOnce({
|
|
295
|
+
status: 200,
|
|
296
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
297
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
298
|
+
});
|
|
299
|
+
await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
300
|
+
headers: customHeaders,
|
|
301
|
+
});
|
|
302
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
303
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
304
|
+
expect(options).toBeDefined();
|
|
305
|
+
expect(options.headers.get("X-Custom-Header")).toBe("custom-value");
|
|
306
|
+
});
|
|
307
|
+
it("should include User-Agent header with SDK version", async () => {
|
|
308
|
+
const mockResponse = { data: "test" };
|
|
309
|
+
mockFetch.mockResolvedValueOnce({
|
|
310
|
+
status: 200,
|
|
311
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
312
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
313
|
+
});
|
|
314
|
+
await ShopeeFetch.fetch(mockConfig, "/test/endpoint");
|
|
315
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
316
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
317
|
+
expect(options).toBeDefined();
|
|
318
|
+
const userAgent = options.headers.get("user-agent");
|
|
319
|
+
expect(userAgent).toMatch(/^congminh1254\/shopee-sdk\/v\d+\.\d+\.\d+$/);
|
|
320
|
+
});
|
|
321
|
+
it("should include query parameters in URL", async () => {
|
|
322
|
+
const mockResponse = { data: "test" };
|
|
323
|
+
const params = {
|
|
324
|
+
param1: "value1",
|
|
325
|
+
param2: 123,
|
|
326
|
+
param3: ["array1", "array2"],
|
|
327
|
+
};
|
|
328
|
+
mockFetch.mockResolvedValueOnce({
|
|
329
|
+
status: 200,
|
|
330
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
331
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
332
|
+
});
|
|
333
|
+
await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
334
|
+
params,
|
|
335
|
+
});
|
|
336
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
337
|
+
const [url] = mockFetch.mock.calls[0];
|
|
338
|
+
expect(url).toContain("param1=value1");
|
|
339
|
+
expect(url).toContain("param2=123");
|
|
340
|
+
expect(url).toContain("param3=array1");
|
|
341
|
+
expect(url).toContain("param3=array2");
|
|
342
|
+
});
|
|
343
|
+
it("should remove undefined query parameters from URL", async () => {
|
|
344
|
+
const mockResponse = { data: "test" };
|
|
345
|
+
const params = {
|
|
346
|
+
param1: "value1",
|
|
347
|
+
param2: undefined,
|
|
348
|
+
};
|
|
349
|
+
mockFetch.mockResolvedValueOnce({
|
|
350
|
+
status: 200,
|
|
351
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
352
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
353
|
+
});
|
|
354
|
+
await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
355
|
+
params,
|
|
356
|
+
});
|
|
357
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
358
|
+
const [url] = mockFetch.mock.calls[0];
|
|
359
|
+
expect(url).toContain("param1=value1");
|
|
360
|
+
expect(url).not.toContain("param2");
|
|
361
|
+
});
|
|
362
|
+
it("should handle invalid access token error when refresh fails", async () => {
|
|
363
|
+
const invalidTokenResponse = {
|
|
364
|
+
error: "invalid_acceess_token",
|
|
365
|
+
message: "Invalid access token",
|
|
366
|
+
request_id: "test-request-id",
|
|
367
|
+
};
|
|
368
|
+
const mockToken = {
|
|
369
|
+
access_token: "old_token",
|
|
370
|
+
refresh_token: "refresh_token",
|
|
371
|
+
expire_in: 3600,
|
|
372
|
+
expired_at: Date.now() + 3600000,
|
|
373
|
+
shop_id: 67890,
|
|
374
|
+
request_id: "test-request-id",
|
|
375
|
+
error: "",
|
|
376
|
+
message: "",
|
|
377
|
+
};
|
|
378
|
+
mockSdk.getAuthToken.mockResolvedValue(mockToken);
|
|
379
|
+
mockSdk.refreshToken.mockRejectedValue(new Error("Refresh failed"));
|
|
380
|
+
mockFetch.mockResolvedValueOnce({
|
|
381
|
+
status: 401,
|
|
382
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
383
|
+
json: jest.fn(() => Promise.resolve(invalidTokenResponse)),
|
|
384
|
+
});
|
|
385
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint", { auth: true })).rejects.toThrow("API Error: 401");
|
|
386
|
+
});
|
|
387
|
+
it("should throw ShopeeApiError when API returns error", async () => {
|
|
388
|
+
const errorResponse = {
|
|
389
|
+
error: "error_code",
|
|
390
|
+
message: "Error message",
|
|
391
|
+
request_id: "test-request-id",
|
|
392
|
+
};
|
|
393
|
+
mockFetch.mockResolvedValueOnce({
|
|
394
|
+
status: 400,
|
|
395
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
396
|
+
json: jest.fn(() => Promise.resolve(errorResponse)),
|
|
397
|
+
});
|
|
398
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint")).rejects.toThrow("API Error: 400");
|
|
399
|
+
});
|
|
400
|
+
it("should throw error for unknown response type", async () => {
|
|
401
|
+
const headers = new Map([["content-type", "text/html"]]);
|
|
402
|
+
mockFetch.mockResolvedValueOnce({
|
|
403
|
+
status: 200,
|
|
404
|
+
headers: {
|
|
405
|
+
get: (name) => headers.get(name.toLowerCase()),
|
|
406
|
+
},
|
|
407
|
+
text: jest.fn(() => Promise.resolve("<html>Not JSON</html>")),
|
|
408
|
+
json: jest.fn(),
|
|
409
|
+
});
|
|
410
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint")).rejects.toThrow("Unknown response type");
|
|
411
|
+
});
|
|
412
|
+
it("should return Buffer for application/pdf response", async () => {
|
|
413
|
+
const text = "%PDF-1.4 test pdf content";
|
|
414
|
+
const uint8 = new TextEncoder().encode(text);
|
|
415
|
+
const headers = new Map([["content-type", "application/pdf"]]);
|
|
416
|
+
mockFetch.mockResolvedValueOnce({
|
|
417
|
+
status: 200,
|
|
418
|
+
headers: {
|
|
419
|
+
get: (name) => headers.get(name.toLowerCase()),
|
|
420
|
+
},
|
|
421
|
+
arrayBuffer: jest.fn(() => Promise.resolve(uint8.buffer)),
|
|
422
|
+
});
|
|
423
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint");
|
|
424
|
+
expect(result).toBeInstanceOf(Buffer);
|
|
425
|
+
expect(result.toString()).toBe(text);
|
|
426
|
+
});
|
|
427
|
+
it("should return Buffer for application/octet-stream response", async () => {
|
|
428
|
+
const text = "binary data";
|
|
429
|
+
const uint8 = new TextEncoder().encode(text);
|
|
430
|
+
const headers = new Map([["content-type", "application/octet-stream"]]);
|
|
431
|
+
mockFetch.mockResolvedValueOnce({
|
|
432
|
+
status: 200,
|
|
433
|
+
headers: {
|
|
434
|
+
get: (name) => headers.get(name.toLowerCase()),
|
|
435
|
+
},
|
|
436
|
+
arrayBuffer: jest.fn(() => Promise.resolve(uint8.buffer)),
|
|
437
|
+
});
|
|
438
|
+
const result = await ShopeeFetch.fetch(mockConfig, "/test/endpoint");
|
|
439
|
+
expect(result).toBeInstanceOf(Buffer);
|
|
440
|
+
expect(result.toString()).toBe(text);
|
|
441
|
+
});
|
|
442
|
+
it("should handle network errors", async () => {
|
|
443
|
+
const networkError = new Error("Network failed");
|
|
444
|
+
networkError.name = "FetchError";
|
|
445
|
+
mockFetch.mockRejectedValueOnce(networkError);
|
|
446
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint")).rejects.toThrow("Network error: Network failed");
|
|
447
|
+
});
|
|
448
|
+
it("should handle unexpected errors", async () => {
|
|
449
|
+
const unexpectedError = new Error("Unexpected error");
|
|
450
|
+
mockFetch.mockRejectedValueOnce(unexpectedError);
|
|
451
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint")).rejects.toThrow("Unexpected error: Unexpected error");
|
|
452
|
+
});
|
|
453
|
+
it("should handle unknown non-Error exceptions", async () => {
|
|
454
|
+
mockFetch.mockRejectedValueOnce("string error");
|
|
455
|
+
await expect(ShopeeFetch.fetch(mockConfig, "/test/endpoint")).rejects.toThrow("Unknown error occurred");
|
|
456
|
+
});
|
|
457
|
+
it("should handle null body correctly", async () => {
|
|
458
|
+
const mockResponse = { success: true };
|
|
459
|
+
mockFetch.mockResolvedValueOnce({
|
|
460
|
+
status: 200,
|
|
461
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
462
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
463
|
+
});
|
|
464
|
+
await ShopeeFetch.fetch(mockConfig, "/test/endpoint", {
|
|
465
|
+
method: "POST",
|
|
466
|
+
body: null,
|
|
467
|
+
});
|
|
468
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
469
|
+
expect(options).toBeDefined();
|
|
470
|
+
expect(options.body).toBe("null");
|
|
471
|
+
});
|
|
472
|
+
it("should serialize multipart bodies with fallback isBlobLike conditions", async () => {
|
|
473
|
+
const mockResponse = { success: true };
|
|
474
|
+
mockFetch.mockResolvedValueOnce({
|
|
475
|
+
status: 200,
|
|
476
|
+
headers: new Map([["content-type", "application/json"]]),
|
|
477
|
+
json: jest.fn(() => Promise.resolve(mockResponse)),
|
|
478
|
+
});
|
|
479
|
+
await ShopeeFetch.fetch(mockConfig, "/media_space/upload_image", {
|
|
480
|
+
method: "POST",
|
|
481
|
+
body: {
|
|
482
|
+
image: Buffer.from("image-data"),
|
|
483
|
+
nullProto: Object.create(null),
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
const [, options] = mockFetch.mock.calls[0];
|
|
487
|
+
expect(options).toBeDefined();
|
|
488
|
+
const formData = options.body;
|
|
489
|
+
expect(formData).toBeInstanceOf(FormData);
|
|
490
|
+
expect(formData.get("image")).toBeInstanceOf(Blob);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
//# sourceMappingURL=fetch.test.js.map
|