diff --git a/.storybook/preview.js b/.storybook/preview.js index 07768fd8a..a6bf329ea 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -36,7 +36,8 @@ const pinia = createPinia(); // Initialize MSW initialize({ - onUnhandledRequest: ({method, url}) => { + /** To be migrated to msw2 if neede */ + /*onUnhandledRequest: ({method, url}) => { if (url.pathname.includes('://mock/')) { console.error(`Unhandled ${method} request to ${url}. @@ -45,7 +46,7 @@ initialize({ If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses `); } - }, + },*/ }); setup((app) => { diff --git a/.storybook/public/mockServiceWorker.js b/.storybook/public/mockServiceWorker.js index 51d85eeeb..e369128ec 100644 --- a/.storybook/public/mockServiceWorker.js +++ b/.storybook/public/mockServiceWorker.js @@ -2,13 +2,14 @@ /* tslint:disable */ /** - * Mock Service Worker (1.3.2). + * Mock Service Worker (2.0.11). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. */ -const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() self.addEventListener('install', function () { @@ -86,12 +87,6 @@ self.addEventListener('message', async function (event) { self.addEventListener('fetch', function (event) { const { request } = event - const accept = request.headers.get('accept') || '' - - // Bypass server-sent events. - if (accept.includes('text/event-stream')) { - return - } // Bypass navigation requests. if (request.mode === 'navigate') { @@ -112,29 +107,8 @@ self.addEventListener('fetch', function (event) { } // Generate unique request ID. - const requestId = Math.random().toString(16).slice(2) - - event.respondWith( - handleRequest(event, requestId).catch((error) => { - if (error.name === 'NetworkError') { - console.warn( - '[MSW] Successfully emulated a network error for the "%s %s" request.', - request.method, - request.url, - ) - return - } - - // At this point, any exception indicates an issue with the original request/response. - console.error( - `\ -[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, - request.method, - request.url, - `${error.name}: ${error.message}`, - ) - }), - ) + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) }) async function handleRequest(event, requestId) { @@ -146,21 +120,24 @@ async function handleRequest(event, requestId) { // this message will pend indefinitely. if (client && activeClientIds.has(client.id)) { ;(async function () { - const clonedResponse = response.clone() - sendToClient(client, { - type: 'RESPONSE', - payload: { - requestId, - type: clonedResponse.type, - ok: clonedResponse.ok, - status: clonedResponse.status, - statusText: clonedResponse.statusText, - body: - clonedResponse.body === null ? null : await clonedResponse.text(), - headers: Object.fromEntries(clonedResponse.headers.entries()), - redirected: clonedResponse.redirected, + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, }, - }) + [responseClone.body], + ) })() } @@ -196,20 +173,20 @@ async function resolveMainClient(event) { async function getResponse(event, client, requestId) { const { request } = event - const clonedRequest = request.clone() + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() function passthrough() { - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const headers = Object.fromEntries(clonedRequest.headers.entries()) + const headers = Object.fromEntries(requestClone.headers.entries()) - // Remove MSW-specific request headers so the bypassed requests - // comply with the server's CORS preflight check. - // Operate with the headers as an object because request "Headers" - // are immutable. - delete headers['x-msw-bypass'] + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] - return fetch(clonedRequest, { headers }) + return fetch(requestClone, { headers }) } // Bypass mocking when the client is not active. @@ -227,31 +204,36 @@ async function getResponse(event, client, requestId) { // Bypass requests with the explicit bypass header. // Such requests can be issued by "ctx.fetch()". - if (request.headers.get('x-msw-bypass') === 'true') { + const mswIntention = request.headers.get('x-msw-intention') + if (['bypass', 'passthrough'].includes(mswIntention)) { return passthrough() } // Notify the client that a request has been intercepted. - const clientMessage = await sendToClient(client, { - type: 'REQUEST', - payload: { - id: requestId, - url: request.url, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - mode: request.mode, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: await request.text(), - bodyUsed: request.bodyUsed, - keepalive: request.keepalive, + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, }, - }) + [requestBuffer], + ) switch (clientMessage.type) { case 'MOCK_RESPONSE': { @@ -261,21 +243,12 @@ async function getResponse(event, client, requestId) { case 'MOCK_NOT_FOUND': { return passthrough() } - - case 'NETWORK_ERROR': { - const { name, message } = clientMessage.data - const networkError = new Error(message) - networkError.name = name - - // Rejecting a "respondWith" promise emulates a network error. - throw networkError - } } return passthrough() } -function sendToClient(client, message) { +function sendToClient(client, message, transferrables = []) { return new Promise((resolve, reject) => { const channel = new MessageChannel() @@ -287,17 +260,28 @@ function sendToClient(client, message) { resolve(event.data) } - client.postMessage(message, [channel.port2]) + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) }) } -function sleep(timeMs) { - return new Promise((resolve) => { - setTimeout(resolve, timeMs) +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, }) -} -async function respondWithMock(response) { - await sleep(response.delay) - return new Response(response.body, response) + return mockedResponse } diff --git a/package-lock.json b/package-lock.json index 864bcbb43..4df24fc21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,8 +55,8 @@ "husky": "^8.0.3", "less": "^4.2.0", "lint-staged": "^14.0.1", - "msw": "^1.3.2", - "msw-storybook-addon": "^1.10.0", + "msw": "^2.0.11", + "msw-storybook-addon": "^2.0.0--canary.122.b3ed3b1.0", "postcss": "^8.4.27", "prettier": "^3.0.2", "prettier-plugin-tailwindcss": "^0.5.6", @@ -2162,6 +2162,33 @@ "node": ">=6.9.0" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/js-levenshtein": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/js-levenshtein/-/js-levenshtein-2.0.1.tgz", + "integrity": "sha512-DERMS3yfbAljKsQc0U2wcqGKUWpdFjwqWuoMugEJlqBnKO180/n+4SR/J8MRDt1AN48X1ovgoD9KrdVXcaa3Rg==", + "dev": true, + "dependencies": { + "js-levenshtein": "^1.1.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3009,44 +3036,29 @@ } }, "node_modules/@mswjs/cookies": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-0.2.2.tgz", - "integrity": "sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", + "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", "dev": true, - "dependencies": { - "@types/set-cookie-parser": "^2.4.0", - "set-cookie-parser": "^2.4.6" - }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@mswjs/interceptors": { - "version": "0.17.10", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.17.10.tgz", - "integrity": "sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw==", + "version": "0.25.13", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.25.13.tgz", + "integrity": "sha512-xfjR81WwXPHwhDbqJRHlxYmboJuiSaIKpP4I5TJVFl/EmByOU13jOBT9hmEnxcjR3jvFYoqoNKt7MM9uqerj9A==", "dev": true, "dependencies": { - "@open-draft/until": "^1.0.3", - "@types/debug": "^4.1.7", - "@xmldom/xmldom": "^0.8.3", - "debug": "^4.3.3", - "headers-polyfill": "3.2.5", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", "outvariant": "^1.2.1", - "strict-event-emitter": "^0.2.4", - "web-encoding": "^1.1.5" + "strict-event-emitter": "^0.5.1" }, "engines": { - "node": ">=14" - } - }, - "node_modules/@mswjs/interceptors/node_modules/strict-event-emitter": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", - "integrity": "sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==", - "dev": true, - "dependencies": { - "events": "^3.3.0" + "node": ">=18" } }, "node_modules/@ndelangen/get-tarball": { @@ -3095,10 +3107,26 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, "node_modules/@open-draft/until": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", - "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true }, "node_modules/@pkgjs/parseargs": { @@ -5470,21 +5498,18 @@ "@types/node": "*" } }, - "node_modules/@types/set-cookie-parser": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.7.tgz", - "integrity": "sha512-+ge/loa0oTozxip6zmhRIk8Z/boU51wl9Q6QdLZcokIGMzY5lFXYy/x7Htj2HTC6/KZP1hUbZ1ekx8DYXICvWg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/sortablejs": { "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.7.tgz", "integrity": "sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==", "peer": true }, + "node_modules/@types/statuses": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.4.tgz", + "integrity": "sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==", + "dev": true + }, "node_modules/@types/unist": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", @@ -6095,15 +6120,6 @@ } } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@yarnpkg/esbuild-plugin-pnp": { "version": "3.0.0-rc.15", "resolved": "https://registry.npmjs.org/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.15.tgz", @@ -6157,13 +6173,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "dev": true, - "optional": true - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -8697,15 +8706,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -9782,9 +9782,9 @@ } }, "node_modules/headers-polyfill": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.2.5.tgz", - "integrity": "sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz", + "integrity": "sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw==", "dev": true }, "node_modules/highlight.js": { @@ -12560,29 +12560,31 @@ "dev": true }, "node_modules/msw": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/msw/-/msw-1.3.2.tgz", - "integrity": "sha512-wKLhFPR+NitYTkQl5047pia0reNGgf0P6a1eTnA5aNlripmiz0sabMvvHcicE8kQ3/gZcI0YiPFWmYfowfm3lA==", + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.0.11.tgz", + "integrity": "sha512-dAXFS2DxZX0uFqMPhS3oUAu8S/5IQ5qKKSwtXl3/dMTeML0C8JfSvbeWtowYg6pu4Iehgp5L/pHLrlIcG++y/A==", "dev": true, "hasInstallScript": true, "dependencies": { - "@mswjs/cookies": "^0.2.2", - "@mswjs/interceptors": "^0.17.10", - "@open-draft/until": "^1.0.3", + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/js-levenshtein": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.25.13", + "@open-draft/until": "^2.1.0", "@types/cookie": "^0.4.1", "@types/js-levenshtein": "^1.1.1", - "chalk": "^4.1.1", + "@types/statuses": "^2.0.1", + "chalk": "^4.1.2", "chokidar": "^3.4.2", - "cookie": "^0.4.2", "graphql": "^16.8.1", - "headers-polyfill": "3.2.5", + "headers-polyfill": "^4.0.1", "inquirer": "^8.2.0", "is-node-process": "^1.2.0", "js-levenshtein": "^1.1.6", - "node-fetch": "^2.6.7", "outvariant": "^1.4.0", "path-to-regexp": "^6.2.0", - "strict-event-emitter": "^0.4.3", + "strict-event-emitter": "^0.5.0", "type-fest": "^2.19.0", "yargs": "^17.3.1" }, @@ -12590,14 +12592,14 @@ "msw": "cli/index.js" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mswjs" }, "peerDependencies": { - "typescript": ">= 4.4.x <= 5.2.x" + "typescript": ">= 4.7.x <= 5.2.x" }, "peerDependenciesMeta": { "typescript": { @@ -12606,24 +12608,15 @@ } }, "node_modules/msw-storybook-addon": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-1.10.0.tgz", - "integrity": "sha512-soCTMTf7DnLeaMnFHPrtVgbyeFTJALVvnDHpzzXpJad+HOzJgQdwU4EAzVfDs1q+X5cVEgxOdAhSMC7ljvnSXg==", + "version": "2.0.0--canary.122.b3ed3b1.0", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.0--canary.122.b3ed3b1.0.tgz", + "integrity": "sha512-HZn9B6MCdfHpgm5wQ92A0K3moXDCDTxPRjWWH6C/4myg3KmsD0kwiaE14vWO49A4+TG10yXMGqIll2NjIMtnQg==", "dev": true, "dependencies": { "is-node-process": "^1.0.1" }, "peerDependencies": { - "msw": ">=0.35.0 <2.0.0" - } - }, - "node_modules/msw/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "dev": true, - "engines": { - "node": ">= 0.6" + "msw": "^2.0.0" } }, "node_modules/msw/node_modules/path-to-regexp": { @@ -13057,9 +13050,9 @@ } }, "node_modules/outvariant": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", - "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", "dev": true }, "node_modules/p-limit": { @@ -14763,12 +14756,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true - }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -15088,9 +15075,9 @@ "dev": true }, "node_modules/strict-event-emitter": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.4.6.tgz", - "integrity": "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", "dev": true }, "node_modules/string_decoder": { @@ -17805,18 +17792,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "dev": true, - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index 77472760b..44d02504e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "chromatic": "npx chromatic --project-token chpt_d560551f1e4c0c7", - "unittest": "vitest" + "test": "vitest" }, "dependencies": { "@headlessui/vue": "^1.7.16", @@ -61,8 +61,8 @@ "husky": "^8.0.3", "less": "^4.2.0", "lint-staged": "^14.0.1", - "msw": "^1.3.2", - "msw-storybook-addon": "^1.10.0", + "msw": "^2.0.11", + "msw-storybook-addon": "^2.0.0--canary.122.b3ed3b1.0", "postcss": "^8.4.27", "prettier": "^3.0.2", "prettier-plugin-tailwindcss": "^0.5.6", diff --git a/src/components/Container/ManageEmailsPage.stories.js b/src/components/Container/ManageEmailsPage.stories.js index bcf2189e2..05277005c 100644 --- a/src/components/Container/ManageEmailsPage.stories.js +++ b/src/components/Container/ManageEmailsPage.stories.js @@ -1,4 +1,4 @@ -import {rest} from 'msw'; +import {http, HttpResponse} from 'msw'; import MailableMock from '@/mocks/mailable.json'; import MailablesMock from '@/mocks/mailables.json'; @@ -217,10 +217,10 @@ export const Default = { parameters: { msw: { handlers: [ - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/mailable/*', - async (req, res, ctx) => { - return res(ctx.json(MailableMock)); + async (r) => { + return HttpResponse.json(MailableMock); }, ), ], diff --git a/src/components/FileAttacher/FileAttacher.stories.js b/src/components/FileAttacher/FileAttacher.stories.js index 35bfaafc7..7b7bf4a15 100644 --- a/src/components/FileAttacher/FileAttacher.stories.js +++ b/src/components/FileAttacher/FileAttacher.stories.js @@ -1,9 +1,8 @@ import {ref} from 'vue'; -import {rest} from 'msw'; +import {http, HttpResponse, delay} from 'msw'; import FileAttacher from './FileAttacher.vue'; import fileAttachers from '@/docs/data/fileAttachers'; import submissionFiles from '@/docs/data/submissionFiles'; -const delay = (time) => new Promise((resolve) => setTimeout(resolve, time)); export default { title: 'Components/FileAttacher', @@ -26,9 +25,9 @@ export const Default = { parameters: { msw: { handlers: [ - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/submissions/26/files', - async (req, res, ctx) => { + async () => { await delay(500); const files = [ @@ -42,12 +41,12 @@ export const Default = { }, ]; - return res(ctx.json({itemsMax: files.length, items: files})); + return HttpResponse.json({itemsMax: files.length, items: files}); }, ), - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/_library', - async (req, res, ctx) => { + async () => { await delay(500); const file = { @@ -76,7 +75,7 @@ export const Default = { }, ]; - return res(ctx.json({itemsMax: files.length, items: files})); + return HttpResponse.json({itemsMax: files.length, items: files}); }, ), ], diff --git a/src/components/FileUploader/FileUploader.stories.js b/src/components/FileUploader/FileUploader.stories.js index c871d2961..3e6bf9e5c 100644 --- a/src/components/FileUploader/FileUploader.stories.js +++ b/src/components/FileUploader/FileUploader.stories.js @@ -2,7 +2,7 @@ import {ref} from 'vue'; import FileUploader from './FileUploader.vue'; import FileUploadProgress from '@/components/FileUploadProgress/FileUploadProgress.vue'; import dropzoneOptions from '@/docs/data/dropzoneOptions'; -import {rest} from 'msw'; +import {http, HttpResponse} from 'msw'; import './FileUploader.stories.less'; @@ -72,17 +72,15 @@ export const Default = { // http://localhost:7003/index.php/publicknowledge/api/v1/temporaryFiles msw: { handlers: [ - rest.post( + http.post( 'https://mock/index.php/publicknowledge/api/v1/temporaryFiles', - async (req, res, ctx) => { - return res( - ctx.json({ - id: 5, - name: 'i-see-you-theme-from-avatar-violin-1.pdf', - mimetype: 'application/pdf', - documentType: 'pdf', - }), - ); + async () => { + return HttpResponse.json({ + id: 5, + name: 'i-see-you-theme-from-avatar-violin-1.pdf', + mimetype: 'application/pdf', + documentType: 'pdf', + }); }, ), ], diff --git a/src/components/Filter/FilterAutosuggest.stories.js b/src/components/Filter/FilterAutosuggest.stories.js index be8a1e897..9797af269 100644 --- a/src/components/Filter/FilterAutosuggest.stories.js +++ b/src/components/Filter/FilterAutosuggest.stories.js @@ -1,5 +1,5 @@ import {ref} from 'vue'; -import {rest} from 'msw'; +import {http, HttpResponse} from 'msw'; import FilterAutosuggest from './FilterAutosuggest.vue'; import fieldBase from '@/docs/components/Form/helpers/field-base'; import fieldBaseAutosuggest from '@/docs/components/Form/helpers/field-autosuggest'; @@ -78,16 +78,16 @@ export const Autosuggest = { parameters: { msw: { handlers: [ - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/users', - async (req, res, ctx) => { - return res(ctx.json(UsernamesMock)); + async () => { + return HttpResponse.json(UsernamesMock); }, ), - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/issues', - async (req, res, ctx) => { - return res(ctx.json(IssuesMock)); + async () => { + return HttpResponse.json(IssuesMock); }, ), ], diff --git a/src/components/Form/fields/FieldBaseAutosuggest.stories.js b/src/components/Form/fields/FieldBaseAutosuggest.stories.js index de8aca928..f69830ec8 100644 --- a/src/components/Form/fields/FieldBaseAutosuggest.stories.js +++ b/src/components/Form/fields/FieldBaseAutosuggest.stories.js @@ -1,4 +1,4 @@ -import {rest} from 'msw'; +import {http, HttpResponse} from 'msw'; import FieldBaseAutosuggest from './FieldBaseAutosuggest.vue'; import FieldBaseMock from '../mocks/field-base'; @@ -29,10 +29,10 @@ export default { parameters: { msw: { handlers: [ - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/users', async (req, res, ctx) => { - return res(ctx.json(UsernamesMock)); + return HttpResponse.json(UsernamesMock); }, ), ], diff --git a/src/composables/useFetch.js b/src/composables/useFetch.js index 65326d9f8..daddb47ef 100644 --- a/src/composables/useFetch.js +++ b/src/composables/useFetch.js @@ -1,7 +1,18 @@ import {ref, unref} from 'vue'; -import {ofetch} from 'ofetch'; +import {ofetch, createFetch} from 'ofetch'; import {useDialogStore} from '@/stores/dialogStore'; + +let ofetchInstance = ofetch; export function useFetch(url, options) { + /** + * Workaround for testing https://github.com/unjs/ofetch/issues/295 + * Can be removed once issue is addressed + * (likely getting fetch instance in runtime) + * */ + if (typeof process !== 'undefined' && process?.env?.VITEST == 'true') { + ofetchInstance = createFetch(); + } + const dialogStore = useDialogStore(); const isLoading = ref(false); const data = ref(null); @@ -17,11 +28,11 @@ export function useFetch(url, options) { const signal = lastRequestController.signal; - const opts = options; + const opts = {...options, signal}; isLoading.value = true; try { - const result = await ofetch(unref(url), opts); + const result = await ofetchInstance(unref(url), opts); data.value = result; } catch (e) { data.value = null; diff --git a/src/composables/useFetch.test.js b/src/composables/useFetch.test.js new file mode 100644 index 000000000..76b50a8be --- /dev/null +++ b/src/composables/useFetch.test.js @@ -0,0 +1,73 @@ +import { + describe, + test, + afterAll, + afterEach, + beforeAll, + beforeEach, + expect, +} from 'vitest'; +import {setupServer} from 'msw/node'; +import {ref} from 'vue'; +import {HttpResponse, http, delay} from 'msw'; + +import {setActivePinia, createPinia} from 'pinia'; + +import {useFetch} from './useFetch'; +import {useDialogStore} from '@/stores/dialogStore'; + +export const restHandlers = [ + http.get('http://mock/delayed', async ({request}) => { + const url = new URL(request.url); + + const id = parseInt(url.searchParams.get('id')); + await delay(id * 200); + + return HttpResponse.json({id}); + }), + + http.get('http://mock/status500', async ({request}) => { + return new HttpResponse(null, {status: 500}); + }), +]; + +const server = setupServer(...restHandlers); + +// Start server before all tests +beforeAll(() => server.listen({onUnhandledRequest: 'error'})); + +// Close server after all tests +afterAll(() => server.close()); + +// Reset handlers after each test `important for test isolation` +afterEach(() => server.resetHandlers()); + +beforeEach(() => { + setActivePinia(createPinia()); +}); + +describe('useFetch', () => { + test('last request data is used, previous are aborted', async () => { + const url = ref('http://mock/delayed?id=5'); + const {data, fetch} = useFetch(url); + const longFetch = fetch(); + + url.value = 'http://mock/delayed?id=1'; + const shortFetch = fetch(); + await Promise.all([longFetch, shortFetch]); + expect(data.value).toStrictEqual({id: 1}); + }); + + test('network dialog error is displayed if there is http code other than 2XX', async () => { + const url = ref('http://mock/status500'); + const dialogStore = useDialogStore(); + expect(dialogStore.dialogOpened).toBe(false); + + const {fetch} = useFetch(url); + await fetch(); + + expect(dialogStore.dialogOpened).toBe(true); + dialogStore.closeDialog(); + expect(dialogStore.dialogOpened).toBe(false); + }); +}); diff --git a/src/pages/example/ExamplePage.mdx b/src/pages/example/ExamplePage.mdx index 8530cabe0..cf79866f0 100644 --- a/src/pages/example/ExamplePage.mdx +++ b/src/pages/example/ExamplePage.mdx @@ -1,3 +1,9 @@ +import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks'; + +import * as ExamplePageStories from './ExamplePage.stories.js'; + + + # Example page These principles applies for version 3.5 and higher, when building new pages or making significant changes to existing ones. @@ -33,7 +39,9 @@ State management is intentionally done via [pinia store](../?path=/docs/guide-vu ## Server side configuration -On initial page load, there is still opportunity to pass JS object from PHP to the Vue.js. +On initial page load, there is still opportunity to pass JS object from PHP to the Vue.js. Best is to express individual items as props, so it can be easily displayed in storybook and its well documented: + + This might include things like: diff --git a/src/pages/example/ExamplePage.stories.js b/src/pages/example/ExamplePage.stories.js index bcb8dca0e..f1a6c01ef 100644 --- a/src/pages/example/ExamplePage.stories.js +++ b/src/pages/example/ExamplePage.stories.js @@ -1,8 +1,6 @@ import ExamplePage from './ExamplePage.vue'; import SubmissionsMock from './mocks/submissions25'; -import {rest} from 'msw'; - -const delay = (time) => new Promise((resolve) => setTimeout(resolve, time)); +import {http, HttpResponse, delay} from 'msw'; export default { title: 'Pages/Example', @@ -26,12 +24,12 @@ export const ExamplePage1 = { parameters: { msw: { handlers: [ - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/_submissions', - async (req, res, ctx) => { + async (r) => { await delay(500); - return res(ctx.json(SubmissionsMock)); + return HttpResponse.json(SubmissionsMock); }, ), ], diff --git a/src/pages/submissions/SubmissionsPage.stories.js b/src/pages/submissions/SubmissionsPage.stories.js index 9a4b7d720..de0fa7c02 100644 --- a/src/pages/submissions/SubmissionsPage.stories.js +++ b/src/pages/submissions/SubmissionsPage.stories.js @@ -1,5 +1,5 @@ import SubmissionsPage from './SubmissionsPage.vue'; -import {rest} from 'msw'; +import {http, HttpResponse} from 'msw'; import SubmissionsMock25 from './mocks/submissions25.js'; import PageInitConfigMock from './mocks/pageInitConfig'; @@ -16,10 +16,10 @@ export const init = { parameters: { msw: { handlers: [ - rest.get( + http.get( 'https://mock/index.php/publicknowledge/api/v1/_submissions', - (req, res, ctx) => { - return res(ctx.json(SubmissionsMock25)); + () => { + return HttpResponse.json(SubmissionsMock25); }, ), ], diff --git a/src/stores/dialogStore.js b/src/stores/dialogStore.js index bd0da6eda..3b8aaa605 100644 --- a/src/stores/dialogStore.js +++ b/src/stores/dialogStore.js @@ -1,35 +1,41 @@ import {defineStore} from 'pinia'; +import {ref} from 'vue'; import {t} from '@/utils/i18n'; -export const useDialogStore = defineStore('dialog', { - state: () => { - return { - dialogProps: {}, - dialogOpened: false, - }; - }, - actions: { - openDialog(dialogProps) { - this.dialogProps = dialogProps; - this.dialogOpened = true; - }, - openDialogNetworkError(fetchError) { - const msg = fetchError?.data?.errorMessage || t('common.unknownError'); +export const useDialogStore = defineStore('dialog', () => { + const dialogProps = ref({}); + const dialogOpened = ref(false); - this.openDialog({ - name: 'ajaxError', - title: t('common.error'), - message: msg, - actions: [ - { - label: t('common.ok'), - callback: (close) => close(), - }, - ], - }); - }, - closeDialog() { - this.dialogProps = {}; - this.dialogOpened = false; - }, - }, + function openDialog(_dialogProps) { + dialogProps.value = _dialogProps; + dialogOpened.value = true; + } + + function openDialogNetworkError(fetchError) { + const msg = fetchError?.data?.errorMessage || t('common.unknownError'); + + openDialog({ + name: 'ajaxError', + title: t('common.error'), + message: msg, + actions: [ + { + label: t('common.ok'), + callback: (close) => close(), + }, + ], + }); + } + + function closeDialog() { + dialogProps.value = {}; + dialogOpened.value = false; + } + + return { + dialogProps, + dialogOpened, + openDialogNetworkError, + openDialog, + closeDialog, + }; }); diff --git a/src/utils/i18n.js b/src/utils/i18n.js index 681fcb3a7..499f8589c 100644 --- a/src/utils/i18n.js +++ b/src/utils/i18n.js @@ -14,7 +14,10 @@ * @return {String} */ export function t(key, params) { - if (typeof pkp.localeKeys[key] === 'undefined') { + if ( + typeof pkp === 'undefined' || + typeof pkp.localeKeys[key] === 'undefined' + ) { if (process.env.NODE_ENV === 'development') { // eslint-disable-next-line console.error('Missing locale key: ', key);