From d5646ff4d3e4ba585fc887033b73caea40207c4d Mon Sep 17 00:00:00 2001 From: Obada Khalili Date: Wed, 8 Feb 2023 12:08:35 +0200 Subject: [PATCH 1/5] feat(spy) implement mock.withImplementation API --- packages/spy/src/index.ts | 19 ++ pnpm-lock.yaml | 324 +++++++++++++++++++++++++--------- test/core/test/mocked.test.ts | 26 +++ 3 files changed, 286 insertions(+), 83 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 6a57cf60a357..4ff80b801c72 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -46,6 +46,7 @@ export interface SpyInstance { getMockImplementation(): ((...args: TArgs) => TReturns) | undefined mockImplementation(fn: ((...args: TArgs) => TReturns) | (() => Promise)): this mockImplementationOnce(fn: ((...args: TArgs) => TReturns) | (() => Promise)): this + withImplementation(fn: ((...args: TArgs) => TReturns), cb: () => void): this mockReturnThis(): this mockReturnValue(obj: TReturns): this mockReturnValueOnce(obj: TReturns): this @@ -248,6 +249,24 @@ function enhanceSpy( return stub } + stub.withImplementation = (fn, cb) => { + const originalImplementation = implementation! + stub.mockImplementation(fn) + + if (cb.constructor.name === 'AsyncFunction') { + const asyncCb = cb as () => Promise + + asyncCb().then(() => { + stub.mockImplementation(originalImplementation) + }) + } + else { + stub.mockImplementation(originalImplementation) + } + + return stub + } + stub.mockReturnThis = () => stub.mockImplementation(function (this: TReturns) { return this diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fba6c3d1cbfc..857aa050c5ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,20 +110,20 @@ importers: vue: latest workbox-window: ^6.5.4 dependencies: - '@vueuse/core': 9.10.0_vue@3.2.45 + '@vueuse/core': 9.10.0_vue@3.2.47 jiti: 1.16.1 - vue: 3.2.45 + vue: 3.2.47 devDependencies: '@iconify-json/carbon': 1.1.13 '@unocss/reset': 0.48.3 '@vite-pwa/vitepress': 0.0.4_vite-plugin-pwa@0.14.1 - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 esno: 0.16.3 fast-glob: 3.2.12 fs-extra: 10.1.0 https-localhost: 4.7.1 unocss: 0.48.3_vite@4.0.0 - unplugin-vue-components: 0.22.12_vue@3.2.45 + unplugin-vue-components: 0.22.12_vue@3.2.47 vite: 4.0.0 vite-plugin-pwa: 0.14.1_hsd7wmhtxdre4ds2vghqydhvau vitepress: 1.0.0-alpha.40 @@ -242,9 +242,9 @@ importers: react-dom: 18.0.0_react@18.0.0 devDependencies: '@testing-library/react': 13.3.0_zpnidt7m3osuk7shl3s4oenomq - '@types/node': 18.11.18 + '@types/node': 18.11.19 '@types/react': 18.0.27 - '@vitejs/plugin-react': 3.0.1 + '@vitejs/plugin-react': 3.1.0 jsdom: 21.1.0 typescript: 4.8.4 vitest: link:../../packages/vitest @@ -292,9 +292,9 @@ importers: devDependencies: '@types/react': 17.0.49 '@types/react-test-renderer': 17.0.2 - '@vitejs/plugin-react': 3.0.1_vite@4.0.0 + '@vitejs/plugin-react': 3.1.0_vite@4.0.0 '@vitest/ui': link:../../packages/ui - happy-dom: 8.1.5 + happy-dom: 8.2.6 jsdom: 21.1.0 react-test-renderer: 17.0.2_react@17.0.2 vite: 4.0.0 @@ -321,7 +321,7 @@ importers: '@types/enzyme': 3.10.12 '@types/react': 17.0.49 '@types/react-dom': 17.0.17 - '@vitejs/plugin-react': 3.0.1_vite@4.0.0 + '@vitejs/plugin-react': 3.1.0_vite@4.0.0 '@vitest/ui': link:../../packages/ui '@wojtekmaj/enzyme-adapter-react-17': 0.6.7_7ltvq4e2railvf5uya4ffxpe2a enzyme: 3.11.0 @@ -503,13 +503,13 @@ importers: vitest: workspace:* vue: latest devDependencies: - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 - '@vue/test-utils': 2.2.8_vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 + '@vue/test-utils': 2.2.10_vue@3.2.47 jsdom: 21.1.0 vite: 4.0.0 vite-plugin-ruby: 3.1.2_vite@4.0.0 vitest: link:../../packages/vitest - vue: 3.2.45 + vue: 3.2.47 examples/solid: specifiers: @@ -559,13 +559,13 @@ importers: vitest: workspace:* vue: latest dependencies: - vue: 3.2.45 + vue: 3.2.47 devDependencies: - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 - '@vue/test-utils': 2.0.2_vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 + '@vue/test-utils': 2.0.2_vue@3.2.47 jsdom: 21.1.0 unplugin-auto-import: 0.11.2_vite@4.0.0 - unplugin-vue-components: 0.22.4_vite@4.0.0+vue@3.2.45 + unplugin-vue-components: 0.22.4_vite@4.0.0+vue@3.2.47 vite: 4.0.0 vitest: link:../../packages/vitest @@ -578,10 +578,10 @@ importers: vitest: workspace:* vue: latest dependencies: - vue: 3.2.45 + vue: 3.2.47 devDependencies: - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 - '@vue/test-utils': 2.0.0_vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 + '@vue/test-utils': 2.0.0_vue@3.2.47 jsdom: 21.1.0 vite: 4.0.0 vitest: link:../../packages/vitest @@ -596,13 +596,13 @@ importers: vitest: workspace:* vue: latest devDependencies: - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 - '@vitejs/plugin-vue-jsx': 3.0.0_vite@4.0.0+vue@3.2.45 - '@vue/test-utils': 2.2.8_vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 + '@vitejs/plugin-vue-jsx': 3.0.0_vite@4.0.0+vue@3.2.47 + '@vue/test-utils': 2.2.10_vue@3.2.47 jsdom: 21.1.0 vite: 4.0.0 vitest: link:../../packages/vitest - vue: 3.2.45 + vue: 3.2.47 examples/vue2: specifiers: @@ -1072,12 +1072,12 @@ importers: vitest: workspace:* vue: latest devDependencies: - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 - '@vue/test-utils': 2.2.8_vue@3.2.45 - happy-dom: 8.1.5 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 + '@vue/test-utils': 2.2.10_vue@3.2.47 + happy-dom: 8.2.6 vite: 4.0.0 vitest: link:../../packages/vitest - vue: 3.2.45 + vue: 3.2.47 test/css: specifiers: @@ -1293,9 +1293,6 @@ packages: peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' - peerDependenciesMeta: - '@algolia/client-search': - optional: true dependencies: '@algolia/autocomplete-shared': 1.7.2 algoliasearch: 4.14.2 @@ -4988,7 +4985,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.18 + '@types/node': 18.11.19 '@types/yargs': 15.0.14 chalk: 4.1.2 dev: true @@ -5000,7 +4997,7 @@ packages: '@jest/schemas': 29.0.0 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 18.11.18 + '@types/node': 18.11.19 '@types/yargs': 17.0.12 chalk: 4.1.2 dev: true @@ -5512,7 +5509,7 @@ packages: engines: {node: '>=14'} hasBin: true dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 playwright-core: 1.28.0 dev: true @@ -7338,7 +7335,7 @@ packages: /@types/cheerio/0.22.31: resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/codemirror/5.60.6: @@ -7405,33 +7402,33 @@ packages: resolution: {integrity: sha512-zdV5odfHf95B4qr6bdpshG4VMm/3xgnPhSJLa3xh75CYr35e34k+4FQli82Q48sPqwHazJGy+6+jl4T+Vw1AMg==} dependencies: '@types/jsonfile': 6.1.1 - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/fs-extra/9.0.13: resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/glob/7.2.0: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/glob/8.0.0: resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} dependencies: '@types/minimatch': 5.1.1 - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/graceful-fs/4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/hast/2.3.4: @@ -7492,7 +7489,7 @@ packages: /@types/jsdom/20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 '@types/tough-cookie': 4.0.2 parse5: 7.1.1 dev: true @@ -7508,7 +7505,7 @@ packages: /@types/jsonfile/6.1.1: resolution: {integrity: sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/lodash/4.14.191: @@ -7542,7 +7539,7 @@ packages: /@types/node-fetch/2.6.2: resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 form-data: 3.0.1 dev: true @@ -7558,6 +7555,10 @@ packages: resolution: {integrity: sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==} dev: true + /@types/node/18.11.19: + resolution: {integrity: sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw==} + dev: true + /@types/node/18.7.13: resolution: {integrity: sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==} dev: false @@ -7588,7 +7589,7 @@ packages: /@types/prompts/2.4.2: resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 kleur: 3.0.3 dev: true @@ -7661,7 +7662,7 @@ packages: /@types/resolve/1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/resolve/1.20.2: @@ -7678,7 +7679,7 @@ packages: /@types/set-cookie-parser/2.4.2: resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/sinonjs__fake-timers/8.1.1: @@ -7765,7 +7766,7 @@ packages: /@types/webpack-sources/3.2.0: resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 '@types/source-list-map': 0.1.2 source-map: 0.7.4 dev: true @@ -7773,7 +7774,7 @@ packages: /@types/webpack/4.41.32: resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 '@types/tapable': 1.0.8 '@types/uglify-js': 3.17.0 '@types/webpack-sources': 3.2.0 @@ -7784,7 +7785,7 @@ packages: /@types/ws/8.5.4: resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true /@types/yargs-parser/21.0.0: @@ -7807,7 +7808,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 dev: true optional: true @@ -8200,11 +8201,11 @@ packages: - supports-color dev: true - /@vitejs/plugin-react/3.0.1: - resolution: {integrity: sha512-mx+QvYwIbbpOIJw+hypjnW1lAbKDHtWK5ibkF/V1/oMBu8HU/chb+SnqJDAsLq1+7rGqjktCEomMTM5KShzUKQ==} + /@vitejs/plugin-react/3.1.0: + resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.0.0 + vite: ^4.1.0-beta.0 dependencies: '@babel/core': 7.20.12 '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.20.12 @@ -8215,11 +8216,11 @@ packages: - supports-color dev: true - /@vitejs/plugin-react/3.0.1_vite@4.0.0: - resolution: {integrity: sha512-mx+QvYwIbbpOIJw+hypjnW1lAbKDHtWK5ibkF/V1/oMBu8HU/chb+SnqJDAsLq1+7rGqjktCEomMTM5KShzUKQ==} + /@vitejs/plugin-react/3.1.0_vite@4.0.0: + resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - vite: ^4.0.0 + vite: ^4.1.0-beta.0 dependencies: '@babel/core': 7.20.12 '@babel/plugin-transform-react-jsx-self': 7.18.6_@babel+core@7.20.12 @@ -8247,6 +8248,22 @@ packages: - supports-color dev: true + /@vitejs/plugin-vue-jsx/3.0.0_vite@4.0.0+vue@3.2.47: + resolution: {integrity: sha512-vurkuzgac5SYuxd2HUZqAFAWGTF10diKBwJNbCvnWijNZfXd+7jMtqjPFbGt7idOJUn584fP1Ar9j/GN2jQ3Ew==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 + vue: ^3.0.0 + dependencies: + '@babel/core': 7.20.12 + '@babel/plugin-transform-typescript': 7.20.2_@babel+core@7.20.12 + '@vue/babel-plugin-jsx': 1.1.1_@babel+core@7.20.12 + vite: 4.0.0 + vue: 3.2.47 + transitivePeerDependencies: + - supports-color + dev: true + /@vitejs/plugin-vue/4.0.0_vite@4.0.0+vue@3.2.45: resolution: {integrity: sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8258,6 +8275,17 @@ packages: vue: 3.2.45 dev: true + /@vitejs/plugin-vue/4.0.0_vite@4.0.0+vue@3.2.47: + resolution: {integrity: sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 + vue: ^3.2.25 + dependencies: + vite: 4.0.0 + vue: 3.2.47 + dev: true + /@vitejs/plugin-vue2/1.1.2_vite@4.0.0+vue@2.7.10: resolution: {integrity: sha512-y6OEA+2UdJ0xrEQHodq20v9r3SpS62IOHrgN92JPLvVpNkhcissu7yvD5PXMzMESyazj0XNWGsc8UQk8+mVrjQ==} engines: {node: '>=14.6.0'} @@ -8299,11 +8327,11 @@ packages: dependencies: '@volar/code-gen': 0.40.13 '@volar/source-map': 0.40.13 - '@vue/compiler-core': 3.2.45 - '@vue/compiler-dom': 3.2.45 - '@vue/compiler-sfc': 3.2.45 - '@vue/reactivity': 3.2.45 - '@vue/shared': 3.2.45 + '@vue/compiler-core': 3.2.47 + '@vue/compiler-dom': 3.2.47 + '@vue/compiler-sfc': 3.2.47 + '@vue/reactivity': 3.2.47 + '@vue/shared': 3.2.47 dev: true /@volar/vue-typescript/0.40.13: @@ -8350,6 +8378,15 @@ packages: '@vue/shared': 3.2.45 estree-walker: 2.0.2 source-map: 0.6.1 + dev: true + + /@vue/compiler-core/3.2.47: + resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==} + dependencies: + '@babel/parser': 7.20.7 + '@vue/shared': 3.2.47 + estree-walker: 2.0.2 + source-map: 0.6.1 /@vue/compiler-dom/3.2.39: resolution: {integrity: sha512-HMFI25Be1C8vLEEv1hgEO1dWwG9QQ8LTTPmCkblVJY/O3OvWx6r1+zsox5mKPMGvqYEZa6l8j+xgOfUspgo7hw==} @@ -8362,6 +8399,13 @@ packages: dependencies: '@vue/compiler-core': 3.2.45 '@vue/shared': 3.2.45 + dev: true + + /@vue/compiler-dom/3.2.47: + resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} + dependencies: + '@vue/compiler-core': 3.2.47 + '@vue/shared': 3.2.47 /@vue/compiler-sfc/2.7.10: resolution: {integrity: sha512-55Shns6WPxlYsz4WX7q9ZJBL77sKE1ZAYNYStLs6GbhIOMrNtjMvzcob6gu3cGlfpCR4bT7NXgyJ3tly2+Hx8Q==} @@ -8397,6 +8441,21 @@ packages: magic-string: 0.25.9 postcss: 8.4.19 source-map: 0.6.1 + dev: true + + /@vue/compiler-sfc/3.2.47: + resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} + dependencies: + '@babel/parser': 7.20.7 + '@vue/compiler-core': 3.2.47 + '@vue/compiler-dom': 3.2.47 + '@vue/compiler-ssr': 3.2.47 + '@vue/reactivity-transform': 3.2.47 + '@vue/shared': 3.2.47 + estree-walker: 2.0.2 + magic-string: 0.25.9 + postcss: 8.4.19 + source-map: 0.6.1 /@vue/compiler-ssr/3.2.39: resolution: {integrity: sha512-EoGCJ6lincKOZGW+0Ky4WOKsSmqL7hp1ZYgen8M7u/mlvvEQUaO9tKKOy7K43M9U2aA3tPv0TuYYQFrEbK2eFQ==} @@ -8409,6 +8468,13 @@ packages: dependencies: '@vue/compiler-dom': 3.2.45 '@vue/shared': 3.2.45 + dev: true + + /@vue/compiler-ssr/3.2.47: + resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} + dependencies: + '@vue/compiler-dom': 3.2.47 + '@vue/shared': 3.2.47 /@vue/devtools-api/6.4.5: resolution: {integrity: sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==} @@ -8431,6 +8497,16 @@ packages: '@vue/shared': 3.2.45 estree-walker: 2.0.2 magic-string: 0.25.9 + dev: true + + /@vue/reactivity-transform/3.2.47: + resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==} + dependencies: + '@babel/parser': 7.20.7 + '@vue/compiler-core': 3.2.47 + '@vue/shared': 3.2.47 + estree-walker: 2.0.2 + magic-string: 0.25.9 /@vue/reactivity/3.2.38: resolution: {integrity: sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw==} @@ -8447,6 +8523,12 @@ packages: resolution: {integrity: sha512-PRvhCcQcyEVohW0P8iQ7HDcIOXRjZfAsOds3N99X/Dzewy8TVhTCT4uXpAHfoKjVTJRA0O0K+6QNkDIZAxNi3A==} dependencies: '@vue/shared': 3.2.45 + dev: true + + /@vue/reactivity/3.2.47: + resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==} + dependencies: + '@vue/shared': 3.2.47 /@vue/runtime-core/3.2.39: resolution: {integrity: sha512-xKH5XP57JW5JW+8ZG1khBbuLakINTgPuINKL01hStWLTTGFOrM49UfCFXBcFvWmSbci3gmJyLl2EAzCaZWsx8g==} @@ -8459,6 +8541,13 @@ packages: dependencies: '@vue/reactivity': 3.2.45 '@vue/shared': 3.2.45 + dev: true + + /@vue/runtime-core/3.2.47: + resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==} + dependencies: + '@vue/reactivity': 3.2.47 + '@vue/shared': 3.2.47 /@vue/runtime-dom/3.2.39: resolution: {integrity: sha512-4G9AEJP+sLhsqf5wXcyKVWQKUhI+iWfy0hWQgea+CpaTD7BR0KdQzvoQdZhwCY6B3oleSyNLkLAQwm0ya/wNoA==} @@ -8473,6 +8562,14 @@ packages: '@vue/runtime-core': 3.2.45 '@vue/shared': 3.2.45 csstype: 2.6.20 + dev: true + + /@vue/runtime-dom/3.2.47: + resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==} + dependencies: + '@vue/runtime-core': 3.2.47 + '@vue/shared': 3.2.47 + csstype: 2.6.20 /@vue/server-renderer/3.2.39_vue@3.2.39: resolution: {integrity: sha512-1yn9u2YBQWIgytFMjz4f/t0j43awKytTGVptfd3FtBk76t1pd8mxbek0G/DrnjJhd2V7mSTb5qgnxMYt8Z5iSQ==} @@ -8491,6 +8588,16 @@ packages: '@vue/compiler-ssr': 3.2.45 '@vue/shared': 3.2.45 vue: 3.2.45 + dev: true + + /@vue/server-renderer/3.2.47_vue@3.2.47: + resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==} + peerDependencies: + vue: 3.2.47 + dependencies: + '@vue/compiler-ssr': 3.2.47 + '@vue/shared': 3.2.47 + vue: 3.2.47 /@vue/shared/3.2.38: resolution: {integrity: sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==} @@ -8501,6 +8608,10 @@ packages: /@vue/shared/3.2.45: resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==} + dev: true + + /@vue/shared/3.2.47: + resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} /@vue/test-utils/1.3.0_42puyn3dcxirnpdjnosl7pbb6a: resolution: {integrity: sha512-Xk2Xiyj2k5dFb8eYUKkcN9PzqZSppTlx7LaQWBbdA8tqh3jHr/KHX2/YLhNFc/xwDrgeLybqd+4ZCPJSGPIqeA==} @@ -8515,30 +8626,31 @@ packages: vue-template-compiler: 2.7.10 dev: true - /@vue/test-utils/2.0.0_vue@3.2.45: + /@vue/test-utils/2.0.0_vue@3.2.47: resolution: {integrity: sha512-zL5kygNq7hONrO1CzaUGprEAklAX+pH8J1MPMCU3Rd2xtSYkZ+PmKU3oEDRg8VAGdL5lNJHzDgrud5amFPtirw==} peerDependencies: vue: ^3.0.1 dependencies: - vue: 3.2.45 + vue: 3.2.47 dev: true - /@vue/test-utils/2.0.2_vue@3.2.45: + /@vue/test-utils/2.0.2_vue@3.2.47: resolution: {integrity: sha512-E2P4oXSaWDqTZNbmKZFVLrNN/siVN78YkEqs7pHryWerrlZR9bBFLWdJwRoguX45Ru6HxIflzKl4vQvwRMwm5g==} peerDependencies: vue: ^3.0.1 dependencies: - vue: 3.2.45 + vue: 3.2.47 dev: true - /@vue/test-utils/2.2.8_vue@3.2.45: - resolution: {integrity: sha512-/R8DKzp41Ip/RqTt1jvOVi5gxby3EwNWiYHNYsG9FAjEvt0gzDvYN55lCKzX7IdnI5zVIOo5tHtts0SLT+JrWw==} + /@vue/test-utils/2.2.10_vue@3.2.47: + resolution: {integrity: sha512-UPY+VdWST5vYZ/Qhl+sLuJAv596e6kTbrOPgdGY82qd9kGN/MfjzLT5KXlmpChkiCbPP3abZ8XT25u1n5h+mRg==} peerDependencies: - '@vue/compiler-dom': ^3.0.1 vue: ^3.0.1 dependencies: js-beautify: 1.14.6 - vue: 3.2.45 + vue: 3.2.47 + optionalDependencies: + '@vue/compiler-dom': 3.2.47 dev: true /@vueuse/core/8.9.4_vue@3.2.39: @@ -8569,6 +8681,18 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue + dev: true + + /@vueuse/core/9.10.0_vue@3.2.47: + resolution: {integrity: sha512-CxMewME07qeuzuT/AOIQGv0EhhDoojniqU6pC3F8m5VC76L47UT18DcX88kWlP3I7d3qMJ4u/PD8iSRsy3bmNA==} + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.10.0 + '@vueuse/shared': 9.10.0_vue@3.2.47 + vue-demi: 0.13.11_vue@3.2.47 + transitivePeerDependencies: + - '@vue/composition-api' + - vue /@vueuse/integrations/8.9.4_axios@0.26.1+vue@3.2.39: resolution: {integrity: sha512-Nk7mH0ThTdiuiiuB+1lyC+5ihnywrr+9h9IA4R4Ry8Mli/cZL38zc3qZWIsCVPm66Lr+7kEp3nnHdSxKi7ivrg==} @@ -8643,6 +8767,15 @@ packages: transitivePeerDependencies: - '@vue/composition-api' - vue + dev: true + + /@vueuse/shared/9.10.0_vue@3.2.47: + resolution: {integrity: sha512-vakHJ2ZRklAzqmcVBL38RS7BxdBA4+5poG9NsSyqJxrt9kz0zX3P5CXMy0Hm6LFbZXUgvKdqAS3pUH1zX/5qTQ==} + dependencies: + vue-demi: 0.13.11_vue@3.2.47 + transitivePeerDependencies: + - '@vue/composition-api' + - vue /@webassemblyjs/ast/1.11.1: resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} @@ -13607,8 +13740,8 @@ packages: - encoding dev: true - /happy-dom/8.1.5: - resolution: {integrity: sha512-/UXAJ2fHTs4H3vy7TS7c9PKFvPyaNialk2Er9NdXfpBKNaCITMOH03rkjHXp5jnJnSmRBa+av8E08PUAaIB1jQ==} + /happy-dom/8.2.6: + resolution: {integrity: sha512-s53VwyMFpQPEZdN00M82i3tFTzz0T4kBVD4tu8b+im99s1NkLK6tfKGCCl2Jmf3ZWfFCRwS+DV2qkR7S1wmIhQ==} dependencies: css.escape: 1.5.1 he: 1.2.0 @@ -14741,7 +14874,7 @@ packages: dependencies: '@jest/types': 26.6.2 '@types/graceful-fs': 4.1.5 - '@types/node': 18.11.18 + '@types/node': 18.11.19 anymatch: 3.1.2 fb-watchman: 2.0.1 graceful-fs: 4.2.10 @@ -14809,7 +14942,7 @@ packages: resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} engines: {node: '>= 10.14.2'} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 graceful-fs: 4.2.10 dev: true @@ -14818,7 +14951,7 @@ packages: engines: {node: '>= 10.14.2'} dependencies: '@jest/types': 26.6.2 - '@types/node': 18.11.18 + '@types/node': 18.11.19 chalk: 4.1.2 graceful-fs: 4.2.10 is-ci: 2.0.0 @@ -14830,7 +14963,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.0.1 - '@types/node': 18.11.18 + '@types/node': 18.11.19 chalk: 4.1.2 ci-info: 3.7.0 graceful-fs: 4.2.10 @@ -14841,7 +14974,7 @@ packages: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 merge-stream: 2.0.0 supports-color: 7.2.0 dev: true @@ -14850,7 +14983,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.11.18 + '@types/node': 18.11.19 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -20559,7 +20692,7 @@ packages: - supports-color dev: true - /unplugin-vue-components/0.22.12_vue@3.2.45: + /unplugin-vue-components/0.22.12_vue@3.2.47: resolution: {integrity: sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==} engines: {node: '>=14'} peerDependencies: @@ -20579,13 +20712,13 @@ packages: minimatch: 5.1.1 resolve: 1.22.1 unplugin: 1.0.1 - vue: 3.2.45 + vue: 3.2.47 transitivePeerDependencies: - rollup - supports-color dev: true - /unplugin-vue-components/0.22.4_vite@4.0.0+vue@3.2.45: + /unplugin-vue-components/0.22.4_vite@4.0.0+vue@3.2.47: resolution: {integrity: sha512-2rRZcM9OnJGXnYxQNfaceEYuPeVACcWySIjy8WBwIiN3onr980TmA3XE5pRJFt8zoQrUA+c46oyIq96noLqrEQ==} engines: {node: '>=14'} peerDependencies: @@ -20605,7 +20738,7 @@ packages: minimatch: 5.1.0 resolve: 1.22.1 unplugin: 0.9.5_vite@4.0.0 - vue: 3.2.45 + vue: 3.2.47 transitivePeerDependencies: - esbuild - rollup @@ -21045,13 +21178,13 @@ packages: dependencies: '@docsearch/css': 3.3.1 '@docsearch/js': 3.3.1 - '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.45 + '@vitejs/plugin-vue': 4.0.0_vite@4.0.0+vue@3.2.47 '@vue/devtools-api': 6.4.5 - '@vueuse/core': 9.10.0_vue@3.2.45 + '@vueuse/core': 9.10.0_vue@3.2.47 body-scroll-lock: 4.0.0-beta.0 shiki: 0.12.1 vite: 4.0.0 - vue: 3.2.45 + vue: 3.2.47 transitivePeerDependencies: - '@algolia/client-search' - '@types/node' @@ -21116,6 +21249,21 @@ packages: optional: true dependencies: vue: 3.2.45 + dev: true + + /vue-demi/0.13.11_vue@3.2.47: + resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.2.47 /vue-eslint-parser/9.1.0_eslint@8.31.0: resolution: {integrity: sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==} @@ -21194,6 +21342,16 @@ packages: '@vue/runtime-dom': 3.2.45 '@vue/server-renderer': 3.2.45_vue@3.2.45 '@vue/shared': 3.2.45 + dev: true + + /vue/3.2.47: + resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==} + dependencies: + '@vue/compiler-dom': 3.2.47 + '@vue/compiler-sfc': 3.2.47 + '@vue/runtime-dom': 3.2.47 + '@vue/server-renderer': 3.2.47_vue@3.2.47 + '@vue/shared': 3.2.47 /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} diff --git a/test/core/test/mocked.test.ts b/test/core/test/mocked.test.ts index f6824991d730..1fd4fa97ba31 100644 --- a/test/core/test/mocked.test.ts +++ b/test/core/test/mocked.test.ts @@ -169,3 +169,29 @@ describe('mocked function which fails on toReturnWith', () => { test('streams', () => { expect(exportedStream).toBeDefined() }) + +describe('temporary mock implementation', () => { + test('temporary mock implementation works as expected', () => { + const mock = vi.fn(() => 1) + + mock.withImplementation(() => 2, () => { + expect(mock()).toBe(2) + expect(mock()).toBe(2) + }) + + expect(mock()).toBe(1) + }) + + test('async temporary mock implementation works as expecetd', async () => { + const mock = vi.fn(() => 1) + + await mock.withImplementation(() => 2, async () => { + await Promise.resolve() + + expect(mock()).toBe(2) + expect(mock()).toBe(2) + }) + + expect(mock()).toBe(1) + }) +}) From da12da6e2809a3f3704cba834a134589bbc9ddaa Mon Sep 17 00:00:00 2001 From: Obada Khalili Date: Fri, 10 Feb 2023 22:38:10 +0200 Subject: [PATCH 2/5] feat(spy): better type the method to account for async callbacks, add missing call to callback in function, refine the function's implementation, give precedence to 'withImplementation' over 'mockImplementationOnce', and add more tests --- packages/spy/src/index.ts | 33 +++++++++++++++------- test/core/test/mocked.test.ts | 53 ++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 4ff80b801c72..25e800e1256b 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -46,7 +46,8 @@ export interface SpyInstance { getMockImplementation(): ((...args: TArgs) => TReturns) | undefined mockImplementation(fn: ((...args: TArgs) => TReturns) | (() => Promise)): this mockImplementationOnce(fn: ((...args: TArgs) => TReturns) | (() => Promise)): this - withImplementation(fn: ((...args: TArgs) => TReturns), cb: () => void): this + withImplementation(fn: ((...args: TArgs) => T), cb: () => void): this + withImplementation(fn: ((...args: TArgs) => T), cb: () => Promise): Promise mockReturnThis(): this mockReturnValue(obj: TReturns): this mockReturnValueOnce(obj: TReturns): this @@ -209,6 +210,7 @@ function enhanceSpy( } let onceImplementations: ((...args: TArgs) => TReturns)[] = [] + let implementationChangedTemporarily = false let name: string = (stub as any).name @@ -249,24 +251,35 @@ function enhanceSpy( return stub } - stub.withImplementation = (fn, cb) => { + function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void): EnhancedSpy + function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => Promise): Promise> + function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void | Promise): EnhancedSpy | Promise> { const originalImplementation = implementation! + stub.mockImplementation(fn) + implementationChangedTemporarily = true + + const clean = () => { + stub.mockImplementation(originalImplementation) + implementationChangedTemporarily = false + } - if (cb.constructor.name === 'AsyncFunction') { - const asyncCb = cb as () => Promise + const result = cb() - asyncCb().then(() => { - stub.mockImplementation(originalImplementation) + if (result instanceof Promise) { + return result.then(() => { + clean() + return stub }) } - else { - stub.mockImplementation(originalImplementation) - } + + clean() return stub } + stub.withImplementation = withImplementation + stub.mockReturnThis = () => stub.mockImplementation(function (this: TReturns) { return this @@ -294,7 +307,7 @@ function enhanceSpy( stub.willCall(function (this: unknown, ...args) { instances.push(this) invocations.push(++callOrder) - const impl = onceImplementations.shift() || implementation || stub.getOriginal() || (() => {}) + const impl = implementationChangedTemporarily ? implementation! : onceImplementations.shift() || implementation || stub.getOriginal() || (() => {}) return impl.apply(this, args) }) diff --git a/test/core/test/mocked.test.ts b/test/core/test/mocked.test.ts index 1fd4fa97ba31..c6b8aa20917e 100644 --- a/test/core/test/mocked.test.ts +++ b/test/core/test/mocked.test.ts @@ -174,6 +174,8 @@ describe('temporary mock implementation', () => { test('temporary mock implementation works as expected', () => { const mock = vi.fn(() => 1) + expect.assertions(3) + mock.withImplementation(() => 2, () => { expect(mock()).toBe(2) expect(mock()).toBe(2) @@ -182,9 +184,24 @@ describe('temporary mock implementation', () => { expect(mock()).toBe(1) }) - test('async temporary mock implementation works as expecetd', async () => { + test('temporary mock implementation return value can be of different type than the original', async () => { + const mock = vi.fn(() => 1) + + expect.assertions(3) + + mock.withImplementation(() => '2', () => { + expect(mock()).toBe('2') + expect(mock()).toBe('2') + }) + + expect(mock()).toBe(1) + }) + + test('temporary mock implementation with async callback works as expecetd', async () => { const mock = vi.fn(() => 1) + expect.assertions(3) + await mock.withImplementation(() => 2, async () => { await Promise.resolve() @@ -194,4 +211,38 @@ describe('temporary mock implementation', () => { expect(mock()).toBe(1) }) + + test('temporary mock implementation can be async', async () => { + const mock = vi.fn(() => 1) + + expect.assertions(3) + + /* NOTE: + * If the callback has to be async, either because the mock implementation is async or because the callback body is async for some other reason: + * - The async/await syntax must be used inside the callback because the mock variable type doesn't change to async just because the mock implementation changed temporarily, so you can't use the `.then` method with mock variable. + * - The async/await syntax must be used with the call to `mock.withImplementation` because of the same reason. + * + * This is not very good, because at anytime, the variables type should represent its value, which is important in terms of API design. + */ + await mock.withImplementation(async () => 2, async () => { + expect(await mock()).toBe(2) + expect(await mock()).toBe(2) + }) + + expect(mock()).toBe(1) + }) + + test('temporary mock implementation takes precedence over mockImplementationOnce', () => { + const mock = vi.fn(() => 1) + + expect.assertions(3) + + mock.mockImplementationOnce(() => 2) + mock.withImplementation(() => 3, () => { + expect(mock()).toBe(3) + expect(mock()).toBe(3) + }) + + expect(mock()).toBe(2) + }) }) From 1d2df6c45e59a3aecde867e41150682850bd9235 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Sat, 11 Feb 2023 13:50:31 +0100 Subject: [PATCH 3/5] fix: update withImplementation types, add test --- packages/spy/src/index.ts | 9 ++++----- test/core/test/mocked.test.ts | 32 ++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 25e800e1256b..9c34a6719bdc 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -46,8 +46,7 @@ export interface SpyInstance { getMockImplementation(): ((...args: TArgs) => TReturns) | undefined mockImplementation(fn: ((...args: TArgs) => TReturns) | (() => Promise)): this mockImplementationOnce(fn: ((...args: TArgs) => TReturns) | (() => Promise)): this - withImplementation(fn: ((...args: TArgs) => T), cb: () => void): this - withImplementation(fn: ((...args: TArgs) => T), cb: () => Promise): Promise + withImplementation(fn: ((...args: TArgs) => TReturns), cb: () => T): T extends Promise ? Promise : this mockReturnThis(): this mockReturnValue(obj: TReturns): this mockReturnValueOnce(obj: TReturns): this @@ -254,13 +253,13 @@ function enhanceSpy( function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void): EnhancedSpy function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => Promise): Promise> function withImplementation(fn: (...args: TArgs) => TReturns, cb: () => void | Promise): EnhancedSpy | Promise> { - const originalImplementation = implementation! + const originalImplementation = implementation - stub.mockImplementation(fn) + implementation = fn implementationChangedTemporarily = true const clean = () => { - stub.mockImplementation(originalImplementation) + implementation = originalImplementation implementationChangedTemporarily = false } diff --git a/test/core/test/mocked.test.ts b/test/core/test/mocked.test.ts index c6b8aa20917e..cb32fdc26737 100644 --- a/test/core/test/mocked.test.ts +++ b/test/core/test/mocked.test.ts @@ -184,14 +184,29 @@ describe('temporary mock implementation', () => { expect(mock()).toBe(1) }) + test('original implementation restored as undefined, when there is none', () => { + const mock = vi.fn() + + expect.assertions(5) + + mock.withImplementation(() => 2, () => { + expect(mock.getMockImplementation()).toBeTypeOf('function') + expect(mock()).toBe(2) + expect(mock()).toBe(2) + }) + + expect(mock()).toBe(undefined) + expect(mock.getMockImplementation()).toBe(undefined) + }) + test('temporary mock implementation return value can be of different type than the original', async () => { const mock = vi.fn(() => 1) expect.assertions(3) - mock.withImplementation(() => '2', () => { - expect(mock()).toBe('2') - expect(mock()).toBe('2') + mock.withImplementation(() => 2, () => { + expect(mock()).toBe(2) + expect(mock()).toBe(2) }) expect(mock()).toBe(1) @@ -213,23 +228,16 @@ describe('temporary mock implementation', () => { }) test('temporary mock implementation can be async', async () => { - const mock = vi.fn(() => 1) + const mock = vi.fn(async () => 1) expect.assertions(3) - /* NOTE: - * If the callback has to be async, either because the mock implementation is async or because the callback body is async for some other reason: - * - The async/await syntax must be used inside the callback because the mock variable type doesn't change to async just because the mock implementation changed temporarily, so you can't use the `.then` method with mock variable. - * - The async/await syntax must be used with the call to `mock.withImplementation` because of the same reason. - * - * This is not very good, because at anytime, the variables type should represent its value, which is important in terms of API design. - */ await mock.withImplementation(async () => 2, async () => { expect(await mock()).toBe(2) expect(await mock()).toBe(2) }) - expect(mock()).toBe(1) + expect(await mock()).toBe(1) }) test('temporary mock implementation takes precedence over mockImplementationOnce', () => { From da2e0f88d0e1ef5de3b5bfc36a69472385d2ce69 Mon Sep 17 00:00:00 2001 From: Obada Khalili Date: Sun, 12 Feb 2023 15:14:34 +0200 Subject: [PATCH 4/5] change clean function name to reset in method --- packages/spy/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index 9c34a6719bdc..b855f8c7b73e 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -258,7 +258,7 @@ function enhanceSpy( implementation = fn implementationChangedTemporarily = true - const clean = () => { + const reset = () => { implementation = originalImplementation implementationChangedTemporarily = false } @@ -267,12 +267,12 @@ function enhanceSpy( if (result instanceof Promise) { return result.then(() => { - clean() + reset() return stub }) } - clean() + reset() return stub } From 1359ace2cd5492acdeec15061b413ae91fe1437d Mon Sep 17 00:00:00 2001 From: Obada Khalili Date: Sun, 12 Feb 2023 15:42:50 +0200 Subject: [PATCH 5/5] feat(docs): add withImplementation docs --- docs/api/mock.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/api/mock.md b/docs/api/mock.md index cbdf294fcb47..40777340294a 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -91,6 +91,43 @@ You should use spy assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeen console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()) ``` +## withImplementation + +- **Type:** `(fn: Function, callback: () => void) => MockInstance` +- **Type:** `(fn: Function, callback: () => Promise) => Promise` + + Overrides the original mock implementation temporarily while the callback is being executed. + + ```js + const myMockFn = vi.fn(() => 'original') + + myMockFn.withImplementation(() => 'temp', () => { + myMockFn() // 'temp' + }) + + myMockFn() // 'original' + ``` + + Can be used with an asynchronous callback. The method has to be awaited to use the original implementation afterward. + + ```ts + test('async callback', () => { + const myMockFn = vi.fn(() => 'original') + + // We await this call since the callback is async + await myMockFn.withImplementation( + () => 'temp', + async () => { + myMockFn() // 'temp' + }, + ) + + myMockFn() // 'original' + }) + ``` + + Also, it takes precedence over the [`mockImplementationOnce`](https://vitest.dev/api/mock.html#mockimplementationonce). + ## mockRejectedValue - **Type:** `(value: any) => MockInstance`