diff --git a/package.json b/package.json index 7431c53b43..b7bcfccd20 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@babel/preset-env": "^7.27.2", "@babel/preset-flow": "^7.27.1", "@babel/preset-react": "^7.27.1", + "@fetch-mock/jest": "^0.2.15", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -146,7 +147,6 @@ "eslint-plugin-testing-library": "^7.2.1", "espree": "^10.3.0", "fake-indexeddb": "^6.0.1", - "fetch-mock-jest": "^1.5.1", "file-loader": "^6.2.0", "flow-bin": "^0.96.0", "glob": "^11.0.2", @@ -159,7 +159,6 @@ "local-web-server": "^5.4.0", "lockfile-lint": "^4.14.1", "mkdirp": "^3.0.1", - "node-fetch": "^2.6.11", "npm-run-all2": "^8.0.4", "open": "^10.1.2", "postcss": "^8.5.4", @@ -188,7 +187,7 @@ "jsx" ], "transformIgnorePatterns": [ - "/node_modules/(?!(query-string|decode-uri-component|split-on-first|filter-obj)/)" + "/node_modules/(?!(query-string|decode-uri-component|split-on-first|filter-obj|@fetch-mock/jest|fetch-mock)/)" ], "moduleNameMapper": { "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ftl)$": "/src/test/fixtures/mocks/file-mock.js", @@ -205,7 +204,7 @@ "escapeString": true, "printBasicPrototype": true }, - "testEnvironment": "jsdom", + "testEnvironment": "./src/test/custom-environment", "verbose": false }, "husky": { diff --git a/src/actions/receive-profile.js b/src/actions/receive-profile.js index 05253cac08..b7d26ce1d5 100644 --- a/src/actions/receive-profile.js +++ b/src/actions/receive-profile.js @@ -1030,8 +1030,14 @@ async function _extractZipFromResponse( reportError: (...data: Array) => void ): Promise { const buffer = await response.arrayBuffer(); + // Workaround for https://github.com/Stuk/jszip/issues/941 + // When running this code in tests, `buffer` doesn't inherits from _this_ + // realm's ArrayBuffer object, and this breaks JSZip which doesn't account for + // this case. We workaround the issue by wrapping the buffer in an Uint8Array + // that comes from this realm. + const typedBuffer = new Uint8Array(buffer); try { - const zip = await JSZip.loadAsync(buffer); + const zip = await JSZip.loadAsync(typedBuffer); // Catch the error if unable to load the zip. return zip; } catch (error) { diff --git a/src/profile-logic/mozilla-symbolication-api.js b/src/profile-logic/mozilla-symbolication-api.js index 8ae096bfc1..8c41301e34 100644 --- a/src/profile-logic/mozilla-symbolication-api.js +++ b/src/profile-logic/mozilla-symbolication-api.js @@ -95,7 +95,13 @@ type APIResultV5 = { // Make sure that the JSON blob we receive from the API conforms to our flow // type definition. function _ensureIsAPIResultV5(result: MixedObject): APIResultV5 { - if (!(result instanceof Object) || !('results' in result)) { + // It's possible (especially when running tests with Jest) that the parameter + // inherits from a `Object` global from another realm. By using toString + // this issue is solved wherever the parameter comes from. + const isObject = (subject) => + Object.prototype.toString.call(subject) === '[object Object]'; + + if (!isObject(result) || !('results' in result)) { throw new Error('Expected an object with property `results`'); } const results = result.results; @@ -104,7 +110,7 @@ function _ensureIsAPIResultV5(result: MixedObject): APIResultV5 { } for (const jobResult of results) { if ( - !(jobResult instanceof Object) || + !isObject(jobResult) || !('found_modules' in jobResult) || !('stacks' in jobResult) ) { @@ -113,7 +119,7 @@ function _ensureIsAPIResultV5(result: MixedObject): APIResultV5 { ); } const found_modules = jobResult.found_modules; - if (!(found_modules instanceof Object)) { + if (!isObject(found_modules)) { throw new Error('Expected `found_modules` to be an object'); } const stacks = jobResult.stacks; @@ -125,7 +131,7 @@ function _ensureIsAPIResultV5(result: MixedObject): APIResultV5 { throw new Error('Expected `stack` to be an array'); } for (const frameInfo of stack) { - if (!(frameInfo instanceof Object)) { + if (!isObject(frameInfo)) { throw new Error('Expected `frameInfo` to be an object'); } if ( @@ -165,7 +171,7 @@ function _ensureIsAPIResultV5(result: MixedObject): APIResultV5 { throw new Error('Expected `inlines` to be an array'); } for (const inlineFrame of inlines) { - if (!(inlineFrame instanceof Object)) { + if (!isObject(inlineFrame)) { throw new Error('Expected `inlineFrame` to be an object'); } } diff --git a/src/test/components/AppLocalizationProvider.test.js b/src/test/components/AppLocalizationProvider.test.js index ec43660969..05097b999a 100644 --- a/src/test/components/AppLocalizationProvider.test.js +++ b/src/test/components/AppLocalizationProvider.test.js @@ -37,11 +37,11 @@ describe('AppLocalizationProvider', () => { jest.spyOn(window.navigator, 'languages', 'get').mockReturnValue(languages); const translatedText = (language) => `This is ${language} Text`; - const fetchUrlRe = /^\/locales\/(?[^/]+)\/app.ftl$/; - window.fetch + const fetchUrlRe = /\/locales\/(?[^/]+)\/app.ftl$/; + window.fetchMock .catch(404) // catchall - .get(fetchUrlRe, (fetchUrl) => { - const matchUrlResult = fetchUrlRe.exec(fetchUrl); + .get(fetchUrlRe, ({ url }) => { + const matchUrlResult = fetchUrlRe.exec(url); if (matchUrlResult) { // $FlowExpectError Our Flow doesn't know about named groups. const { language } = matchUrlResult.groups; @@ -188,15 +188,15 @@ describe('AppLocalizationProvider', () => { expect(await screen.findByText(translatedText('de'))).toBeInTheDocument(); expect(document.documentElement).toHaveAttribute('lang', 'de'); - expect(window.fetch).toHaveBeenCalledWith('/locales/de/app.ftl', { + expect(window.fetch).toHaveFetched('/locales/de/app.ftl', { credentials: 'include', mode: 'no-cors', }); - expect(window.fetch).toHaveBeenCalledWith('/locales/en-US/app.ftl', { + expect(window.fetch).toHaveFetched('/locales/en-US/app.ftl', { credentials: 'include', mode: 'no-cors', }); - expect(window.fetch).toHaveBeenCalledTimes(2); + expect(window.fetch).toHaveFetchedTimes(2); }); it('falls back properly on en-US if the primary locale lacks a string', async () => { @@ -220,14 +220,14 @@ describe('AppLocalizationProvider', () => { await screen.findByText(translatedText('en-US')) ).toBeInTheDocument(); expect(document.documentElement).toHaveAttribute('lang', 'de'); - expect(window.fetch).toHaveBeenCalledWith('/locales/de/app.ftl', { + expect(window.fetch).toHaveFetched('/locales/de/app.ftl', { credentials: 'include', mode: 'no-cors', }); - expect(window.fetch).toHaveBeenCalledWith('/locales/en-US/app.ftl', { + expect(window.fetch).toHaveFetched('/locales/en-US/app.ftl', { credentials: 'include', mode: 'no-cors', }); - expect(window.fetch).toHaveBeenCalledTimes(2); + expect(window.fetch).toHaveFetchedTimes(2); }); }); diff --git a/src/test/components/BottomBox.test.js b/src/test/components/BottomBox.test.js index 43f2b335d1..40c0bf3d5d 100644 --- a/src/test/components/BottomBox.test.js +++ b/src/test/components/BottomBox.test.js @@ -50,7 +50,7 @@ describe('BottomBox', () => { const revision = '997f00815e6bc28806b75448c8829f0259d2cb28'; const filepath = 'widget/cocoa/nsAppShell.mm'; - window.fetch + window.fetchMock .post('http://127.0.0.1:8000/source/v1', 500) .get( `https://hg.mozilla.org/mozilla-central/raw-file/${revision}/${filepath}`, diff --git a/src/test/components/FooterLinks.test.js b/src/test/components/FooterLinks.test.js index d41bddc0e1..87f78ecedf 100755 --- a/src/test/components/FooterLinks.test.js +++ b/src/test/components/FooterLinks.test.js @@ -15,26 +15,22 @@ import { render, screen, fireEvent } from '@testing-library/react'; beforeEach(() => { // Implement the fetch operation for local language files, so that we can test // switching languages. - const fetchUrlRe = /^\/locales\/(?[^/]+)\/app.ftl$/; - window.fetch + const fetchUrlRe = /\/locales\/(?[^/]+)\/app.ftl$/; + window.fetchMock .catch(404) // catchall - .get( - fetchUrlRe, - (fetchUrl) => { - const matchUrlResult = fetchUrlRe.exec(fetchUrl); - if (matchUrlResult) { - // $FlowExpectError Our Flow doesn't know about named groups. - const { language } = matchUrlResult.groups; - const path = `locales/${language}/app.ftl`; - if (fs.existsSync(path)) { - return fs.readFileSync(path); - } + .get(fetchUrlRe, ({ url }) => { + const matchUrlResult = fetchUrlRe.exec(url); + if (matchUrlResult) { + // $FlowExpectError Our Flow doesn't know about named groups. + const { language } = matchUrlResult.groups; + const path = `locales/${language}/app.ftl`; + if (fs.existsSync(path)) { + return fs.readFileSync(path); } + } - return 404; - }, - { sendAsJson: false } - ); + return 404; + }); }); afterEach(function () { diff --git a/src/test/components/ListOfPublishedProfiles.test.js b/src/test/components/ListOfPublishedProfiles.test.js index 0d65be2bd9..1f41e586e0 100644 --- a/src/test/components/ListOfPublishedProfiles.test.js +++ b/src/test/components/ListOfPublishedProfiles.test.js @@ -285,11 +285,11 @@ describe('ListOfPublishedProfiles', () => { endpointUrl: string, jwtToken: string, }) { - window.fetch + window.fetchMock .catch(404) // Catchall - .mock(endpointUrl, (urlString, options) => { + .route(endpointUrl, ({ options }) => { const { method, headers } = options; - if (method !== 'DELETE') { + if (method !== 'delete') { return new Response(null, { status: 405, statusText: 'Method not allowed', @@ -297,8 +297,8 @@ describe('ListOfPublishedProfiles', () => { } if ( - headers['Content-Type'] !== 'application/json' || - headers.Accept !== + headers['content-type'] !== 'application/json' || + headers.accept !== 'application/vnd.firefox-profiler+json;version=1.0' ) { return new Response(null, { @@ -307,7 +307,7 @@ describe('ListOfPublishedProfiles', () => { }); } - if (headers.Authorization !== `Bearer ${jwtToken}`) { + if (headers.authorization !== `Bearer ${jwtToken}`) { return new Response(null, { status: 401, statusText: 'Forbidden', diff --git a/src/test/components/Root-history.test.js b/src/test/components/Root-history.test.js index f3885bbea7..3534184e0b 100644 --- a/src/test/components/Root-history.test.js +++ b/src/test/components/Root-history.test.js @@ -188,7 +188,7 @@ describe('Root with history', function () { }); function mockFetchProfileAtUrl(url: string, profile: Profile): void { - window.fetch + window.fetchMock .catch(404) // catchall .get(url, profile); } diff --git a/src/test/components/UrlManager.test.js b/src/test/components/UrlManager.test.js index 6e78087fe1..44267868d6 100644 --- a/src/test/components/UrlManager.test.js +++ b/src/test/components/UrlManager.test.js @@ -146,7 +146,7 @@ describe('UrlManager', function () { }); it(`sets the data source to public and doesn't change the URL when there's a fetch error`, async function () { - window.fetch.getAny({ throws: new Error('Simulated network error') }); + window.fetchMock.get('*', { throws: new Error('Simulated network error') }); const urlPath = '/public/FAKE_HASH/marker-chart'; const { getState, createUrlManager, waitUntilUrlSetupPhase } = setup(urlPath); @@ -163,7 +163,7 @@ describe('UrlManager', function () { }); it(`sets the data source to public and doesn't change the URL when there's a URL upgrading error`, async function () { - window.fetch.getAny(getSerializableProfile()); + window.fetchMock.get('*', getSerializableProfile()); const urlPath = '/public/FAKE_HASH/calltree'; const searchString = '?v=' + (CURRENT_URL_VERSION + 1); @@ -185,7 +185,7 @@ describe('UrlManager', function () { }); it(`fetches profile and sets the phase to done when everything works`, async function () { - window.fetch.getAny(getSerializableProfile()); + window.fetchMock.get('*', getSerializableProfile()); const urlPath = '/public/FAKE_HASH/'; const expectedResultingPath = urlPath + 'calltree/'; @@ -206,7 +206,7 @@ describe('UrlManager', function () { }); it('allows navigating back and forward when changing view options', async () => { - window.fetch.getAny(getSerializableProfile()); + window.fetchMock.get('*', getSerializableProfile()); const urlPath = '/public/FAKE_HASH/calltree/'; const searchString = 'v=' + CURRENT_URL_VERSION; diff --git a/src/test/custom-environment.js b/src/test/custom-environment.js new file mode 100644 index 0000000000..34f46957b2 --- /dev/null +++ b/src/test/custom-environment.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow +// +import { TestEnvironment } from 'jest-environment-jsdom'; +import { TextDecoder, TextEncoder } from 'util'; + +// This class registers various globals coming from node in test environments. +export default class CustomTestEnvironment extends TestEnvironment { + async setup() { + await super.setup(); + + // Register TextDecoder and TextEncoder with the global scope. + // These are now available globally in nodejs, but not when running with jsdom + // in jest apparently. + // Still let's double check that they're from the global scope as expected, so + // that this can be removed once it's implemented. + if ('TextDecoder' in this.global) { + throw new Error( + 'TextDecoder is already present in the global scope, please update custom-environment.js.' + ); + } + + this.global.TextDecoder = TextDecoder; + this.global.TextEncoder = TextEncoder; + + // Register Request and friends with the global scope. + // These are now available globally in nodejs, but not when running with jsdom + // in jest apparently. + // Still let's double check that they're from the global scope as expected, so + // that this can be removed once it's implemented. + if ('Request' in this.global) { + throw new Error( + 'Request is already present in the global scope, please update custom-environment.js.' + ); + } + + this.global.fetch = fetch; + this.global.Request = Request; + this.global.Response = Response; + this.global.ReadableStream = ReadableStream; + } +} diff --git a/src/test/integration/symbolicator-cli/symbolicator-cli.test.js b/src/test/integration/symbolicator-cli/symbolicator-cli.test.js index fd10fce63e..f0d77e969e 100644 --- a/src/test/integration/symbolicator-cli/symbolicator-cli.test.js +++ b/src/test/integration/symbolicator-cli/symbolicator-cli.test.js @@ -33,7 +33,7 @@ describe('symbolicator-cli tool', function () { 'src/test/integration/symbolicator-cli/symbol-server-response.json' ); - window.fetch.post( + window.fetchMock.post( 'http://symbol.server/symbolicate/v5', new Response(symbolsJson) ); diff --git a/src/test/setup.js b/src/test/setup.js index 8b22638ad8..aff40bd57d 100644 --- a/src/test/setup.js +++ b/src/test/setup.js @@ -7,9 +7,7 @@ import '@testing-library/jest-dom'; // This installs jest matchers as a side effect as well. -import fetchMock from 'fetch-mock-jest'; -import { Headers, Request, Response } from 'node-fetch'; -import { TextDecoder, TextEncoder } from 'util'; +import fetchMock from '@fetch-mock/jest'; import crypto from 'crypto'; jest.mock('../utils/worker-factory'); @@ -22,28 +20,8 @@ if (process.env.TZ !== 'UTC') { throw new Error('Jest must be run from `yarn test`'); } -// Register TextDecoder and TextEncoder with the global scope. -// These are now available globally in nodejs, but not when running with jsdom -// in jest apparently. -// Still let's double check that they're from the global scope as expected, so -// that this can be removed once it's implemented. -if ('TextDecoder' in global) { - throw new Error( - 'TextDecoder is already present in the global scope, please update setup.js.' - ); -} - -global.TextDecoder = TextDecoder; -global.TextEncoder = TextEncoder; - -beforeEach(function () { - // Install fetch and fetch-related objects globally. - // Using the sandbox ensures that parallel tests run properly. - global.fetch = fetchMock.sandbox(); - global.Headers = Headers; - global.Request = Request; - global.Response = Response; -}); +fetchMock.mockGlobal(); +global.fetchMock = fetchMock; afterEach(function () { // This `__shutdownWorkers` function only exists in the mocked test environment, diff --git a/src/test/store/__snapshots__/receive-profile.test.js.snap b/src/test/store/__snapshots__/receive-profile.test.js.snap index 6f0ff21fb7..e0a8aaf49a 100644 --- a/src/test/store/__snapshots__/receive-profile.test.js.snap +++ b/src/test/store/__snapshots__/receive-profile.test.js.snap @@ -14,40 +14,87 @@ Array [ Array [ "Fetch response:", Response { - "_fmResults": Object { - "arrayBuffer": Promise {}, - }, - "size": 0, - "timeout": 0, - Symbol(Body internals): Object { + Symbol(state): Object { + "aborted": false, "body": Object { - "data": Array [ - 105, - 110, - 118, - 97, - 108, - 105, - 100, - ], - "type": "Buffer", + "length": 7, + "source": Uint8Array [], + "stream": ReadableStream { + Symbol(kType): "ReadableStream", + Symbol(kState): Object { + "controller": ReadableByteStreamController { + Symbol(kType): "ReadableByteStreamController", + Symbol(kState): Object { + "autoAllocateChunkSize": undefined, + "byobRequest": null, + "cancelAlgorithm": undefined, + "closeRequested": false, + "highWaterMark": 0, + "pendingPullIntos": Array [], + "pullAgain": false, + "pullAlgorithm": undefined, + "pulling": false, + "queue": Array [], + "queueTotalSize": 0, + "started": true, + "stream": [Circular], + }, + }, + "disturbed": true, + "reader": ReadableStreamDefaultReader { + Symbol(kType): "ReadableStreamDefaultReader", + Symbol(kState): Object { + "close": Object { + "promise": Promise {}, + "reject": [Function], + "resolve": [Function], + }, + "readRequests": Array [], + "stream": [Circular], + }, + }, + "state": "closed", + "storedError": undefined, + "transfer": Object { + "port1": undefined, + "port2": undefined, + "promise": undefined, + "writable": undefined, + }, + }, + Symbol(nodejs.webstream.isClosedPromise): Object { + "promise": Promise {}, + "reject": [Function], + "resolve": [Function], + }, + Symbol(nodejs.webstream.controllerErrorFunction): [Function], + }, }, - "disturbed": true, - "error": null, - }, - Symbol(Response internals): Object { - "counter": undefined, - "headers": Headers { - Symbol(map): Object { - "content-type": Array [ - "application/json", - ], + "cacheState": "", + "headersList": HeadersList { + "cookies": null, + Symbol(headers map): Map { + "content-length" => Object { + "name": "content-length", + "value": "7", + }, + "content-type" => Object { + "name": "content-type", + "value": "application/json", + }, }, + Symbol(headers map sorted): null, }, + "rangeRequested": false, + "requestIncludesCredentials": false, "status": 200, "statusText": "OK", - "url": "https://example.com/profile.json", + "timingAllowPassed": false, + "timingInfo": null, + "type": "default", + "urlList": Array [], }, + Symbol(headers): Headers {}, }, ], ] @@ -67,40 +114,87 @@ Array [ Array [ "Fetch response:", Response { - "_fmResults": Object { - "arrayBuffer": Promise {}, - }, - "size": 0, - "timeout": 0, - Symbol(Body internals): Object { + Symbol(state): Object { + "aborted": false, "body": Object { - "data": Array [ - 105, - 110, - 118, - 97, - 108, - 105, - 100, - ], - "type": "Buffer", + "length": 7, + "source": Uint8Array [], + "stream": ReadableStream { + Symbol(kType): "ReadableStream", + Symbol(kState): Object { + "controller": ReadableByteStreamController { + Symbol(kType): "ReadableByteStreamController", + Symbol(kState): Object { + "autoAllocateChunkSize": undefined, + "byobRequest": null, + "cancelAlgorithm": undefined, + "closeRequested": false, + "highWaterMark": 0, + "pendingPullIntos": Array [], + "pullAgain": false, + "pullAlgorithm": undefined, + "pulling": false, + "queue": Array [], + "queueTotalSize": 0, + "started": true, + "stream": [Circular], + }, + }, + "disturbed": true, + "reader": ReadableStreamDefaultReader { + Symbol(kType): "ReadableStreamDefaultReader", + Symbol(kState): Object { + "close": Object { + "promise": Promise {}, + "reject": [Function], + "resolve": [Function], + }, + "readRequests": Array [], + "stream": [Circular], + }, + }, + "state": "closed", + "storedError": undefined, + "transfer": Object { + "port1": undefined, + "port2": undefined, + "promise": undefined, + "writable": undefined, + }, + }, + Symbol(nodejs.webstream.isClosedPromise): Object { + "promise": Promise {}, + "reject": [Function], + "resolve": [Function], + }, + Symbol(nodejs.webstream.controllerErrorFunction): [Function], + }, }, - "disturbed": true, - "error": null, - }, - Symbol(Response internals): Object { - "counter": undefined, - "headers": Headers { - Symbol(map): Object { - "content-type": Array [ - "undefined", - ], + "cacheState": "", + "headersList": HeadersList { + "cookies": null, + Symbol(headers map): Map { + "content-length" => Object { + "name": "content-length", + "value": "7", + }, + "content-type" => Object { + "name": "content-type", + "value": "undefined", + }, }, + Symbol(headers map sorted): null, }, + "rangeRequested": false, + "requestIncludesCredentials": false, "status": 200, "statusText": "OK", - "url": "https://example.com/profile.json", + "timingAllowPassed": false, + "timingInfo": null, + "type": "default", + "urlList": Array [], }, + Symbol(headers): Headers {}, }, ], ] @@ -120,37 +214,87 @@ Array [ Array [ "Fetch response:", Response { - "_fmResults": Object { - "arrayBuffer": Promise {}, - }, - "size": 0, - "timeout": 0, - Symbol(Body internals): Object { + Symbol(state): Object { + "aborted": false, "body": Object { - "data": Array [ - 0, - 1, - 2, - 3, - ], - "type": "Buffer", + "length": 4, + "source": Uint8Array [], + "stream": ReadableStream { + Symbol(kType): "ReadableStream", + Symbol(kState): Object { + "controller": ReadableByteStreamController { + Symbol(kType): "ReadableByteStreamController", + Symbol(kState): Object { + "autoAllocateChunkSize": undefined, + "byobRequest": null, + "cancelAlgorithm": undefined, + "closeRequested": false, + "highWaterMark": 0, + "pendingPullIntos": Array [], + "pullAgain": false, + "pullAlgorithm": undefined, + "pulling": false, + "queue": Array [], + "queueTotalSize": 0, + "started": true, + "stream": [Circular], + }, + }, + "disturbed": true, + "reader": ReadableStreamDefaultReader { + Symbol(kType): "ReadableStreamDefaultReader", + Symbol(kState): Object { + "close": Object { + "promise": Promise {}, + "reject": [Function], + "resolve": [Function], + }, + "readRequests": Array [], + "stream": [Circular], + }, + }, + "state": "closed", + "storedError": undefined, + "transfer": Object { + "port1": undefined, + "port2": undefined, + "promise": undefined, + "writable": undefined, + }, + }, + Symbol(nodejs.webstream.isClosedPromise): Object { + "promise": Promise {}, + "reject": [Function], + "resolve": [Function], + }, + Symbol(nodejs.webstream.controllerErrorFunction): [Function], + }, }, - "disturbed": true, - "error": null, - }, - Symbol(Response internals): Object { - "counter": undefined, - "headers": Headers { - Symbol(map): Object { - "content-type": Array [ - "application/zip", - ], + "cacheState": "", + "headersList": HeadersList { + "cookies": null, + Symbol(headers map): Map { + "content-length" => Object { + "name": "content-length", + "value": "4", + }, + "content-type" => Object { + "name": "content-type", + "value": "application/zip", + }, }, + Symbol(headers map sorted): null, }, + "rangeRequested": false, + "requestIncludesCredentials": false, "status": 200, "statusText": "OK", - "url": "https://example.com/profile.file", + "timingAllowPassed": false, + "timingInfo": null, + "type": "default", + "urlList": Array [], }, + Symbol(headers): Headers {}, }, ], ] diff --git a/src/test/store/receive-profile.test.js b/src/test/store/receive-profile.test.js index 8fa4105f8c..36fa4cccf1 100644 --- a/src/test/store/receive-profile.test.js +++ b/src/test/store/receive-profile.test.js @@ -681,7 +681,9 @@ describe('actions/receive-profile', function () { } }; - window.fetch.any({ throws: new Error('No symbolication API in place') }); + window.fetchMock.any({ + throws: new Error('No symbolication API in place'), + }); simulateSymbolStoreHasNoCache(); @@ -799,7 +801,7 @@ describe('actions/receive-profile', function () { expect.any(String) ); - expect(window.fetch).toHaveBeenCalledWith( + expect(window.fetch).toHaveFetched( 'https://symbolication.services.mozilla.com/symbolicate/v5', expect.objectContaining({ body: expect.stringMatching(/memoryMap.*firefox/), @@ -814,7 +816,7 @@ describe('actions/receive-profile', function () { await createBrowserConnection('Firefox/123.0'); await dispatch(retrieveProfileFromBrowser(browserConnectionStatus)); - expect(window.fetch).toHaveBeenCalledWith( + expect(window.fetch).toHaveFetched( 'https://symbolication.services.mozilla.com/symbolicate/v5', expect.objectContaining({ body: expect.stringMatching(/memoryMap.*firefox/), @@ -825,11 +827,7 @@ describe('actions/receive-profile', function () { it('gets the favicons for the received profile using webchannel', async () => { // For some reason fetch-mock-jest removes the `data:` protocol. const mockDataUrl = 'image/png,test'; - window.fetch.get('image/png,test', { - arrayBuffer: () => { - return new Uint8Array([1, 2, 3, 4, 5, 6]).buffer; - }, - }); + window.fetchMock.spy('begin:data:'); // Create a simple urls getter for the pages. const faviconsGetter = async (): Promise> => { @@ -858,7 +856,7 @@ describe('actions/receive-profile', function () { describe('retrieveProfileFromStore', function () { beforeEach(function () { - window.fetch.catch(403); + window.fetchMock.catch(403); // Call the argument of setTimeout asynchronously right away // (instead of waiting for the timeout). @@ -871,7 +869,7 @@ describe('actions/receive-profile', function () { const hash = 'c5e53f9ab6aecef926d4be68c84f2de550e2ac2f'; const expectedUrl = `https://storage.googleapis.com/profile-store/${hash}`; - window.fetch.get(expectedUrl, _getSimpleProfile()); + window.fetchMock.get(expectedUrl, _getSimpleProfile()); const store = blankStore(); await store.dispatch(retrieveProfileFromStore(hash)); @@ -890,7 +888,7 @@ describe('actions/receive-profile', function () { getProfileFromTextSamples('0xA[lib:libxul]'); unsymbolicatedProfile.meta.symbolicated = false; - window.fetch + window.fetchMock .get( 'https://storage.googleapis.com/profile-store/FAKEHASH', unsymbolicatedProfile @@ -905,7 +903,7 @@ describe('actions/receive-profile', function () { const store = blankStore(); await store.dispatch(retrieveProfileFromStore('FAKEHASH')); - expect(window.fetch).toHaveBeenLastCalledWith( + expect(window.fetch).toHaveLastFetched( 'https://symbolication.services.mozilla.com/symbolicate/v5', expect.objectContaining({ body: expect.stringMatching(/memoryMap.*libxul/), @@ -926,11 +924,9 @@ describe('actions/receive-profile', function () { it('requests several times in case of 403', async function () { const hash = 'c5e53f9ab6aecef926d4be68c84f2de550e2ac2f'; const expectedUrl = `https://storage.googleapis.com/profile-store/${hash}`; - window.fetch + window.fetchMock .getOnce(expectedUrl, 403) - .get(expectedUrl, _getSimpleProfile(), { - overwriteRoutes: false, - }); + .get(expectedUrl, _getSimpleProfile()); const store = blankStore(); const views = ( @@ -964,7 +960,7 @@ describe('actions/receive-profile', function () { it('fails in case the profile cannot be found after several tries', async function () { const hash = 'c5e53f9ab6aecef926d4be68c84f2de550e2ac2f'; const expectedUrl = `https://storage.googleapis.com/profile-store/${hash}`; - window.fetch.get(expectedUrl, 403); + window.fetchMock.get(expectedUrl, 403); const store = blankStore(); const views = ( await observeStoreStateChanges(store, () => @@ -991,7 +987,7 @@ describe('actions/receive-profile', function () { it('fails in case the fetch returns a server error', async function () { const hash = 'c5e53f9ab6aecef926d4be68c84f2de550e2ac2f'; const expectedUrl = `https://storage.googleapis.com/profile-store/${hash}`; - window.fetch.get(expectedUrl, 500); + window.fetchMock.get(expectedUrl, 500); const store = blankStore(); await store.dispatch(retrieveProfileFromStore(hash)); @@ -1004,7 +1000,7 @@ describe('actions/receive-profile', function () { describe('retrieveProfileOrZipFromUrl', function () { beforeEach(function () { - window.fetch.catch(403); + window.fetchMock.catch(403); // Call the argument of setTimeout asynchronously right away // (instead of waiting for the timeout). @@ -1015,7 +1011,7 @@ describe('actions/receive-profile', function () { it('can retrieve a profile from the web and save it to state', async function () { const expectedUrl = 'https://profiles.club/shared.json'; - window.fetch.get(expectedUrl, _getSimpleProfile()); + window.fetchMock.get(expectedUrl, _getSimpleProfile()); const store = blankStore(); await store.dispatch(retrieveProfileOrZipFromUrl(expectedUrl)); @@ -1031,10 +1027,9 @@ describe('actions/receive-profile', function () { it('can retrieve a gzipped profile from the web and save it to state', async function () { const expectedUrl = 'https://profiles.club/shared.json'; - window.fetch.get( + window.fetchMock.get( expectedUrl, - compress(serializeProfile(_getSimpleProfile())), - { sendAsJson: false } + compress(serializeProfile(_getSimpleProfile())) ); const store = blankStore(); await store.dispatch(retrieveProfileOrZipFromUrl(expectedUrl)); @@ -1051,11 +1046,9 @@ describe('actions/receive-profile', function () { it('requests several times in case of 403', async function () { const expectedUrl = 'https://profiles.club/shared.json'; // The first call will still be a 403 -- remember, it's the default return value. - window.fetch + window.fetchMock .getOnce(expectedUrl, 403) - .get(expectedUrl, _getSimpleProfile(), { - overwriteRoutes: false, - }); + .get(expectedUrl, _getSimpleProfile()); const store = blankStore(); const views = ( @@ -1088,7 +1081,7 @@ describe('actions/receive-profile', function () { it('fails in case the profile cannot be found after several tries', async function () { const expectedUrl = 'https://profiles.club/shared.json'; - window.fetch.get(expectedUrl, 403); + window.fetchMock.get(expectedUrl, 403); const store = blankStore(); const views = ( @@ -1115,7 +1108,7 @@ describe('actions/receive-profile', function () { it('fails in case the fetch returns a server error', async function () { const expectedUrl = 'https://profiles.club/shared.json'; - window.fetch.any(500); + window.fetchMock.any(500); const store = blankStore(); await store.dispatch(retrieveProfileOrZipFromUrl(expectedUrl)); @@ -1162,16 +1155,12 @@ describe('actions/receive-profile', function () { break; } - window.fetch.catch(403).get( - url, - { - body: arrayBuffer, - headers: { - 'content-type': contentType, - }, + window.fetchMock.catch(403).get(url, { + body: arrayBuffer, + headers: { + 'content-type': contentType, }, - { sendAsJson: false } - ); + }); const reportError = jest.fn(); const args = { @@ -1394,7 +1383,9 @@ describe('actions/receive-profile', function () { it('symbolicates unsymbolicated profiles', async function () { simulateSymbolStoreHasNoCache(); - window.fetch.any({ throws: new Error('No symbolication API in place') }); + window.fetchMock.any({ + throws: new Error('No symbolication API in place'), + }); // Silence console logs coming from the previous rejections jest.spyOn(console, 'warn').mockImplementation(() => {}); @@ -1406,7 +1397,7 @@ describe('actions/receive-profile', function () { payload: profile, }); - expect(window.fetch).toHaveBeenCalledWith( + expect(window.fetch).toHaveFetched( 'https://symbolication.services.mozilla.com/symbolicate/v5', expect.objectContaining({ body: expect.stringMatching(/memoryMap.*firefox/), @@ -1702,9 +1693,7 @@ describe('actions/receive-profile', function () { ]) ); } - window.fetch.getOnce('*', profile1).getOnce('*', profile2, { - overwriteRoutes: false, - }); + window.fetchMock.getOnce('*', profile1).getOnce('*', profile2); const { dispatch, getState } = blankStore(); await dispatch(retrieveProfilesToCompare([url1, url2])); @@ -1731,7 +1720,7 @@ describe('actions/receive-profile', function () { } beforeEach(function () { - window.fetch.catch({ + window.fetchMock.catch({ throws: new Error('No more answers have been configured.'), }); }); @@ -1980,9 +1969,7 @@ describe('actions/receive-profile', function () { // Add mock fetch response for the required number of times. // Usually it's 1 but it can be also 2 for `compare` dataSource. for (let i = 0; i < requiredProfile; i++) { - window.fetch.getOnce('*', profile, { - overwriteRoutes: false, - }); + window.fetchMock.getOnce('*', profile); } const geckoProfiler = { @@ -2039,7 +2026,7 @@ describe('actions/receive-profile', function () { } beforeEach(function () { - window.fetch.catch({ + window.fetchMock.catch({ throws: new Error('No more answers have been configured.'), }); }); diff --git a/src/test/unit/profile-store.test.js b/src/test/unit/profile-store.test.js index 5433e69e12..a17bed9ed7 100644 --- a/src/test/unit/profile-store.test.js +++ b/src/test/unit/profile-store.test.js @@ -173,12 +173,12 @@ describe('profile deletion', () => { endpointUrl: string, jwtToken: string, }) { - window.fetch + window.fetchMock .catch(404) // catchall - .mock(endpointUrl, async (urlString, options) => { + .route(endpointUrl, async ({ options }) => { const { method, headers } = options; - if (method !== 'DELETE') { + if (method !== 'delete') { return new Response(null, { status: 405, statusText: 'Method not allowed', @@ -186,8 +186,8 @@ describe('profile deletion', () => { } if ( - headers['Content-Type'] !== 'application/json' || - headers.Accept !== 'application/vnd.firefox-profiler+json;version=1.0' + headers['content-type'] !== 'application/json' || + headers.accept !== 'application/vnd.firefox-profiler+json;version=1.0' ) { return new Response(null, { status: 406, @@ -195,7 +195,7 @@ describe('profile deletion', () => { }); } - if (headers.Authorization !== `Bearer ${jwtToken}`) { + if (headers.authorization !== `Bearer ${jwtToken}`) { return new Response(null, { status: 401, statusText: 'Forbidden', @@ -219,6 +219,6 @@ describe('profile deletion', () => { profileToken: PROFILE_TOKEN, jwtToken: JWT_TOKEN, }); - expect(window.fetch).toHaveBeenCalledWith(endpointUrl, expect.anything()); + expect(window.fetch).toHaveFetched(endpointUrl, expect.anything()); }); }); diff --git a/src/test/unit/shorten-url.test.js b/src/test/unit/shorten-url.test.js index 2932988109..d4a1c09713 100644 --- a/src/test/unit/shorten-url.test.js +++ b/src/test/unit/shorten-url.test.js @@ -15,12 +15,12 @@ function mockFetchForBitly({ endpointUrl: string, responseFromRequestPayload: (any) => any, |}) { - window.fetch + window.fetchMock .catch(404) // catch all - .mock(endpointUrl, async (urlString, options) => { + .route(endpointUrl, async ({ options }) => { const { method, headers, body } = options; - if (method !== 'POST') { + if (method !== 'post') { return new Response(null, { status: 405, statusText: 'Method not allowed', @@ -28,8 +28,8 @@ function mockFetchForBitly({ } if ( - headers['Content-Type'] !== 'application/json' || - headers.Accept !== 'application/vnd.firefox-profiler+json;version=1.0' + headers['content-type'] !== 'application/json' || + headers.accept !== 'application/vnd.firefox-profiler+json;version=1.0' ) { return new Response(null, { status: 406, @@ -64,12 +64,7 @@ describe('shortenUrl', () => { const shortUrl = await shortenUrl(longUrl); expect(shortUrl).toBe(expectedShortUrl); - expect(window.fetch).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - body: expect.stringContaining(`"longUrl":"${longUrl}"`), - }) - ); + expect(window.fetch).toHaveFetched({ body: { longUrl } }); }); it('changes the requested url if is not the main URL', async () => { @@ -86,12 +81,7 @@ describe('shortenUrl', () => { const shortUrl = await shortenUrl(longUrl); expect(shortUrl).toBe(expectedShortUrl); - expect(window.fetch).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - body: expect.stringContaining(`"longUrl":"${expectedLongUrl}"`), - }) - ); + expect(window.fetch).toHaveFetched({ body: { longUrl: expectedLongUrl } }); }); }); @@ -115,23 +105,18 @@ describe('expandUrl', () => { const longUrl = await expandUrl(shortUrl); expect(longUrl).toBe(returnedLongUrl); - expect(window.fetch).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - body: expect.stringContaining(`"shortUrl":"${shortUrl}"`), - }) - ); + expect(window.fetch).toHaveFetched({ body: { shortUrl } }); }); it('forwards errors', async () => { - window.fetch.any(503); // server error + window.fetchMock.any(503); // server error const shortUrl = 'https://share.firefox.dev/BITLYHASH'; await expect(expandUrl(shortUrl)).rejects.toThrow(); }); it('returns an error when there is no match for this hash', async () => { - window.fetch.any(404); // not found + window.fetchMock.any(404); // not found const shortUrl = 'https://share.firefox.dev/BITLYHASH'; await expect(expandUrl(shortUrl)).rejects.toThrow(); diff --git a/src/types/globals/Window.js b/src/types/globals/Window.js index a9e61b8081..5498e1077f 100644 --- a/src/types/globals/Window.js +++ b/src/types/globals/Window.js @@ -7,6 +7,7 @@ import type { IDBFactory, IDBKeyRange } from '../indexeddb'; import type { SymbolTableAsTuple } from '../../profile-logic/symbol-store-db'; import type { GoogleAnalytics } from '../../utils/analytics'; import type { MixedObject } from '../utils'; +import type { FetchMockJest } from '@fetch-mock/jest'; // Because this type isn't an existing Global type, but still it's useful to // have it available, we define it with a $ as prfix. @@ -70,6 +71,7 @@ declare class Window { }, }; fetch: typeof fetch; + fetchMock: FetchMockJest /* only used in tests */; requestIdleCallback: typeof requestIdleCallback; requestAnimationFrame: typeof requestAnimationFrame; devicePixelRatio: number; diff --git a/src/types/libdef/npm-custom/@fetch-mock/jest_vx.x.x.js b/src/types/libdef/npm-custom/@fetch-mock/jest_vx.x.x.js new file mode 100644 index 0000000000..1582b9e9ca --- /dev/null +++ b/src/types/libdef/npm-custom/@fetch-mock/jest_vx.x.x.js @@ -0,0 +1,19 @@ +// flow-typed signature: cd33d60e2cb749449f5c2d918d136851 +// flow-typed version: <>/fetch-mock-jest_v1.5.1/flow_v0.96.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'fetch-mock-jest' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +// @flow +declare module '@fetch-mock/jest' { + declare module.exports: any; +} diff --git a/src/types/libdef/npm-custom/fetch-mock-jest_vx.x.x.js b/src/types/libdef/npm-custom/fetch-mock-jest_vx.x.x.js deleted file mode 100644 index cb8c90593d..0000000000 --- a/src/types/libdef/npm-custom/fetch-mock-jest_vx.x.x.js +++ /dev/null @@ -1,117 +0,0 @@ -// flow-typed signature: cd33d60e2cb749449f5c2d918d136851 -// flow-typed version: <>/fetch-mock-jest_v1.5.1/flow_v0.96.0 - -/** - * This is an autogenerated libdef stub for: - * - * 'fetch-mock-jest' - * - * Fill this stub out by replacing all the `any` types. - * - * Once filled out, we encourage you to share your work with the - * community by sending a pull request to: - * https://github.com/flowtype/flow-typed - */ - -// @flow -declare module 'fetch-mock-jest' { - declare module.exports: any; -} - -/** - * We include stubs for each file inside this npm package in case you need to - * require those files directly. Feel free to delete any files that aren't - * needed. - */ -declare module 'fetch-mock-jest/__tests__/jest-built-ins.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions-delete.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions-get.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions-head.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions-patch.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions-post.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions-put.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/jest-extensions.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/__tests__/regressions.spec' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/browser' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/jest-extensions' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/jestify' { - declare module.exports: any; -} - -declare module 'fetch-mock-jest/server' { - declare module.exports: any; -} - -// Filename aliases -declare module 'fetch-mock-jest/__tests__/jest-built-ins.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-built-ins.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions-delete.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions-delete.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions-get.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions-get.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions-head.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions-head.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions-patch.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions-patch.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions-post.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions-post.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions-put.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions-put.spec'>; -} -declare module 'fetch-mock-jest/__tests__/jest-extensions.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/jest-extensions.spec'>; -} -declare module 'fetch-mock-jest/__tests__/regressions.spec.js' { - declare module.exports: $Exports<'fetch-mock-jest/__tests__/regressions.spec'>; -} -declare module 'fetch-mock-jest/browser.js' { - declare module.exports: $Exports<'fetch-mock-jest/browser'>; -} -declare module 'fetch-mock-jest/jest-extensions.js' { - declare module.exports: $Exports<'fetch-mock-jest/jest-extensions'>; -} -declare module 'fetch-mock-jest/jestify.js' { - declare module.exports: $Exports<'fetch-mock-jest/jestify'>; -} -declare module 'fetch-mock-jest/server.js' { - declare module.exports: $Exports<'fetch-mock-jest/server'>; -} diff --git a/src/types/libdef/npm-custom/jest-environment-jsdom_vx.x.x.js b/src/types/libdef/npm-custom/jest-environment-jsdom_vx.x.x.js new file mode 100644 index 0000000000..036814c902 --- /dev/null +++ b/src/types/libdef/npm-custom/jest-environment-jsdom_vx.x.x.js @@ -0,0 +1,5 @@ +// @flow + +declare module 'jest-environment-jsdom' { + declare module.exports: any; +} diff --git a/src/types/libdef/npm-custom/jest_v26.x.x.js b/src/types/libdef/npm-custom/jest_v26.x.x.js index 3ead7d668e..4e610b6ee3 100644 --- a/src/types/libdef/npm-custom/jest_v26.x.x.js +++ b/src/types/libdef/npm-custom/jest_v26.x.x.js @@ -550,6 +550,140 @@ type JestExtendedMatchersType = { ... }; +// @fetch-mock/jest +// https://www.wheresrhys.co.uk/fetch-mock/docs/wrappers/jest +type $$FetchMockJestMatchersType$$Filter = any; +type $$FetchMockJestMatchersType$$Options = any; +type FetchMockJestMatchersType = { + toHaveFetched( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastFetched( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthFetched( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveFetchedTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveGot( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastGot( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthGot( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveGotTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHavePosted( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastPosted( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthPosted( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHavePostedTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHavePut( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastPut( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthPut( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHavePutTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveDeleted( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastDeleted( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthDeleted( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveDeletedTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveFetchedHead( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastFetchedHead( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthFetchedHead( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveFetchedHeadTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHavePatched( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveLastPatched( + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHaveNthPatched( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toHavePatchedTimes( + n: number, + filter: $$FetchMockJestMatchersType$$Filter, + options: $$FetchMockJestMatchersType$$Options + ): void, + toBeDone(filter: $$FetchMockJestMatchersType$$Filter): void, +}; + // Diffing snapshot utility for Jest (snapshot-diff) // https://github.com/jest-community/snapshot-diff type SnapshotDiffType = { @@ -579,6 +713,7 @@ interface JestExpectType { JestJQueryMatchersType & JestStyledComponentsMatchersType & JestExtendedMatchersType & + FetchMockJestMatchersType & SnapshotDiffType; /** * If you have a mock function, you can use .lastCalledWith to test what @@ -1169,6 +1304,7 @@ declare var expect: { JestJQueryMatchersType & JestStyledComponentsMatchersType & JestExtendedMatchersType & + FetchMockJestMatchersType & SnapshotDiffType, /** Add additional Jasmine matchers to Jest's roster */ extend(matchers: { [name: string]: JestMatcher, ... }): void, diff --git a/src/types/libdef/npm-custom/jszip_v3.x.x.js b/src/types/libdef/npm-custom/jszip_v3.x.x.js index 7c94ede4a0..add660b816 100644 --- a/src/types/libdef/npm-custom/jszip_v3.x.x.js +++ b/src/types/libdef/npm-custom/jszip_v3.x.x.js @@ -34,7 +34,7 @@ declare module 'jszip' { files: {| [fileName: string]: JSZipFile |}; file: (fileName: string, contents: string) => void; generateAsync({ type: 'uint8array' }): Promise; - static loadAsync: (data: ArrayBuffer) => Promise; + static loadAsync: (data: ArrayBuffer | $TypedArray) => Promise; } declare module.exports: typeof JSZip; diff --git a/src/types/libdef/npm/node-fetch_v2.x.x.js b/src/types/libdef/npm/node-fetch_v2.x.x.js deleted file mode 100644 index cfbedfa593..0000000000 --- a/src/types/libdef/npm/node-fetch_v2.x.x.js +++ /dev/null @@ -1,111 +0,0 @@ -declare module 'node-fetch' { - import type http from 'http'; - import type https from 'https'; - - declare export class Request mixins Body { - constructor(input: string | { href: string } | Request, init?: RequestInit): this; - context: RequestContext; - headers: Headers; - method: string; - redirect: RequestRedirect; - referrer: string; - url: string; - - // node-fetch extensions - agent: http.Agent | https.Agent; - compress: boolean; - counter: number; - follow: number; - hostname: string; - port: number; - protocol: string; - size: number; - timeout: number; - } - - declare type HeaderObject = { - [index: string]: string, - } - - declare interface RequestInit { - body?: BodyInit, - headers?: HeaderObject, - method?: string, - redirect?: RequestRedirect, - - // node-fetch extensions - agent?: http.Agent | https.Agent, - compress?: boolean, - follow?: number, - size?: number, - timeout?: number, - } - - declare type RequestContext = - 'audio' | 'beacon' | 'cspreport' | 'download' | 'embed' | - 'eventsource' | 'favicon' | 'fetch' | 'font' | 'form' | 'frame' | - 'hyperlink' | 'iframe' | 'image' | 'imageset' | 'import' | - 'internal' | 'location' | 'manifest' | 'object' | 'ping' | 'plugin' | - 'prefetch' | 'script' | 'serviceworker' | 'sharedworker' | - 'subresource' | 'style' | 'track' | 'video' | 'worker' | - 'xmlhttprequest' | 'xslt'; - declare type RequestRedirect = 'error' | 'follow' | 'manual'; - - declare export class Headers { - append(name: string, value: string): void; - delete(name: string): void; - forEach(callback: (value: string, name: string) => void): void; - get(name: string): string; - getAll(name: string): Array; - has(name: string): boolean; - raw(): { [k: string]: string[] }; - set(name: string, value: string): void; - entries(): Iterator<[string, string]>; - keys(): Iterator; - values(): Iterator; - @@iterator(): Iterator<[string, string]>; - } - - declare export class Body { - buffer(): Promise; - json(): Promise; - json(): Promise; - text(): Promise; - body: stream$Readable; - bodyUsed: boolean; - } - - declare export class Response mixins Body { - constructor(body?: BodyInit, init?: ResponseInit): this; - clone(): Response; - error(): Response; - redirect(url: string, status: number): Response; - headers: Headers; - ok: boolean; - status: number; - statusText: string; - size: number; - timeout: number; - type: ResponseType; - url: string; - } - - declare type ResponseType = - | 'basic' - | 'cors' - | 'default' - | 'error' - | 'opaque' - | 'opaqueredirect'; - - declare interface ResponseInit { - headers?: HeaderInit, - status: number, - statusText?: string, - } - - declare type HeaderInit = Headers | Array; - declare type BodyInit = string; - - declare export default function fetch(url: string | Request, init?: RequestInit): Promise -} diff --git a/yarn.lock b/yarn.lock index 9d129b1009..7141f3b775 100644 --- a/yarn.lock +++ b/yarn.lock @@ -67,7 +67,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.2.tgz#4183f9e642fd84e74e3eea7ffa93a412e3b102c9" integrity sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ== -"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.24.4", "@babel/core@^7.27.4": +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.24.4", "@babel/core@^7.27.4": version "7.27.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.4.tgz#cc1fc55d0ce140a1828d1dd2a2eba285adbfb3ce" integrity sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g== @@ -980,7 +980,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.27.1" "@babel/plugin-transform-react-pure-annotations" "^7.27.1" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1161,6 +1161,13 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@fetch-mock/jest@^0.2.15": + version "0.2.15" + resolved "https://registry.yarnpkg.com/@fetch-mock/jest/-/jest-0.2.15.tgz#8af9b7ada35f69d3eb784e5e9492ded4f6ac6682" + integrity sha512-cudDyqZr/mzA/AsXzowx3Il5C7//lOaBn3CW/+gzitGRk621ZPSdlZYbaq3kwxji5vwUaaFQE0saKBLHi2K6fQ== + dependencies: + fetch-mock "^12.5.2" + "@firefox-devtools/react-contextmenu@^5.2.2": version "5.2.2" resolved "https://registry.yarnpkg.com/@firefox-devtools/react-contextmenu/-/react-contextmenu-5.2.2.tgz#0ad241cba81b292ff1e04578044b63f39da6007f" @@ -1989,6 +1996,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/glob-to-regexp@^0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@types/glob-to-regexp/-/glob-to-regexp-0.4.4.tgz#409e71290253203185b1ea8a3d6ea406a4bdc902" + integrity sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg== + "@types/graceful-fs@^4.1.3": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15" @@ -3669,7 +3681,7 @@ core-js-compat@^3.40.0: dependencies: browserslist "^4.24.4" -core-js@^3.0.0, core-js@^3.42.0: +core-js@^3.42.0: version "3.42.0" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.42.0.tgz#edbe91f78ac8cfb6df8d997e74d368a68082fe37" integrity sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g== @@ -5034,28 +5046,15 @@ fdir@^6.4.3: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== -fetch-mock-jest@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz#0e13df990d286d9239e284f12b279ed509bf53cd" - integrity sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ== - dependencies: - fetch-mock "^9.11.0" - -fetch-mock@^9.11.0: - version "9.11.0" - resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f" - integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q== +fetch-mock@^12.5.2: + version "12.5.2" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-12.5.2.tgz#dbd7e2bb6a61fdbc7af45d399fe9aa1da97e3582" + integrity sha512-b5KGDFmdmado2MPQjZl6ix3dAG3iwCitb0XQwN72y2s9VnWZ3ObaGNy+bkpm1390foiLDybdJ7yjRGKD36kATw== dependencies: - "@babel/core" "^7.0.0" - "@babel/runtime" "^7.0.0" - core-js "^3.0.0" - debug "^4.1.1" - glob-to-regexp "^0.4.0" - is-subset "^0.1.1" - lodash.isequal "^4.5.0" - path-to-regexp "^2.2.1" - querystring "^0.2.0" - whatwg-url "^6.5.0" + "@types/glob-to-regexp" "^0.4.4" + dequal "^2.0.3" + glob-to-regexp "^0.4.1" + regexparam "^3.0.0" file-entry-cache@^10.1.0: version "10.1.1" @@ -5398,7 +5397,7 @@ glob-parent@^6.0.1, glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1: +glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== @@ -6432,11 +6431,6 @@ is-string@^1.0.7, is-string@^1.1.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" -is-subset@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6" - integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY= - is-symbol@^1.0.4, is-symbol@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" @@ -7523,11 +7517,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= - lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -8698,7 +8687,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-fetch@^2.6.1, node-fetch@^2.6.11: +node-fetch@^2.6.1: version "2.6.11" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== @@ -9243,11 +9232,6 @@ path-to-regexp@0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== -path-to-regexp@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704" - integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w== - path-to-regexp@^6.1.0, path-to-regexp@^6.2.1, path-to-regexp@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" @@ -9798,11 +9782,6 @@ query-string@^9.2.0: filter-obj "^5.1.0" split-on-first "^3.0.0" -querystring@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" - integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== - querystringify@^2.1.1: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" @@ -10136,6 +10115,11 @@ regexp.prototype.flags@^1.5.3: gopd "^1.2.0" set-function-name "^2.0.2" +regexparam@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-3.0.0.tgz#1673e09d41cb7fd41eaafd4040a6aa90daa0a21a" + integrity sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q== + regexpu-core@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" @@ -11005,7 +10989,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11127,7 +11120,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11141,6 +11134,13 @@ strip-ansi@^0.3.0: dependencies: ansi-regex "^0.2.1" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -12355,15 +12355,6 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -whatwg-url@^6.5.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" - integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== - dependencies: - lodash.sortby "^4.7.0" - tr46 "^1.0.1" - webidl-conversions "^4.0.2" - whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" @@ -12650,8 +12641,16 @@ workbox-window@7.3.0, workbox-window@^7.3.0: "@types/trusted-types" "^2.0.2" workbox-core "7.3.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==