scratch-storage 2.0.2 → 2.2.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/.circleci/config.yml +70 -0
- package/.nyc_output/c826ae6b-4ae2-4d8e-bdb2-162df5761fa7.json +1 -0
- package/.nyc_output/ec2d52a1-5131-4dd0-9c23-59106f297082.json +1 -0
- package/.nyc_output/processinfo/c826ae6b-4ae2-4d8e-bdb2-162df5761fa7.json +1 -0
- package/.nyc_output/processinfo/ec2d52a1-5131-4dd0-9c23-59106f297082.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/README.md +2 -1
- package/dist/node/8c63ecb0448dfbb8e804.worker.js +4187 -0
- package/dist/node/8c63ecb0448dfbb8e804.worker.js.map +1 -0
- package/dist/node/scratch-storage.js +2933 -142
- package/dist/node/scratch-storage.js.map +1 -1
- package/dist/web/90c5db0dff5ca2a36ff6.worker.js +743 -0
- package/dist/web/90c5db0dff5ca2a36ff6.worker.js.map +1 -0
- package/dist/web/scratch-storage.js +1149 -85
- package/dist/web/scratch-storage.js.map +1 -1
- package/dist/web/scratch-storage.min.js +1149 -85
- package/dist/web/scratch-storage.min.js.map +1 -1
- package/package.json +9 -4
- package/src/FetchTool.js +17 -13
- package/src/FetchWorkerTool.js +14 -7
- package/src/FetchWorkerTool.worker.js +6 -10
- package/src/scratchFetch.js +86 -0
- package/test/integration/download-known-assets.js +27 -30
- package/test/mocks/mockFetch.js +64 -0
- package/test/unit/fetch-tool.js +27 -44
- package/test/unit/metadata.js +136 -0
- package/webpack.config.js +9 -8
- package/.travis.yml +0 -24
- package/dist/node/f98711f696b187f5e007.worker.js +0 -1906
- package/dist/node/f98711f696b187f5e007.worker.js.map +0 -1
- package/dist/web/01b4fbd4d14a4f22cab8.worker.js +0 -190
- package/dist/web/01b4fbd4d14a4f22cab8.worker.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scratch-storage",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Load and store project and asset files for Scratch 3.0",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"homepage": "https://github.com/LLK/scratch-storage#readme",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/LLK/scratch-storage.git",
|
|
10
|
-
"sha": "
|
|
10
|
+
"sha": "5c6588073bb3c9ba4500403054d6409db2379286"
|
|
11
11
|
},
|
|
12
12
|
"main": "./dist/node/scratch-storage.js",
|
|
13
13
|
"browser": "./src/index.js",
|
|
@@ -24,9 +24,14 @@
|
|
|
24
24
|
"watch": "webpack --progress --colors --watch",
|
|
25
25
|
"semantic-release": "semantic-release"
|
|
26
26
|
},
|
|
27
|
+
"tap": {
|
|
28
|
+
"check-coverage": false
|
|
29
|
+
},
|
|
27
30
|
"dependencies": {
|
|
31
|
+
"@babel/runtime": "7.21.0",
|
|
28
32
|
"arraybuffer-loader": "^1.0.3",
|
|
29
33
|
"base64-js": "1.3.0",
|
|
34
|
+
"cross-fetch": "3.1.5",
|
|
30
35
|
"fastestsmallesttextencoderdecoder": "^1.0.7",
|
|
31
36
|
"js-md5": "0.7.3",
|
|
32
37
|
"minilog": "3.1.0",
|
|
@@ -34,6 +39,7 @@
|
|
|
34
39
|
},
|
|
35
40
|
"devDependencies": {
|
|
36
41
|
"@babel/core": "7.14.8",
|
|
42
|
+
"@babel/plugin-transform-runtime": "7.21.0",
|
|
37
43
|
"@babel/polyfill": "7.12.1",
|
|
38
44
|
"@babel/preset-env": "7.14.8",
|
|
39
45
|
"@commitlint/cli": "8.2.0",
|
|
@@ -48,9 +54,8 @@
|
|
|
48
54
|
"file-loader": "4.1.0",
|
|
49
55
|
"husky": "1.3.1",
|
|
50
56
|
"json": "^9.0.4",
|
|
51
|
-
"node-fetch": "2.6.1",
|
|
52
57
|
"semantic-release": "^15.10.5",
|
|
53
|
-
"tap": "
|
|
58
|
+
"tap": "16.3.4",
|
|
54
59
|
"uglifyjs-webpack-plugin": "2.2.0",
|
|
55
60
|
"webpack": "4.46.0",
|
|
56
61
|
"webpack-cli": "3.1.2"
|
package/src/FetchTool.js
CHANGED
|
@@ -1,48 +1,52 @@
|
|
|
1
|
-
|
|
1
|
+
const {scratchFetch} = require('./scratchFetch');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Request & {withCredentials: boolean}} ScratchSendRequest
|
|
5
|
+
*/
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Get and send assets with the fetch standard web api.
|
|
5
9
|
*/
|
|
6
10
|
class FetchTool {
|
|
7
11
|
/**
|
|
8
|
-
* Is get supported?
|
|
12
|
+
* Is get supported?
|
|
13
|
+
* Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
|
|
9
14
|
* @returns {boolean} Is get supported?
|
|
10
15
|
*/
|
|
11
16
|
get isGetSupported () {
|
|
12
|
-
return
|
|
17
|
+
return true;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
21
|
* Request data from a server with fetch.
|
|
17
|
-
* @param {
|
|
18
|
-
* @
|
|
19
|
-
* @returns {Promise.<Uint8Array>} Resolve to Buffer of data from server.
|
|
22
|
+
* @param {Request} reqConfig - Request configuration for data to get.
|
|
23
|
+
* @returns {Promise.<Uint8Array?>} Resolve to Buffer of data from server.
|
|
20
24
|
*/
|
|
21
25
|
get ({url, ...options}) {
|
|
22
|
-
return
|
|
26
|
+
return scratchFetch(url, Object.assign({method: 'GET'}, options))
|
|
23
27
|
.then(result => {
|
|
24
28
|
if (result.ok) return result.arrayBuffer().then(b => new Uint8Array(b));
|
|
25
29
|
if (result.status === 404) return null;
|
|
26
|
-
return Promise.reject(result.status);
|
|
30
|
+
return Promise.reject(result.status); // TODO: we should throw a proper error
|
|
27
31
|
});
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
|
-
* Is sending supported?
|
|
32
|
-
*
|
|
35
|
+
* Is sending supported?
|
|
36
|
+
* Always true for `FetchTool` because `scratchFetch` ponyfills `fetch` if necessary.
|
|
33
37
|
* @returns {boolean} Is sending supported?
|
|
34
38
|
*/
|
|
35
39
|
get isSendSupported () {
|
|
36
|
-
return
|
|
40
|
+
return true;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
44
|
* Send data to a server with fetch.
|
|
41
|
-
* @param {
|
|
45
|
+
* @param {ScratchSendRequest} reqConfig - Request configuration for data to send.
|
|
42
46
|
* @returns {Promise.<string>} Server returned metadata.
|
|
43
47
|
*/
|
|
44
48
|
send ({url, withCredentials = false, ...options}) {
|
|
45
|
-
return
|
|
49
|
+
return scratchFetch(url, Object.assign({
|
|
46
50
|
credentials: withCredentials ? 'include' : 'omit'
|
|
47
51
|
}, options))
|
|
48
52
|
.then(response => {
|
package/src/FetchWorkerTool.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const {applyMetadata} = require('./scratchFetch');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Get and send assets with a worker that uses fetch.
|
|
3
5
|
*/
|
|
@@ -13,13 +15,13 @@ class PrivateFetchWorkerTool {
|
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* A possible error occurred standing up the worker.
|
|
16
|
-
* @type {
|
|
18
|
+
* @type {Error?}
|
|
17
19
|
*/
|
|
18
20
|
this._supportError = null;
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* The worker that runs fetch and returns data for us.
|
|
22
|
-
* @type {
|
|
24
|
+
* @type {Worker?}
|
|
23
25
|
*/
|
|
24
26
|
this.worker = null;
|
|
25
27
|
|
|
@@ -34,9 +36,9 @@ class PrivateFetchWorkerTool {
|
|
|
34
36
|
// eslint-disable-next-line global-require
|
|
35
37
|
const FetchWorker = require('worker-loader?{"inline":true,"fallback":true}!./FetchWorkerTool.worker');
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const worker = new FetchWorker();
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
worker.addEventListener('message', ({data}) => {
|
|
40
42
|
if (data.support) {
|
|
41
43
|
this._workerSupport = data.support;
|
|
42
44
|
return;
|
|
@@ -52,6 +54,8 @@ class PrivateFetchWorkerTool {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
});
|
|
57
|
+
|
|
58
|
+
this.worker = worker;
|
|
55
59
|
}
|
|
56
60
|
} catch (error) {
|
|
57
61
|
this._supportError = error;
|
|
@@ -78,17 +82,20 @@ class PrivateFetchWorkerTool {
|
|
|
78
82
|
* Request data from a server with a worker using fetch.
|
|
79
83
|
* @param {{url:string}} reqConfig - Request configuration for data to get.
|
|
80
84
|
* @param {{method:string}} options - Additional options to configure fetch.
|
|
81
|
-
* @returns {Promise.<Buffer>} Resolve to Buffer of data from server.
|
|
85
|
+
* @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server.
|
|
82
86
|
*/
|
|
83
87
|
get ({url, ...options}) {
|
|
84
88
|
return new Promise((resolve, reject) => {
|
|
85
89
|
// TODO: Use a Scratch standard ID generator ...
|
|
86
90
|
const id = Math.random().toString(16)
|
|
87
91
|
.substring(2);
|
|
92
|
+
const augmentedOptions = applyMetadata(
|
|
93
|
+
Object.assign({method: 'GET'}, options)
|
|
94
|
+
);
|
|
88
95
|
this.worker.postMessage({
|
|
89
96
|
id,
|
|
90
97
|
url,
|
|
91
|
-
options:
|
|
98
|
+
options: augmentedOptions
|
|
92
99
|
});
|
|
93
100
|
this.jobs[id] = {
|
|
94
101
|
id,
|
|
@@ -153,7 +160,7 @@ class PublicFetchWorkerTool {
|
|
|
153
160
|
/**
|
|
154
161
|
* Request data from a server with a worker that uses fetch.
|
|
155
162
|
* @param {{url:string}} reqConfig - Request configuration for data to get.
|
|
156
|
-
* @returns {Promise.<Buffer>} Resolve to Buffer of data from server.
|
|
163
|
+
* @returns {Promise.<Buffer|Uint8Array|null>} Resolve to Buffer of data from server.
|
|
157
164
|
*/
|
|
158
165
|
get (reqConfig) {
|
|
159
166
|
return this.inner.get(reqConfig);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* eslint-env worker */
|
|
2
2
|
|
|
3
|
+
const crossFetch = require('cross-fetch').default;
|
|
4
|
+
|
|
3
5
|
let jobsActive = 0;
|
|
4
6
|
const complete = [];
|
|
5
7
|
|
|
@@ -48,7 +50,7 @@ const onMessage = ({data: job}) => {
|
|
|
48
50
|
|
|
49
51
|
jobsActive++;
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
crossFetch(job.url, job.options)
|
|
52
54
|
.then(result => {
|
|
53
55
|
if (result.ok) return result.arrayBuffer();
|
|
54
56
|
if (result.status === 404) return null;
|
|
@@ -59,12 +61,6 @@ const onMessage = ({data: job}) => {
|
|
|
59
61
|
.then(() => jobsActive--);
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} else {
|
|
66
|
-
postMessage({support: {fetch: false}});
|
|
67
|
-
self.addEventListener('message', ({data: job}) => {
|
|
68
|
-
postMessage([{id: job.id, error: 'fetch is unavailable'}]);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
64
|
+
// crossFetch means "fetch" is now always supported
|
|
65
|
+
postMessage({support: {fetch: true}});
|
|
66
|
+
self.addEventListener('message', onMessage);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const {fetch, Headers} = require('cross-fetch');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Metadata header names
|
|
5
|
+
* @enum {string} The enum value is the name of the associated header.
|
|
6
|
+
* @readonly
|
|
7
|
+
*/
|
|
8
|
+
const RequestMetadata = {
|
|
9
|
+
/** The ID of the project associated with this request */
|
|
10
|
+
ProjectId: 'X-ProjectId',
|
|
11
|
+
/** The ID of the project run associated with this request */
|
|
12
|
+
RunId: 'X-RunId'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Metadata for requests
|
|
17
|
+
* @type {Map<string, string>}
|
|
18
|
+
*/
|
|
19
|
+
const metadata = new Map();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Non-destructively merge any metadata state (if any) with the provided options object (if any).
|
|
23
|
+
* If there is metadata state but no options object is provided, make a new object.
|
|
24
|
+
* If there is no metadata state, return the provided options parameter without modification.
|
|
25
|
+
* If there is metadata and an options object is provided, modify a copy and return it.
|
|
26
|
+
* Headers in the provided options object may override headers generated from metadata state.
|
|
27
|
+
* @param {RequestInit} [options] The initial request options. May be null or undefined.
|
|
28
|
+
* @returns {RequestInit|undefined} the provided options parameter without modification, or a new options object.
|
|
29
|
+
*/
|
|
30
|
+
const applyMetadata = options => {
|
|
31
|
+
if (metadata.size > 0) {
|
|
32
|
+
const augmentedOptions = Object.assign({}, options);
|
|
33
|
+
augmentedOptions.headers = new Headers(Array.from(metadata));
|
|
34
|
+
if (options && options.headers) {
|
|
35
|
+
const overrideHeaders =
|
|
36
|
+
options.headers instanceof Headers ? options.headers : new Headers(options.headers);
|
|
37
|
+
for (const [name, value] of overrideHeaders.entries()) {
|
|
38
|
+
augmentedOptions.headers.set(name, value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return augmentedOptions;
|
|
42
|
+
}
|
|
43
|
+
return options;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Make a network request.
|
|
48
|
+
* This is a wrapper for the global fetch method, adding some Scratch-specific functionality.
|
|
49
|
+
* @param {RequestInfo|URL} resource The resource to fetch.
|
|
50
|
+
* @param {RequestInit} options Optional object containing custom settings for this request.
|
|
51
|
+
* @see {@link https://developer.mozilla.org/docs/Web/API/fetch} for more about the fetch API.
|
|
52
|
+
* @returns {Promise<Response>} A promise for the response to the request.
|
|
53
|
+
*/
|
|
54
|
+
const scratchFetch = (resource, options) => {
|
|
55
|
+
const augmentedOptions = applyMetadata(options);
|
|
56
|
+
return fetch(resource, augmentedOptions);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set the value of a named request metadata item.
|
|
61
|
+
* Setting the value to `null` or `undefined` will NOT remove the item.
|
|
62
|
+
* Use `unsetMetadata` for that.
|
|
63
|
+
* @param {RequestMetadata} name The name of the metadata item to set.
|
|
64
|
+
* @param {any} value The value to set (will be converted to a string).
|
|
65
|
+
*/
|
|
66
|
+
const setMetadata = (name, value) => {
|
|
67
|
+
metadata.set(name, value);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remove a named request metadata item.
|
|
72
|
+
* @param {RequestMetadata} name The name of the metadata item to remove.
|
|
73
|
+
*/
|
|
74
|
+
const unsetMetadata = name => {
|
|
75
|
+
metadata.delete(name);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
default: scratchFetch,
|
|
80
|
+
|
|
81
|
+
RequestMetadata,
|
|
82
|
+
applyMetadata,
|
|
83
|
+
scratchFetch,
|
|
84
|
+
setMetadata,
|
|
85
|
+
unsetMetadata
|
|
86
|
+
};
|
|
@@ -20,16 +20,17 @@ test('constructor', t => {
|
|
|
20
20
|
* @property {DataFormat} [ext] - Optional: the asset's data format / file extension.
|
|
21
21
|
*/
|
|
22
22
|
const testAssets = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
// TODO: mock project download, since we can no longer download projects directly
|
|
24
|
+
// {
|
|
25
|
+
// type: storage.AssetType.Project,
|
|
26
|
+
// id: '117504922',
|
|
27
|
+
// md5: null // don't check MD5 for project without revision ID
|
|
28
|
+
// },
|
|
29
|
+
// {
|
|
30
|
+
// type: storage.AssetType.Project,
|
|
31
|
+
// id: '117504922.d6ae1ffb76f2bc83421cd3f40fc4fd57',
|
|
32
|
+
// md5: '1225460702e149727de28bff4cfd9e23'
|
|
33
|
+
// },
|
|
33
34
|
{
|
|
34
35
|
type: storage.AssetType.ImageVector,
|
|
35
36
|
id: 'f88bf1935daea28f8ca098462a31dbb0', // cat1-a
|
|
@@ -65,9 +66,9 @@ const testAssets = [
|
|
|
65
66
|
}
|
|
66
67
|
];
|
|
67
68
|
|
|
68
|
-
test('
|
|
69
|
+
test('addWebStore', t => {
|
|
69
70
|
t.doesNotThrow(() => {
|
|
70
|
-
storage.
|
|
71
|
+
storage.addWebStore(
|
|
71
72
|
[storage.AssetType.Project],
|
|
72
73
|
asset => {
|
|
73
74
|
const idParts = asset.assetId.split('.');
|
|
@@ -77,7 +78,7 @@ test('addWebSource', t => {
|
|
|
77
78
|
});
|
|
78
79
|
});
|
|
79
80
|
t.doesNotThrow(() => {
|
|
80
|
-
storage.
|
|
81
|
+
storage.addWebStore(
|
|
81
82
|
[storage.AssetType.ImageVector, storage.AssetType.ImageBitmap, storage.AssetType.Sound],
|
|
82
83
|
asset => `https://cdn.assets.scratch.mit.edu/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`
|
|
83
84
|
);
|
|
@@ -86,29 +87,25 @@ test('addWebSource', t => {
|
|
|
86
87
|
});
|
|
87
88
|
|
|
88
89
|
test('load', t => {
|
|
89
|
-
const
|
|
90
|
-
|
|
90
|
+
const assetChecks = testAssets.map(async assetInfo => {
|
|
91
|
+
const asset = await storage.load(assetInfo.type, assetInfo.id, assetInfo.ext)
|
|
92
|
+
.catch(e => {
|
|
93
|
+
// tap's output isn't great if we just let it catch the unhandled promise rejection
|
|
94
|
+
// wrapping it like this makes a failure much easier to read in the test output
|
|
95
|
+
throw new Error(`failed to load ${assetInfo.type.name} asset with id=${assetInfo.id} (e=${e})`);
|
|
96
|
+
});
|
|
91
97
|
t.type(asset, storage.Asset);
|
|
92
|
-
t.
|
|
93
|
-
t.
|
|
98
|
+
t.equal(asset.assetId, assetInfo.id);
|
|
99
|
+
t.equal(asset.assetType, assetInfo.type);
|
|
94
100
|
t.ok(asset.data.length);
|
|
95
101
|
|
|
96
102
|
// Web assets should come back as clean
|
|
97
|
-
t.
|
|
103
|
+
t.ok(asset.clean);
|
|
98
104
|
|
|
99
105
|
if (assetInfo.md5) {
|
|
100
|
-
t.
|
|
106
|
+
t.equal(md5(asset.data), assetInfo.md5);
|
|
101
107
|
}
|
|
102
|
-
};
|
|
103
|
-
for (let i = 0; i < testAssets.length; ++i) {
|
|
104
|
-
const assetInfo = testAssets[i];
|
|
105
|
-
|
|
106
|
-
let promise = storage.load(assetInfo.type, assetInfo.id, assetInfo.ext);
|
|
107
|
-
t.type(promise, 'Promise');
|
|
108
|
-
|
|
109
|
-
promise = promise.then(asset => checkAsset(assetInfo, asset));
|
|
110
|
-
promises.push(promise);
|
|
111
|
-
}
|
|
108
|
+
});
|
|
112
109
|
|
|
113
|
-
return Promise.all(
|
|
110
|
+
return Promise.all(assetChecks);
|
|
114
111
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const TextEncoder = require('util').TextEncoder;
|
|
2
|
+
const {Headers} = require('cross-fetch');
|
|
3
|
+
|
|
4
|
+
const successText = 'successful response';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef MockFetchResponse The Response-like object returned by mockFetch.
|
|
8
|
+
* @property {boolean} ok True if the simulated request was successful, false otherwise.
|
|
9
|
+
* @property {number} status The HTTP status code of the simulated request.
|
|
10
|
+
* @property {() => Promise<string>} [text] A success string if the simulated request succeeded, undefined otherwise.
|
|
11
|
+
* @property {() => Promise<Uint8Array>} [arrayBuffer] Same as `text`, but encoded with UTF-8 if present.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {RequestInit & {mockFetchTestData: MockFetchTestData}} MockFetchRequestInit
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef MockFetchTestData
|
|
20
|
+
* @property {Headers} [headers] A Headers object initialized with the header info received by mockFetch.
|
|
21
|
+
* @property {Number} [headersCount] The number of headers in the 'headers' property.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mock the 'fetch' method from browsers.
|
|
26
|
+
* @param {RequestInfo|URL} resource The (mock) resource to fetch, which will determine the response.
|
|
27
|
+
* @param {MockFetchRequestInit} [options] Optional object containing custom settings for this request.
|
|
28
|
+
* @returns {Promise<MockFetchResponse>} A promise for a Response-like object. Does not fully implement Response.
|
|
29
|
+
*/
|
|
30
|
+
const mockFetch = (resource, options) => {
|
|
31
|
+
/** @type MockFetchResponse */
|
|
32
|
+
const results = {
|
|
33
|
+
ok: false,
|
|
34
|
+
status: 0
|
|
35
|
+
};
|
|
36
|
+
if (options?.mockFetchTestData) {
|
|
37
|
+
options.mockFetchTestData.headers = new Headers(options.headers);
|
|
38
|
+
options.mockFetchTestData.headersCount = Array.from(options.mockFetchTestData.headers).length;
|
|
39
|
+
}
|
|
40
|
+
switch (resource) {
|
|
41
|
+
case '200':
|
|
42
|
+
results.ok = true;
|
|
43
|
+
results.status = 200;
|
|
44
|
+
results.text = () => Promise.resolve(successText);
|
|
45
|
+
results.arrayBuffer = () => Promise.resolve(new TextEncoder().encode(successText));
|
|
46
|
+
break;
|
|
47
|
+
case '404':
|
|
48
|
+
results.ok = false;
|
|
49
|
+
results.status = 404;
|
|
50
|
+
break;
|
|
51
|
+
case '500':
|
|
52
|
+
results.ok = false;
|
|
53
|
+
results.status = 500;
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
throw new Error('unimplemented');
|
|
57
|
+
}
|
|
58
|
+
return Promise.resolve(results);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
mockFetch,
|
|
63
|
+
successText
|
|
64
|
+
};
|
package/test/unit/fetch-tool.js
CHANGED
|
@@ -1,77 +1,60 @@
|
|
|
1
|
-
const
|
|
2
|
-
const TextEncoder = require('util').TextEncoder;
|
|
1
|
+
const tap = require('tap');
|
|
3
2
|
const TextDecoder = require('util').TextDecoder;
|
|
4
3
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const {mockFetch, successText} = require('../mocks/mockFetch.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This is the real FetchTool, but the 'cross-fetch' module has been replaced with the mockFetch function.
|
|
8
|
+
* @type {typeof import('../../src/FetchTool')}
|
|
9
|
+
*/
|
|
10
|
+
const FetchTool = tap.mock('../../src/FetchTool', {
|
|
11
|
+
'cross-fetch': {
|
|
12
|
+
default: mockFetch,
|
|
13
|
+
fetch: mockFetch
|
|
14
|
+
}
|
|
15
|
+
});
|
|
12
16
|
|
|
17
|
+
tap.test('send success returns response.text()', t => {
|
|
13
18
|
const tool = new FetchTool();
|
|
14
|
-
|
|
19
|
+
|
|
15
20
|
return t.resolves(
|
|
16
|
-
tool.send(
|
|
17
|
-
t.equal(result,
|
|
21
|
+
tool.send({url: '200'}).then(result => {
|
|
22
|
+
t.equal(result, successText);
|
|
18
23
|
})
|
|
19
24
|
);
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
test('send failure returns response.status', t => {
|
|
23
|
-
global.fetch = () => Promise.resolve({
|
|
24
|
-
ok: false,
|
|
25
|
-
status: 500
|
|
26
|
-
});
|
|
27
|
-
|
|
27
|
+
tap.test('send failure returns response.status', t => {
|
|
28
28
|
const tool = new FetchTool();
|
|
29
29
|
|
|
30
|
-
return t.rejects(tool.send(
|
|
30
|
+
return t.rejects(tool.send({url: '500'}), 500);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test('get success returns Uint8Array.body(response.arrayBuffer())', t => {
|
|
34
|
-
const text = 'successful response';
|
|
33
|
+
tap.test('get success returns Uint8Array.body(response.arrayBuffer())', t => {
|
|
35
34
|
const encoding = 'utf-8';
|
|
36
|
-
const encoded = new TextEncoder().encode(text);
|
|
37
35
|
const decoder = new TextDecoder(encoding);
|
|
38
36
|
|
|
39
|
-
global.fetch = () => Promise.resolve({
|
|
40
|
-
ok: true,
|
|
41
|
-
arrayBuffer: () => Promise.resolve(encoded.buffer)
|
|
42
|
-
});
|
|
43
|
-
|
|
44
37
|
const tool = new FetchTool();
|
|
45
|
-
|
|
38
|
+
|
|
46
39
|
return t.resolves(
|
|
47
|
-
tool.get({url: '
|
|
48
|
-
t.equal(decoder.decode(result),
|
|
40
|
+
tool.get({url: '200'}).then(result => {
|
|
41
|
+
t.equal(decoder.decode(result), successText);
|
|
49
42
|
})
|
|
50
43
|
);
|
|
51
44
|
});
|
|
52
45
|
|
|
53
|
-
test('get with 404 response returns null data', t => {
|
|
54
|
-
global.fetch = () => Promise.resolve({
|
|
55
|
-
ok: false,
|
|
56
|
-
status: 404
|
|
57
|
-
});
|
|
58
|
-
|
|
46
|
+
tap.test('get with 404 response returns null data', t => {
|
|
59
47
|
const tool = new FetchTool();
|
|
60
48
|
|
|
61
49
|
return t.resolves(
|
|
62
|
-
tool.get(
|
|
50
|
+
tool.get({url: '404'}).then(result => {
|
|
63
51
|
t.equal(result, null);
|
|
64
52
|
})
|
|
65
53
|
);
|
|
66
54
|
});
|
|
67
55
|
|
|
68
|
-
test('get failure returns response.status', t => {
|
|
69
|
-
global.fetch = () => Promise.resolve({
|
|
70
|
-
ok: false,
|
|
71
|
-
status: 500
|
|
72
|
-
});
|
|
73
|
-
|
|
56
|
+
tap.test('get failure returns response.status', t => {
|
|
74
57
|
const tool = new FetchTool();
|
|
75
58
|
|
|
76
|
-
return t.rejects(tool.get({url: '
|
|
59
|
+
return t.rejects(tool.get({url: '500'}), 500);
|
|
77
60
|
});
|