From 88b8d15f41f894011cf33af0b1f4a3fb24aed46b Mon Sep 17 00:00:00 2001 From: Dima Voytenko Date: Mon, 14 Aug 2023 19:31:00 -0700 Subject: [PATCH] Test proxy mode: intercept the http and https client APIs (#54014) Followup on the https://github.com/vercel/next.js/pull/52520. The enhancements: 1. Both `fetch` and `http(s)` APIs are intercepted in the testmode. 2. The loopback mode that would allow the test-side to use any fetch-mocking library of their choice. --- packages/next/package.json | 1 + .../@mswjs/interceptors/ClientRequest/LICENSE | 9 +++ .../interceptors/ClientRequest/index.js | 1 + .../interceptors/ClientRequest/package.json | 1 + .../testmode/playwright/README.md | 23 ++++++ .../experimental/testmode/playwright/index.ts | 48 +++++++++--- .../experimental/testmode/playwright/msw.ts | 5 +- .../testmode/playwright/next-fixture.ts | 33 ++++++-- .../testmode/playwright/next-options.ts | 3 + .../next/src/experimental/testmode/server.ts | 76 +++++++++++-------- packages/next/taskfile.js | 18 +++++ packages/next/types/misc.d.ts | 4 + pnpm-lock.yaml | 30 ++++++++ 13 files changed, 202 insertions(+), 50 deletions(-) create mode 100644 packages/next/src/compiled/@mswjs/interceptors/ClientRequest/LICENSE create mode 100644 packages/next/src/compiled/@mswjs/interceptors/ClientRequest/index.js create mode 100644 packages/next/src/compiled/@mswjs/interceptors/ClientRequest/package.json create mode 100644 packages/next/src/experimental/testmode/playwright/next-options.ts diff --git a/packages/next/package.json b/packages/next/package.json index 8b510958a4..240a44bb14 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -141,6 +141,7 @@ "@hapi/accept": "5.0.2", "@jest/transform": "29.5.0", "@jest/types": "29.5.0", + "@mswjs/interceptors": "0.23.0", "@napi-rs/cli": "2.16.2", "@napi-rs/triples": "1.1.0", "@next/polyfill-module": "13.4.16", diff --git a/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/LICENSE b/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/LICENSE new file mode 100644 index 0000000000..e5c461e5fd --- /dev/null +++ b/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2018–present Artem Zakharchenko + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/index.js b/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/index.js new file mode 100644 index 0000000000..c1e77f64d7 --- /dev/null +++ b/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/index.js @@ -0,0 +1 @@ +(function(){var e={501:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}var n=r(858);var s=r(331);var i=r(685);var o=_interopRequireDefault(i);var a=r(687);var u=_interopRequireDefault(a);var c=r(362);var l=r(984);var f=new(0,l.Logger)("utils getUrlByRequestOptions");function normalizeClientRequestEndArgs(...e){f.info("arguments",e);const t=new Array(3).fill(null).map(((t,r)=>e[r]||t));t.sort(((e,r)=>{if(typeof e==="function"){return 1}if(typeof r==="function"){return-1}if(typeof e==="string"&&typeof r==="string"){return t.indexOf(e)-t.indexOf(r)}return 0}));f.info("normalized args",t);return t}var h=new(0,l.Logger)("http normalizeWriteArgs");function normalizeClientRequestWriteArgs(e){h.info("normalizing ClientRequest.write arguments...",e);const t=e[0];const r=typeof e[1]==="string"?e[1]:void 0;const n=typeof e[1]==="function"?e[1]:e[2];const s=[t,r,n];h.info("successfully normalized ClientRequest.write arguments:",s);return s}var p=r(781);var d=Symbol("isClone");function cloneIncomingMessage(e){const t=e.pipe(new(0,p.PassThrough));inheritProperties(e,t);const r=Object.create(i.IncomingMessage.prototype);getPrototypes(t).forEach((e=>{inheritProperties(e,r)}));Object.setPrototypeOf(t,r);Object.defineProperty(t,d,{enumerable:true,value:true});return t}function getPrototypes(e){const t=[];let r=e;while(r=Object.getPrototypeOf(r)){t.push(r)}return t}function inheritProperties(e,t){const r=[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)];for(const n of r){if(t.hasOwnProperty(n)){continue}const r=Object.getOwnPropertyDescriptor(e,n);if(!r){continue}Object.defineProperty(t,n,r)}}var g=r(426);function createResponse(e){const t=new ReadableStream({start(t){e.on("data",(e=>t.enqueue(e)));e.on("end",(()=>t.close()))}});return new Response(t,{status:e.statusCode,statusText:e.statusMessage,headers:g.objectToHeaders.call(void 0,e.headers)})}function createRequest(e){const t=new(0,g.Headers);const r=e.getHeaders();for(const e in r){const n=r[e];if(!n){continue}const s=Array.prototype.concat([],n);for(const r of s){t.append(e,r.toString())}}const n=e.method||"GET";return new Request(e.url,{method:n,headers:t,credentials:"same-origin",body:n==="HEAD"||n==="GET"?null:e.requestBuffer})}var m=r(642);var v=class extends i.ClientRequest{constructor([e,t,r],n){super(t,r);this.chunks=[];this.responseSource="mock";this.logger=n.logger.extend(`request ${t.method} ${e.href}`);this.logger.info("constructing ClientRequest using options:",{url:e,requestOptions:t,callback:r});this.url=e;this.emitter=n.emitter;this.requestBuffer=null;this.response=new(0,i.IncomingMessage)(this.socket)}writeRequestBodyChunk(e,t){if(e==null){return}if(this.requestBuffer==null){this.requestBuffer=Buffer.from([])}const r=Buffer.isBuffer(e)?e:Buffer.from(e,t);this.requestBuffer=Buffer.concat([this.requestBuffer,r])}write(...e){var t;const[r,n,s]=normalizeClientRequestWriteArgs(e);this.logger.info("write:",{chunk:r,encoding:n,callback:s});this.chunks.push({chunk:r,encoding:n});this.writeRequestBodyChunk(r,n);this.logger.info("chunk successfully stored!",(t=this.requestBuffer)==null?void 0:t.byteLength);if(!r||r.length===0){this.logger.info("written chunk is empty, skipping callback...")}else{s==null?void 0:s()}return true}end(...e){this.logger.info("end",e);const t=n.uuidv4.call(void 0);const[r,s,i]=normalizeClientRequestEndArgs(...e);this.logger.info("normalized arguments:",{chunk:r,encoding:s,callback:i});this.writeRequestBodyChunk(r,s||void 0);const o=createRequest(this);const a=n.toInteractiveRequest.call(void 0,o);if(this.getHeader("X-Request-Id")!=null){this.removeHeader("X-Request-Id");return this.passthrough(r,s,i)}this.logger.info('emitting the "request" event for %d listener(s)...',this.emitter.listenerCount("request"));this.emitter.emit("request",{request:a,requestId:t});c.until.call(void 0,(async()=>{await this.emitter.untilIdle("request",(({args:[{requestId:e}]})=>e===t));const[e]=await a.respondWith.invoked();this.logger.info("event.respondWith called with:",e);return e})).then((e=>{this.logger.info("the listeners promise awaited!");if(!this.headersSent){for(const[e,t]of o.headers){this.setHeader(e,t)}}if(e.error){this.logger.info("encountered resolver exception, aborting request...",e.error);this.emit("error",e.error);this.terminate();return this}const n=e.data;if(n){const e=n.clone();this.logger.info("received mocked response:",n);this.responseSource="mock";this.respondWith(n);this.logger.info(n.status,n.statusText,"(MOCKED)");i==null?void 0:i();this.logger.info('emitting the custom "response" event...');this.emitter.emit("response",{response:e,isMockedResponse:true,request:o,requestId:t});this.logger.info("request (mock) is completed");return this}this.logger.info("no mocked response received!");this.once("response-internal",(e=>{this.logger.info(e.statusCode,e.statusMessage);this.logger.info("original response headers:",e.headers);this.logger.info('emitting the custom "response" event...');this.emitter.emit("response",{response:createResponse(e),isMockedResponse:false,request:o,requestId:t})}));return this.passthrough(r,s,i)}));return this}emit(e,...t){this.logger.info("emit: %s",e);if(e==="response"){this.logger.info('found "response" event, cloning the response...');try{const r=t[0];const n=cloneIncomingMessage(r);const s=cloneIncomingMessage(r);this.emit("response-internal",s);this.logger.info('response successfully cloned, emitting "response" event...');return super.emit(e,n,...t.slice(1))}catch(r){this.logger.info("error when cloning response:",r);return super.emit(e,...t)}}if(e==="error"){const e=t[0];const r=e.code||"";this.logger.info("error:\n",e);if(this.responseSource==="mock"&&v.suppressErrorCodes.includes(r)){if(!this.capturedError){this.capturedError=e;this.logger.info("captured the first error:",this.capturedError)}return false}}return super.emit(e,...t)}passthrough(e,t,r){this.responseSource="bypass";if(this.capturedError){this.emit("error",this.capturedError);return this}this.logger.info("writing request chunks...",this.chunks);for(const{chunk:e,encoding:t}of this.chunks){if(t){super.write(e,t)}else{super.write(e)}}this.once("error",(e=>{this.logger.info("original request error:",e)}));this.once("abort",(()=>{this.logger.info("original request aborted!")}));this.once("response-internal",(e=>{this.logger.info(e.statusCode,e.statusMessage);this.logger.info("original response headers:",e.headers)}));this.logger.info("performing original request...");return super.end(...[e,t,r].filter(Boolean))}respondWith(e){this.logger.info("responding with a mocked response...",e);Object.defineProperties(this,{writableFinished:{value:true},writableEnded:{value:true}});this.emit("finish");const{status:t,statusText:r,headers:n,body:s}=e;this.response.statusCode=t;this.response.statusMessage=r;if(n){this.response.headers={};n.forEach(((e,t)=>{this.response.rawHeaders.push(t,e);const r=t.toLowerCase();const n=this.response.headers[r];this.response.headers[r]=n?Array.prototype.concat([],n,e):e}))}this.logger.info("mocked response headers ready:",n);const i=new(0,m.DeferredPromise);const finishResponseStream=()=>{this.logger.info("finished response stream!");i.resolve()};if(s){const e=s.getReader();const readNextChunk=async()=>{const{done:t,value:r}=await e.read();if(t){finishResponseStream();return}this.response.emit("data",r);return readNextChunk()};readNextChunk()}else{finishResponseStream()}this.res=this.response;this.emit("response",this.response);i.then((()=>{this.logger.info("finalizing response...");this.response.push(null);this.response.complete=true;this.response.emit("end");this.terminate()}))}terminate(){var e;(e=this.agent)==null?void 0:e.destroy()}};var y=v;y.suppressErrorCodes=["ENOTFOUND","ECONNREFUSED","ECONNRESET","EAI_AGAIN"];function getRequestOptionsByUrl(e){const t={method:"GET",protocol:e.protocol,hostname:typeof e.hostname==="string"&&e.hostname.startsWith("[")?e.hostname.slice(1,-1):e.hostname,host:e.host,path:`${e.pathname}${e.search||""}`};if(!!e.port){t.port=Number(e.port)}if(e.username||e.password){t.auth=`${e.username}:${e.password}`}return t}var b=new(0,l.Logger)("utils getUrlByRequestOptions");var O="/";var w="http:";var q="localhost";var x=443;function getAgent(e){return e.agent instanceof i.Agent?e.agent:void 0}function getProtocolByRequestOptions(e){var t;if(e.protocol){return e.protocol}const r=getAgent(e);const n=r==null?void 0:r.protocol;if(n){return n}const s=getPortByRequestOptions(e);const i=e.cert||s===x;return i?"https:":((t=e.uri)==null?void 0:t.protocol)||w}function getPortByRequestOptions(e){if(e.port){return Number(e.port)}if(e.hostname!=null){const[,t]=e.hostname.match(/:(\d+)$/)||[];if(t!=null){return Number(t)}}const t=getAgent(e);if(t==null?void 0:t.options.port){return Number(t.options.port)}if(t==null?void 0:t.defaultPort){return Number(t.defaultPort)}return void 0}function getHostByRequestOptions(e){const{hostname:t,host:r}=e;if(t!=null){return t.replace(/:\d+$/,"")}return r||q}function getAuthByRequestOptions(e){if(e.auth){const[t,r]=e.auth.split(":");return{username:t,password:r}}}function isRawIPv6Address(e){return e.includes(":")&&!e.startsWith("[")&&!e.endsWith("]")}function getHostname(e,t){const r=typeof t!=="undefined"?`:${t}`:"";if(isRawIPv6Address(e)){return`[${e}]${r}`}if(typeof t==="undefined"){return e}return`${e}${r}`}function getUrlByRequestOptions(e){b.info("request options",e);if(e.uri){b.info('constructing url from explicitly provided "options.uri": %s',e.uri);return new URL(e.uri.href)}b.info("figuring out url from request options...");const t=getProtocolByRequestOptions(e);b.info("protocol",t);const r=getHostByRequestOptions(e);b.info("host",r);const n=getPortByRequestOptions(e);b.info("port",n);const s=getHostname(r,n);b.info("hostname",s);const i=e.path||O;b.info("path",i);const o=getAuthByRequestOptions(e);b.info("credentials",o);const a=o?`${o.username}:${o.password}@`:"";b.info("auth string:",a);const u=new URL(`${t}//${a}${s}${i}`);b.info("created url:",u);return u}var j=new(0,l.Logger)("cloneObject");function isPlainObject(e){var t;j.info("is plain object?",e);if(e==null||!((t=e.constructor)==null?void 0:t.name)){j.info("given object is undefined, not a plain object...");return false}j.info("checking the object constructor:",e.constructor.name);return e.constructor.name==="Object"}function cloneObject(e){j.info("cloning object:",e);const t=Object.entries(e).reduce(((e,[t,r])=>{j.info("analyzing key-value pair:",t,r);e[t]=isPlainObject(r)?cloneObject(r):r;return e}),{});return isPlainObject(e)?t:Object.assign(Object.getPrototypeOf(e),t)}function isObject(e){return Object.prototype.toString.call(e)==="[object Object]"}var P=new(0,l.Logger)("http normalizeClientRequestArgs");function resolveRequestOptions(e,t){if(typeof e[1]==="undefined"||typeof e[1]==="function"){P.info("request options not provided, deriving from the url",t);return getRequestOptionsByUrl(t)}if(e[1]){P.info("has custom RequestOptions!",e[1]);const r=getRequestOptionsByUrl(t);P.info("derived RequestOptions from the URL:",r);P.info("cloning RequestOptions...");const n=cloneObject(e[1]);P.info("successfully cloned RequestOptions!",n);return{...r,...n}}P.info("using an empty object as request options");return{}}function resolveCallback(e){return typeof e[1]==="function"?e[1]:e[2]}function normalizeClientRequestArgs(e,...t){let r;let n;let s;P.info("arguments",t);P.info("using default protocol:",e);if(typeof t[0]==="string"){P.info("first argument is a location string:",t[0]);r=new URL(t[0]);P.info("created a url:",r);const e=getRequestOptionsByUrl(r);P.info("request options from url:",e);n=resolveRequestOptions(t,r);P.info("resolved request options:",n);s=resolveCallback(t)}else if(t[0]instanceof URL){r=t[0];P.info("first argument is a URL:",r);n=resolveRequestOptions(t,r);P.info("derived request options:",n);s=resolveCallback(t)}else if("hash"in t[0]&&!("method"in t[0])){const[r]=t;P.info("first argument is a legacy URL:",r);if(r.hostname===null){P.info("given legacy URL is relative (no hostname)");return isObject(t[1])?normalizeClientRequestArgs(e,{path:r.path,...t[1]},t[2]):normalizeClientRequestArgs(e,{path:r.path},t[1])}P.info("given legacy url is absolute");const n=new URL(r.href);return t[1]===void 0?normalizeClientRequestArgs(e,n):typeof t[1]==="function"?normalizeClientRequestArgs(e,n,t[1]):normalizeClientRequestArgs(e,n,t[1],t[2])}else if(isObject(t[0])){n=t[0];P.info("first argument is RequestOptions:",n);n.protocol=n.protocol||e;P.info("normalized request options:",n);r=getUrlByRequestOptions(n);P.info("created a URL from RequestOptions:",r.href);s=resolveCallback(t)}else{throw new Error(`Failed to construct ClientRequest with these parameters: ${t}`)}n.protocol=n.protocol||r.protocol;n.method=n.method||"GET";if(typeof n.agent==="undefined"){const e=n.protocol==="https:"?new(0,a.Agent)({rejectUnauthorized:n.rejectUnauthorized}):new(0,i.Agent);n.agent=e;P.info("resolved fallback agent:",e)}if(!n._defaultAgent){P.info('has no default agent, setting the default agent for "%s"',n.protocol);n._defaultAgent=n.protocol==="https:"?a.globalAgent:i.globalAgent}P.info("successfully resolved url:",r.href);P.info("successfully resolved options:",n);P.info("successfully resolved callback:",s);return[r,n,s]}function get(e,t){return(...r)=>{const n=normalizeClientRequestArgs(`${e}:`,...r);const s=new y(n,t);s.end();return s}}var E=new(0,l.Logger)("http request");function request(e,t){return(...r)=>{E.info('request call (protocol "%s"):',e,r);const n=normalizeClientRequestArgs(`${e}:`,...r);return new y(n,t)}}var L=class extends s.Interceptor{constructor(){super(L.interceptorSymbol);this.modules=new Map;this.modules.set("http",o.default);this.modules.set("https",u.default)}setup(){const e=this.logger.extend("setup");for(const[t,r]of this.modules){const{request:n,get:s}=r;this.subscriptions.push((()=>{r.request=n;r.get=s;e.info('native "%s" module restored!',t)}));const i={emitter:this.emitter,logger:this.logger};r.request=request(t,i);r.get=get(t,i);e.info('native "%s" module patched!',t)}}};var _=L;_.interceptorSymbol=Symbol("http");t.ClientRequestInterceptor=_},331:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});var n=(e=>true?require:0)((function(e){if(true)return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')}));var s=r(984);var i=r(162);function nextTick(e){setTimeout(e,0)}var o=class extends i.Emitter{constructor(){super();this.logger=new(0,s.Logger)("async-event-emitter");this.queue=new Map;this.readyState="ACTIVE"}on(e,t){const r=this.logger.extend("on");r.info('adding "%s" listener...',e);if(this.readyState==="DEACTIVATED"){r.info("the emitter is destroyed, skipping!");return this}return super.on(e,(async(...n)=>{const s=this.openListenerQueue(e);r.info('awaiting the "%s" listener...',e);s.push({args:n,done:new Promise((async(s,i)=>{try{await t(...n);s();r.info('"%s" listener has resolved!',e)}catch(e){r.info('"%s" listener has rejected!',e);i(e)}}))})}))}emit(e,...t){const r=this.logger.extend("emit");r.info('emitting "%s" event...',e);if(this.readyState==="DEACTIVATED"){r.info("the emitter is destroyed, skipping!");return false}if(this.isInternalEventName(e)){return super.emit(e,...t)}this.openListenerQueue(e);r.info('appending a one-time cleanup "%s" listener...',e);this.once(e,(()=>{nextTick((()=>{this.queue.delete(e);r.info('cleaned up "%s" listeners queue!',e)}))}));return super.emit(e,...t)}async untilIdle(e,t=(()=>true)){const r=this.queue.get(e)||[];await Promise.all(r.filter(t).map((({done:e})=>e))).finally((()=>{this.queue.delete(e)}))}openListenerQueue(e){const t=this.logger.extend("openListenerQueue");t.info('opening "%s" listeners queue...',e);const r=this.queue.get(e);if(!r){t.info("no queue found, creating one...");this.queue.set(e,[]);return[]}t.info("returning an exising queue:",r);return r}removeAllListeners(e){const t=this.logger.extend("removeAllListeners");t.info("event:",e);if(e){this.queue.delete(e);t.info('cleared the "%s" listeners queue!',e,this.queue.get(e))}else{this.queue.clear();t.info("cleared the listeners queue!",this.queue)}return super.removeAllListeners(e)}activate(){const e=this.logger.extend("activate");this.readyState="ACTIVE";e.info("set state to:",this.readyState)}deactivate(){const e=this.logger.extend("deactivate");e.info("removing all listeners...");this.removeAllListeners();this.readyState="DEACTIVATED";e.info("set state to:",this.readyState)}isInternalEventName(e){return e==="newListener"||e==="removeListener"}};function getGlobalSymbol(e){return globalThis[e]||void 0}function setGlobalSymbol(e,t){globalThis[e]=t}function deleteGlobalSymbol(e){delete globalThis[e]}var a=(e=>{e["INACTIVE"]="INACTIVE";e["APPLYING"]="APPLYING";e["APPLIED"]="APPLIED";e["DISPOSING"]="DISPOSING";e["DISPOSED"]="DISPOSED";return e})(a||{});var u=class{constructor(e){this.symbol=e;this.readyState="INACTIVE";this.emitter=new o;this.subscriptions=[];this.logger=new(0,s.Logger)(e.description);this.emitter.setMaxListeners(0);this.logger.info("constructing the interceptor...")}checkEnvironment(){return true}apply(){const e=this.logger.extend("apply");e.info("applying the interceptor...");if(this.readyState==="APPLIED"){e.info("intercepted already applied!");return}const t=this.checkEnvironment();if(!t){e.info("the interceptor cannot be applied in this environment!");return}this.readyState="APPLYING";this.emitter.activate();e.info("activated the emiter!",this.emitter.readyState);const r=this.getInstance();if(r){e.info("found a running instance, reusing...");this.on=(t,n)=>{e.info('proxying the "%s" listener',t);r.emitter.addListener(t,n);this.subscriptions.push((()=>{r.emitter.removeListener(t,n);e.info('removed proxied "%s" listener!',t)}))};this.readyState="APPLIED";return}e.info("no running instance found, setting up a new instance...");this.setup();this.setInstance();this.readyState="APPLIED"}setup(){}on(e,t){const r=this.logger.extend("on");if(this.readyState==="DISPOSING"||this.readyState==="DISPOSED"){r.info("cannot listen to events, already disposed!");return}r.info('adding "%s" event listener:',e,t.name);this.emitter.on(e,t)}dispose(){const e=this.logger.extend("dispose");if(this.readyState==="DISPOSED"){e.info("cannot dispose, already disposed!");return}e.info("disposing the interceptor...");this.readyState="DISPOSING";if(!this.getInstance()){e.info("no interceptors running, skipping dispose...");return}this.clearInstance();e.info("global symbol deleted:",getGlobalSymbol(this.symbol));if(this.subscriptions.length>0){e.info("disposing of %d subscriptions...",this.subscriptions.length);for(const e of this.subscriptions){e()}this.subscriptions=[];e.info("disposed of all subscriptions!",this.subscriptions.length)}this.emitter.deactivate();e.info("destroyed the listener!");this.readyState="DISPOSED"}getInstance(){var e;const t=getGlobalSymbol(this.symbol);this.logger.info("retrieved global instance:",(e=t==null?void 0:t.constructor)==null?void 0:e.name);return t}setInstance(){setGlobalSymbol(this.symbol,this);this.logger.info("set global instance!",this.symbol.description)}clearInstance(){deleteGlobalSymbol(this.symbol);this.logger.info("cleared global instance!",this.symbol.description)}};t.__require=n;t.getGlobalSymbol=getGlobalSymbol;t.deleteGlobalSymbol=deleteGlobalSymbol;t.InterceptorReadyState=a;t.Interceptor=u},858:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});var n=r(270);function createLazyCallback(e={}){let t=0;let r;let n;const s=new Promise((e=>{n=e})).finally((()=>{clearTimeout(r)}));const fn=function(...r){var s;if(e.maxCalls&&t>=e.maxCalls){(s=e.maxCallsCallback)==null?void 0:s.call(e)}n(r);t++};fn.invoked=async()=>{r=setTimeout((()=>{n([])}),0);return s};return fn}function toInteractiveRequest(e){Object.defineProperty(e,"respondWith",{writable:false,enumerable:true,value:createLazyCallback({maxCalls:1,maxCallsCallback(){throw new Error(n.format.call(void 0,'Failed to respond to "%s %s" request: the "request" event has already been responded to.',e.method,e.url))}})});return e}function uuidv4(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(e){const t=Math.random()*16|0;const r=e=="x"?t:t&3|8;return r.toString(16)}))}t.toInteractiveRequest=toInteractiveRequest;t.uuidv4=uuidv4},596:function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});t.DeferredPromise=void 0;const n=r(738);class DeferredPromise extends Promise{#e;resolve;reject;constructor(e=null){const t=(0,n.createDeferredExecutor)();super(((r,n)=>{t(r,n);e?.(t.resolve,t.reject)}));this.#e=t;this.resolve=this.#e.resolve;this.reject=this.#e.reject}get state(){return this.#e.state}get rejectionReason(){return this.#e.rejectionReason}then(e,t){return this.#t(super.then(e,t))}catch(e){return this.#t(super.catch(e))}finally(e){return this.#t(super.finally(e))}#t(e){return Object.defineProperties(e,{resolve:{configurable:true,value:this.resolve},reject:{configurable:true,value:this.reject}})}}t.DeferredPromise=DeferredPromise},738:function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:true});t.createDeferredExecutor=void 0;function createDeferredExecutor(){const executor=(e,t)=>{executor.state="pending";executor.resolve=t=>{if(executor.state!=="pending"){return}executor.result=t;const onFulfilled=e=>{executor.state="fulfilled";return e};return e(t instanceof Promise?t:Promise.resolve(t).then(onFulfilled))};executor.reject=e=>{if(executor.state!=="pending"){return}queueMicrotask((()=>{executor.state="rejected"}));return t(executor.rejectionReason=e)}};return executor}t.createDeferredExecutor=createDeferredExecutor},642:function(e,t,r){"use strict";var n=this&&this.__createBinding||(Object.create?function(e,t,r,n){if(n===undefined)n=r;var s=Object.getOwnPropertyDescriptor(t,r);if(!s||("get"in s?!t.__esModule:s.writable||s.configurable)){s={enumerable:true,get:function(){return t[r]}}}Object.defineProperty(e,n,s)}:function(e,t,r,n){if(n===undefined)n=r;e[n]=t[r]});var s=this&&this.__exportStar||function(e,t){for(var r in e)if(r!=="default"&&!Object.prototype.hasOwnProperty.call(t,r))n(t,e,r)};Object.defineProperty(t,"__esModule",{value:true});s(r(738),t);s(r(596),t)},984:function(e,t,r){var n=Object.defineProperty;var s=Object.getOwnPropertyDescriptor;var i=Object.getOwnPropertyNames;var o=Object.prototype.hasOwnProperty;var __export=(e,t)=>{for(var r in t)n(e,r,{get:t[r],enumerable:true})};var __copyProps=(e,t,r,a)=>{if(t&&typeof t==="object"||typeof t==="function"){for(let u of i(t))if(!o.call(e,u)&&u!==r)n(e,u,{get:()=>t[u],enumerable:!(a=s(t,u))||a.enumerable})}return e};var __toCommonJS=e=>__copyProps(n({},"__esModule",{value:true}),e);var a={};__export(a,{Logger:()=>h});e.exports=__toCommonJS(a);var u=r(576);var c=r(270);var l={};__export(l,{blue:()=>blue,gray:()=>gray,green:()=>green,red:()=>red,yellow:()=>yellow});function yellow(e){return`${e}`}function blue(e){return`${e}`}function gray(e){return`${e}`}function red(e){return`${e}`}function green(e){return`${e}`}var f=(0,u.isNodeProcess)();var h=class{constructor(e){this.name=e;this.prefix=`[${this.name}]`;const t=getVariable("DEBUG");const r=getVariable("LOG_LEVEL");const n=t==="1"||t==="true"||typeof t!=="undefined"&&this.name.startsWith(t);if(n){this.debug=isDefinedAndNotEquals(r,"debug")?noop:this.debug;this.info=isDefinedAndNotEquals(r,"info")?noop:this.info;this.success=isDefinedAndNotEquals(r,"success")?noop:this.success;this.warning=isDefinedAndNotEquals(r,"warning")?noop:this.warning;this.error=isDefinedAndNotEquals(r,"error")?noop:this.error}else{this.info=noop;this.success=noop;this.warning=noop;this.error=noop;this.only=noop}}prefix;extend(e){return new h(`${this.name}:${e}`)}debug(e,...t){this.logEntry({level:"debug",message:gray(e),positionals:t,prefix:this.prefix,colors:{prefix:"gray"}})}info(e,...t){this.logEntry({level:"info",message:e,positionals:t,prefix:this.prefix,colors:{prefix:"blue"}});const r=new p;return(e,...t)=>{r.measure();this.logEntry({level:"info",message:`${e} ${gray(`${r.deltaTime}ms`)}`,positionals:t,prefix:this.prefix,colors:{prefix:"blue"}})}}success(e,...t){this.logEntry({level:"info",message:e,positionals:t,prefix:`✔ ${this.prefix}`,colors:{timestamp:"green",prefix:"green"}})}warning(e,...t){this.logEntry({level:"warning",message:e,positionals:t,prefix:`⚠ ${this.prefix}`,colors:{timestamp:"yellow",prefix:"yellow"}})}error(e,...t){this.logEntry({level:"error",message:e,positionals:t,prefix:`✖ ${this.prefix}`,colors:{timestamp:"red",prefix:"red"}})}only(e){e()}createEntry(e,t){return{timestamp:new Date,level:e,message:t}}logEntry(e){const{level:t,message:r,prefix:n,colors:s,positionals:i=[]}=e;const o=this.createEntry(t,r);const a=s?.timestamp||"gray";const u=s?.prefix||"gray";const c={timestamp:l[a],prefix:l[u]};const f=this.getWriter(t);f([c.timestamp(this.formatTimestamp(o.timestamp))].concat(n!=null?c.prefix(n):[]).concat(serializeInput(r)).join(" "),...i.map(serializeInput))}formatTimestamp(e){return`${e.toLocaleTimeString("en-GB")}:${e.getMilliseconds()}`}getWriter(e){switch(e){case"debug":case"success":case"info":{return log}case"warning":{return warn}case"error":{return error}}}};var p=class{startTime;endTime;deltaTime;constructor(){this.startTime=performance.now()}measure(){this.endTime=performance.now();const e=this.endTime-this.startTime;this.deltaTime=e.toFixed(2)}};var noop=()=>void 0;function log(e,...t){if(f){process.stdout.write((0,c.format)(e,...t)+"\n");return}console.log(e,...t)}function warn(e,...t){if(f){process.stderr.write((0,c.format)(e,...t)+"\n");return}console.warn(e,...t)}function error(e,...t){if(f){process.stderr.write((0,c.format)(e,...t)+"\n");return}console.error(e,...t)}function getVariable(e){if(f){return process.env[e]}return globalThis[e]?.toString()}function isDefinedAndNotEquals(e,t){return e!==void 0&&e!==t}function serializeInput(e){if(typeof e==="undefined"){return"undefined"}if(e===null){return"null"}if(typeof e==="string"){return e}if(typeof e==="object"){return JSON.stringify(e)}return e.toString()}},362:function(e){var t=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var n=Object.getOwnPropertyNames;var s=Object.prototype.hasOwnProperty;var __export=(e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:true})};var __copyProps=(e,i,o,a)=>{if(i&&typeof i==="object"||typeof i==="function"){for(let u of n(i))if(!s.call(e,u)&&u!==o)t(e,u,{get:()=>i[u],enumerable:!(a=r(i,u))||a.enumerable})}return e};var __toCommonJS=e=>__copyProps(t({},"__esModule",{value:true}),e);var i={};__export(i,{until:()=>until});e.exports=__toCommonJS(i);var until=async e=>{try{const t=await e().catch((e=>{throw e}));return{error:null,data:t}}catch(e){return{error:e,data:null}}};0&&0},426:function(e){var t=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var n=Object.getOwnPropertyNames;var s=Object.prototype.hasOwnProperty;var __export=(e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:true})};var __copyProps=(e,i,o,a)=>{if(i&&typeof i==="object"||typeof i==="function"){for(let u of n(i))if(!s.call(e,u)&&u!==o)t(e,u,{get:()=>i[u],enumerable:!(a=r(i,u))||a.enumerable})}return e};var __toCommonJS=e=>__copyProps(t({},"__esModule",{value:true}),e);var i={};__export(i,{Headers:()=>f,flattenHeadersList:()=>flattenHeadersList,flattenHeadersObject:()=>flattenHeadersObject,headersToList:()=>headersToList,headersToObject:()=>headersToObject,headersToString:()=>headersToString,listToHeaders:()=>listToHeaders,objectToHeaders:()=>objectToHeaders,reduceHeadersObject:()=>reduceHeadersObject,stringToHeaders:()=>stringToHeaders});e.exports=__toCommonJS(i);var o=/[^a-z0-9\-#$%&'*+.^_`|~]/i;function normalizeHeaderName(e){if(typeof e!=="string"){e=String(e)}if(o.test(e)||e.trim()===""){throw new TypeError("Invalid character in header field name")}return e.toLowerCase()}function normalizeHeaderValue(e){if(typeof e!=="string"){e=String(e)}return e}var a=Symbol("normalizedHeaders");var u=Symbol("rawHeaderNames");var c,l;var f=class{constructor(e){this[c]={};this[l]=new Map;if(["Headers","HeadersPolyfill"].includes(e==null?void 0:e.constructor.name)||e instanceof f){const t=e;t.forEach(((e,t)=>{this.append(t,e)}),this)}else if(Array.isArray(e)){e.forEach((([e,t])=>{this.append(e,Array.isArray(t)?t.join(", "):t)}))}else if(e){Object.getOwnPropertyNames(e).forEach((t=>{const r=e[t];this.append(t,Array.isArray(r)?r.join(", "):r)}))}}[(c=a,l=u,Symbol.iterator)](){return this.entries()}*keys(){for(const e of Object.keys(this[a])){yield e}}*values(){for(const e of Object.values(this[a])){yield e}}*entries(){for(const e of Object.keys(this[a])){yield[e,this.get(e)]}}get(e){return this[a][normalizeHeaderName(e)]||null}set(e,t){const r=normalizeHeaderName(e);this[a][r]=normalizeHeaderValue(t);this[u].set(r,e)}append(e,t){const r=normalizeHeaderName(e);let n=this.has(r)?`${this.get(r)}, ${t}`:t;this.set(e,n)}delete(e){if(!this.has(e)){return}const t=normalizeHeaderName(e);delete this[a][t];this[u].delete(t)}all(){return this[a]}raw(){const e={};for(const[t,r]of this.entries()){e[this[u].get(t)]=r}return e}has(e){return this[a].hasOwnProperty(normalizeHeaderName(e))}forEach(e,t){for(const r in this[a]){if(this[a].hasOwnProperty(r)){e.call(t,this[a][r],r,this)}}}};function headersToList(e){const t=[];e.forEach(((e,r)=>{const n=e.includes(",")?e.split(",").map((e=>e.trim())):e;t.push([r,n])}));return t}function headersToString(e){const t=headersToList(e);const r=t.map((([e,t])=>{const r=[].concat(t);return`${e}: ${r.join(", ")}`}));return r.join("\r\n")}var h=["user-agent"];function headersToObject(e){const t={};e.forEach(((e,r)=>{const n=!h.includes(r.toLowerCase())&&e.includes(",");t[r]=n?e.split(",").map((e=>e.trim())):e}));return t}function stringToHeaders(e){const t=e.trim().split(/[\r\n]+/);return t.reduce(((e,t)=>{if(t.trim()===""){return e}const r=t.split(": ");const n=r.shift();const s=r.join(": ");e.append(n,s);return e}),new f)}function listToHeaders(e){const t=new f;e.forEach((([e,r])=>{const n=[].concat(r);n.forEach((r=>{t.append(e,r)}))}));return t}function reduceHeadersObject(e,t,r){return Object.keys(e).reduce(((r,n)=>t(r,n,e[n])),r)}function objectToHeaders(e){return reduceHeadersObject(e,((e,t,r)=>{const n=[].concat(r).filter(Boolean);n.forEach((r=>{e.append(t,r)}));return e}),new f)}function flattenHeadersList(e){return e.map((([e,t])=>[e,[].concat(t).join(", ")]))}function flattenHeadersObject(e){return reduceHeadersObject(e,((e,t,r)=>{e[t]=[].concat(r).join(", ");return e}),{})}0&&0},576:function(e){var t=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var n=Object.getOwnPropertyNames;var s=Object.prototype.hasOwnProperty;var __export=(e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:true})};var __copyProps=(e,i,o,a)=>{if(i&&typeof i==="object"||typeof i==="function"){for(let u of n(i))if(!s.call(e,u)&&u!==o)t(e,u,{get:()=>i[u],enumerable:!(a=r(i,u))||a.enumerable})}return e};var __toCommonJS=e=>__copyProps(t({},"__esModule",{value:true}),e);var i={};__export(i,{isNodeProcess:()=>isNodeProcess});e.exports=__toCommonJS(i);function isNodeProcess(){if(typeof navigator!=="undefined"&&navigator.product==="ReactNative"){return true}if(typeof process!=="undefined"){const e=process.type;if(e==="renderer"||e==="worker"){return false}return!!(process.versions&&process.versions.node)}return false}0&&0},270:function(e){"use strict";var t=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var n=Object.getOwnPropertyNames;var s=Object.prototype.hasOwnProperty;var __export=(e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:true})};var __copyProps=(e,i,o,a)=>{if(i&&typeof i==="object"||typeof i==="function"){for(let u of n(i))if(!s.call(e,u)&&u!==o)t(e,u,{get:()=>i[u],enumerable:!(a=r(i,u))||a.enumerable})}return e};var __toCommonJS=e=>__copyProps(t({},"__esModule",{value:true}),e);var i={};__export(i,{InvariantError:()=>u,format:()=>format,invariant:()=>invariant});e.exports=__toCommonJS(i);var o=/(%?)(%([sdjo]))/g;function serializePositional(e,t){switch(t){case"s":return e;case"d":case"i":return Number(e);case"j":return JSON.stringify(e);case"o":{if(typeof e==="string"){return e}const t=JSON.stringify(e);if(t==="{}"||t==="[]"||/^\[object .+?\]$/.test(t)){return e}return t}}}function format(e,...t){if(t.length===0){return e}let r=0;let n=e.replace(o,((e,n,s,i)=>{const o=t[r];const a=serializePositional(o,i);if(!n){r++;return a}return e}));if(r{if(!e){throw new u(t,...r)}};invariant.as=(e,t,r,...n)=>{if(!t){const t=e.prototype.name!=null;const s=t?new e(format(r,n)):e(format(r,n));throw s}};0&&0},162:function(e){var t=Object.defineProperty;var r=Object.getOwnPropertyDescriptor;var n=Object.getOwnPropertyNames;var s=Object.prototype.hasOwnProperty;var __export=(e,r)=>{for(var n in r)t(e,n,{get:r[n],enumerable:true})};var __copyProps=(e,i,o,a)=>{if(i&&typeof i==="object"||typeof i==="function"){for(let u of n(i))if(!s.call(e,u)&&u!==o)t(e,u,{get:()=>i[u],enumerable:!(a=r(i,u))||a.enumerable})}return e};var __toCommonJS=e=>__copyProps(t({},"__esModule",{value:true}),e);var i={};__export(i,{Emitter:()=>u,MemoryLeakError:()=>o});e.exports=__toCommonJS(i);var o=class extends Error{constructor(e,t,r){super(`Possible EventEmitter memory leak detected. ${r} ${t.toString()} listeners added. Use emitter.setMaxListeners() to increase limit`);this.emitter=e;this.type=t;this.count=r;this.name="MaxListenersExceededWarning"}};var a=class{static listenerCount(e,t){return e.listenerCount(t)}constructor(){this.events=new Map;this.maxListeners=a.defaultMaxListeners;this.hasWarnedAboutPotentialMemoryLeak=false}_emitInternalEvent(e,t,r){this.emit(e,...[t,r])}_getListeners(e){return this.events.get(e)||[]}_removeListener(e,t){const r=e.indexOf(t);if(r>-1){e.splice(r,1)}return[]}_wrapOnceListener(e,t){const onceListener=(...r)=>{this.removeListener(e,onceListener);t.apply(this,r)};return onceListener}setMaxListeners(e){this.maxListeners=e;return this}getMaxListeners(){return this.maxListeners}eventNames(){return Array.from(this.events.keys())}emit(e,...t){const r=this._getListeners(e);r.forEach((e=>{e.apply(this,t)}));return r.length>0}addListener(e,t){this._emitInternalEvent("newListener",e,t);const r=this._getListeners(e).concat(t);this.events.set(e,r);if(this.maxListeners>0&&this.listenerCount(e)>this.maxListeners&&!this.hasWarnedAboutPotentialMemoryLeak){this.hasWarnedAboutPotentialMemoryLeak=true;const t=new o(this,e,this.listenerCount(e));console.warn(t)}return this}on(e,t){return this.addListener(e,t)}once(e,t){return this.addListener(e,this._wrapOnceListener(e,t))}prependListener(e,t){const r=this._getListeners(e);if(r.length>0){const n=[t].concat(r);this.events.set(e,n)}else{this.events.set(e,r.concat(t))}return this}prependOnceListener(e,t){return this.prependListener(e,this._wrapOnceListener(e,t))}removeListener(e,t){const r=this._getListeners(e);if(r.length>0){this._removeListener(r,t);this.events.set(e,r);this._emitInternalEvent("removeListener",e,t)}return this}off(e,t){return this.removeListener(e,t)}removeAllListeners(e){if(e){this.events.delete(e)}else{this.events.clear()}return this}listeners(e){return Array.from(this._getListeners(e))}listenerCount(e){return this._getListeners(e).length}rawListeners(e){return this.listeners(e)}};var u=a;u.defaultMaxListeners=10;0&&0},685:function(e){"use strict";e.exports=require("http")},687:function(e){"use strict";e.exports=require("https")},781:function(e){"use strict";e.exports=require("stream")}};var t={};function __nccwpck_require__(r){var n=t[r];if(n!==undefined){return n.exports}var s=t[r]={exports:{}};var i=true;try{e[r].call(s.exports,s,s.exports,__nccwpck_require__);i=false}finally{if(i)delete t[r]}return s.exports}if(typeof __nccwpck_require__!=="undefined")__nccwpck_require__.ab=__dirname+"/";var r={};!function(){"use strict";var e=r;Object.defineProperty(e,"__esModule",{value:true});var t=__nccwpck_require__(501);__nccwpck_require__(858);__nccwpck_require__(331);e.ClientRequestInterceptor=t.ClientRequestInterceptor}();module.exports=r})(); \ No newline at end of file diff --git a/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/package.json b/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/package.json new file mode 100644 index 0000000000..5f158b5a86 --- /dev/null +++ b/packages/next/src/compiled/@mswjs/interceptors/ClientRequest/package.json @@ -0,0 +1 @@ +{"name":"@mswjs/interceptors","main":"index.js","author":"Artem Zakharchenko","license":"MIT"} diff --git a/packages/next/src/experimental/testmode/playwright/README.md b/packages/next/src/experimental/testmode/playwright/README.md index 2d8ea11d76..1034babcd5 100644 --- a/packages/next/src/experimental/testmode/playwright/README.md +++ b/packages/next/src/experimental/testmode/playwright/README.md @@ -94,3 +94,26 @@ test('/product/shoe', async ({ page, msw }) => { await expect(page.locator('body')).toHaveText(/Boot/) }) ``` + +### Or use your favorite Fetch mocking library + +The "fetch loopback" mode can be configured in the `playwright.config.ts` or +via `test.use()` with a test module. This option loops `fetch()` calls via +the `fetch()` of the current test's worker. + +```javascript +import { test, expect } from 'next/experimental/testmode/playwright' +import { myFetchMocker } from 'my-fetch-mocker' + +test.use({ nextOptions: { fetchLoopback: true } }) + +test('/product/shoe', async ({ page, next }) => { + myFetchMocker.mock('http://my-db/product/shoe', { + title: 'A shoe', + }) + + await page.goto('/product/shoe') + + await expect(page.locator('body')).toHaveText(/Shoe/) +}) +``` diff --git a/packages/next/src/experimental/testmode/playwright/index.ts b/packages/next/src/experimental/testmode/playwright/index.ts index 0396243369..545e7bc303 100644 --- a/packages/next/src/experimental/testmode/playwright/index.ts +++ b/packages/next/src/experimental/testmode/playwright/index.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { test as base } from '@playwright/test' +import * as base from '@playwright/test' import type { NextFixture } from './next-fixture' +import type { NextOptions } from './next-options' import type { NextWorkerFixture } from './next-worker-fixture' import { applyNextWorkerFixture } from './next-worker-fixture' import { applyNextFixture } from './next-fixture' @@ -8,13 +9,28 @@ import { applyNextFixture } from './next-fixture' // eslint-disable-next-line import/no-extraneous-dependencies export * from '@playwright/test' -export type { NextFixture } +export type { NextFixture, NextOptions } export type { FetchHandlerResult } from '../proxy' -export const test = base.extend< - { next: NextFixture }, +export interface NextOptionsConfig { + nextOptions?: NextOptions +} + +export function defineConfig( + config: base.PlaywrightTestConfig +): base.PlaywrightTestConfig +export function defineConfig( + config: base.PlaywrightTestConfig +): base.PlaywrightTestConfig { + return base.defineConfig(config) +} + +export const test = base.test.extend< + { next: NextFixture; nextOptions: NextOptions }, { _nextWorker: NextWorkerFixture } >({ + nextOptions: [{ fetchLoopback: false }, { option: true }], + _nextWorker: [ // eslint-disable-next-line no-empty-pattern async ({}, use) => { @@ -23,14 +39,22 @@ export const test = base.extend< { scope: 'worker', auto: true }, ], - next: async ({ _nextWorker, page, extraHTTPHeaders }, use, testInfo) => { - await applyNextFixture(use, { - testInfo, - nextWorker: _nextWorker, - page, - extraHTTPHeaders, - }) - }, + next: [ + async ( + { nextOptions, _nextWorker, page, extraHTTPHeaders }, + use, + testInfo + ) => { + await applyNextFixture(use, { + testInfo, + nextWorker: _nextWorker, + page, + extraHTTPHeaders, + nextOptions, + }) + }, + { auto: true }, + ], }) export default test diff --git a/packages/next/src/experimental/testmode/playwright/msw.ts b/packages/next/src/experimental/testmode/playwright/msw.ts index fae63b6fff..55d1022793 100644 --- a/packages/next/src/experimental/testmode/playwright/msw.ts +++ b/packages/next/src/experimental/testmode/playwright/msw.ts @@ -1,4 +1,4 @@ -import { test as base } from './index' +import { test as base, defineConfig } from './index' import type { NextFixture } from './next-fixture' import { type RequestHandler, @@ -15,6 +15,7 @@ export * from 'msw' // eslint-disable-next-line import/no-extraneous-dependencies export * from '@playwright/test' export type { NextFixture } +export { defineConfig } export interface MswFixture { use: (...handlers: RequestHandler[]) => void @@ -24,7 +25,7 @@ export const test = base.extend<{ msw: MswFixture mswHandlers: RequestHandler[] }>({ - mswHandlers: [], + mswHandlers: [[], { option: true }], msw: [ async ({ next, mswHandlers }, use) => { diff --git a/packages/next/src/experimental/testmode/playwright/next-fixture.ts b/packages/next/src/experimental/testmode/playwright/next-fixture.ts index 5648be551d..a7d2b2b6a8 100644 --- a/packages/next/src/experimental/testmode/playwright/next-fixture.ts +++ b/packages/next/src/experimental/testmode/playwright/next-fixture.ts @@ -1,5 +1,7 @@ import type { Page, TestInfo } from '@playwright/test' import type { NextWorkerFixture, FetchHandler } from './next-worker-fixture' +import type { NextOptions } from './next-options' +import type { FetchHandlerResult } from '../proxy' import { handleRoute } from './page-route' export interface NextFixture { @@ -11,12 +13,13 @@ class NextFixtureImpl implements NextFixture { constructor( public testId: string, + private options: NextOptions, private worker: NextWorkerFixture, private page: Page ) { - this.page.route('**', (route) => - handleRoute(route, page, this.fetchHandler) - ) + const handleFetch = this.handleFetch.bind(this) + worker.onFetch(testId, handleFetch) + this.page.route('**', (route) => handleRoute(route, page, handleFetch)) } teardown(): void { @@ -25,7 +28,20 @@ class NextFixtureImpl implements NextFixture { onFetch(handler: FetchHandler): void { this.fetchHandler = handler - this.worker.onFetch(this.testId, handler) + } + + private async handleFetch(request: Request): Promise { + const handler = this.fetchHandler + if (handler) { + const result = handler(request) + if (result) { + return result + } + } + if (this.options.fetchLoopback) { + return fetch(request) + } + return undefined } } @@ -33,17 +49,24 @@ export async function applyNextFixture( use: (fixture: NextFixture) => Promise, { testInfo, + nextOptions, nextWorker, page, extraHTTPHeaders, }: { testInfo: TestInfo + nextOptions: NextOptions nextWorker: NextWorkerFixture page: Page extraHTTPHeaders: Record | undefined } ): Promise { - const fixture = new NextFixtureImpl(testInfo.testId, nextWorker, page) + const fixture = new NextFixtureImpl( + testInfo.testId, + nextOptions, + nextWorker, + page + ) page.setExtraHTTPHeaders({ ...extraHTTPHeaders, 'Next-Test-Proxy-Port': String(nextWorker.proxyPort), diff --git a/packages/next/src/experimental/testmode/playwright/next-options.ts b/packages/next/src/experimental/testmode/playwright/next-options.ts new file mode 100644 index 0000000000..cf95f2593c --- /dev/null +++ b/packages/next/src/experimental/testmode/playwright/next-options.ts @@ -0,0 +1,3 @@ +export interface NextOptions { + fetchLoopback?: boolean +} diff --git a/packages/next/src/experimental/testmode/server.ts b/packages/next/src/experimental/testmode/server.ts index 7ea86ac336..bb001b0d4a 100644 --- a/packages/next/src/experimental/testmode/server.ts +++ b/packages/next/src/experimental/testmode/server.ts @@ -5,6 +5,7 @@ import type { ProxyFetchResponse, ProxyResponse, } from './proxy' +import { ClientRequestInterceptor } from 'next/dist/compiled/@mswjs/interceptors/ClientRequest' interface TestReqInfo { url: string @@ -64,9 +65,43 @@ function buildResponse(proxyResponse: ProxyFetchResponse): Response { }) } +async function handleFetch( + originalFetch: Fetch, + request: Request +): Promise { + const testInfo = testStorage.getStore() + if (!testInfo) { + throw new Error('No test info') + } + + const { testData, proxyPort } = testInfo + const proxyRequest = await buildProxyRequest(testData, request) + + const resp = await originalFetch(`http://localhost:${proxyPort}`, { + method: 'POST', + body: JSON.stringify(proxyRequest), + }) + if (!resp.ok) { + throw new Error(`Proxy request failed: ${resp.status}`) + } + + const proxyResponse = (await resp.json()) as ProxyResponse + const { api } = proxyResponse + switch (api) { + case 'continue': + return originalFetch(request) + case 'abort': + case 'unhandled': + throw new Error('Proxy request aborted') + default: + break + } + return buildResponse(proxyResponse) +} + function interceptFetch() { const originalFetch = global.fetch - global.fetch = async function testFetch( + global.fetch = function testFetch( input: FetchInputArg, init?: FetchInitArg ): Promise { @@ -75,36 +110,7 @@ function interceptFetch() { if (init?.next?.internal) { return originalFetch(input, init) } - - const testInfo = testStorage.getStore() - if (!testInfo) { - throw new Error('No test info') - } - - const { testData, proxyPort } = testInfo - const originalRequest = new Request(input, init) - const proxyRequest = await buildProxyRequest(testData, originalRequest) - - const resp = await originalFetch(`http://localhost:${proxyPort}`, { - method: 'POST', - body: JSON.stringify(proxyRequest), - }) - if (!resp.ok) { - throw new Error(`Proxy request failed: ${resp.status}`) - } - - const proxyResponse = (await resp.json()) as ProxyResponse - const { api } = proxyResponse - switch (api) { - case 'continue': - return originalFetch(originalRequest) - case 'abort': - case 'unhandled': - throw new Error('Proxy request aborted') - default: - break - } - return buildResponse(proxyResponse) + return handleFetch(originalFetch, new Request(input, init)) } } @@ -112,8 +118,16 @@ export function interceptTestApis(): () => void { const originalFetch = global.fetch interceptFetch() + const clientRequestInterceptor = new ClientRequestInterceptor() + clientRequestInterceptor.on('request', async ({ request }) => { + const response = await handleFetch(originalFetch, request) + request.respondWith(response) + }) + clientRequestInterceptor.apply() + // Cleanup. return () => { + clientRequestInterceptor.dispose() global.fetch = originalFetch } } diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index 634edb6eb7..495da5ef09 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -91,6 +91,22 @@ export async function ncc_node_html_parser(task, opts) { .target('src/compiled/node-html-parser') } +// eslint-disable-next-line camelcase +externals['@mswjs/interceptors/ClientRequest'] = + 'next/dist/compiled/@mswjs/interceptors/ClientRequest' +export async function ncc_mswjs_interceptors(task, opts) { + await task + .source( + relative(__dirname, require.resolve('@mswjs/interceptors/ClientRequest')) + ) + .ncc({ + packageName: '@mswjs/interceptors/ClientRequest', + externals, + target: 'es5', + }) + .target('src/compiled/@mswjs/interceptors/ClientRequest') +} + export async function capsize_metrics() { const { entireMetricsCollection, @@ -2345,6 +2361,7 @@ export async function ncc(task, opts) { 'ncc_edge_runtime_primitives', 'ncc_edge_runtime_ponyfill', 'ncc_edge_runtime', + 'ncc_mswjs_interceptors', ], opts ) @@ -2684,6 +2701,7 @@ export async function minimal_next_server(task) { 'next/dist/compiled/node-html-parser', 'next/dist/compiled/compression', 'next/dist/compiled/jsonwebtoken', + 'next/dist/compiled/@mswjs/interceptors/ClientRequest', ].reduce((acc, pkg) => { acc[pkg] = pkg return acc diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index 2b68db6002..e0f9735b2b 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -41,6 +41,10 @@ declare module 'next/dist/compiled/node-html-parser' { export * from 'node-html-parser' } +declare module 'next/dist/compiled/@mswjs/interceptors/ClientRequest' { + export * from '@mswjs/interceptors/ClientRequest' +} + declare module 'next/dist/compiled/undici' {} declare module 'next/dist/compiled/jest-worker' { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85c2dea50f..ab7c83008d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -814,6 +814,9 @@ importers: '@jest/types': specifier: 29.5.0 version: 29.5.0 + '@mswjs/interceptors': + specifier: 0.23.0 + version: 0.23.0 '@napi-rs/cli': specifier: 2.16.2 version: 2.16.2 @@ -6682,6 +6685,18 @@ packages: - supports-color dev: true + /@mswjs/interceptors@0.23.0: + resolution: {integrity: sha512-JytvDa7pBbxXvCTXBYQs+0eE6MqxpqH/H4peRNY6zVAlvJ6d/hAWLHAef1D9lWN4zuIigN0VkakGOAUrX7FWLg==} + engines: {node: '>=18'} + dependencies: + '@open-draft/deferred-promise': 2.1.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + headers-polyfill: 3.1.2 + outvariant: 1.4.0 + strict-event-emitter: 0.5.0 + dev: true + /@napi-rs/cli@2.16.2: resolution: {integrity: sha512-U2aZfnr0s9KkXpZlYC0l5WxWCXL7vJUNpCnWMwq3T9GG9rhYAAUM9CTZsi1Z+0iR2LcHbfq9EfMgoqnuTyUjfg==} engines: {node: '>= 10'} @@ -7162,10 +7177,25 @@ packages: aggregate-error: 3.1.0 dev: true + /@open-draft/deferred-promise@2.1.0: + resolution: {integrity: sha512-Rzd5JrXZX8zErHzgcGyngh4fmEbSHqTETdGj9rXtejlqMIgXFlyKBA7Jn1Xp0Ls0M0Y22+xHcWiEzbmdWl0BOA==} + dev: true + + /@open-draft/logger@0.3.0: + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.0 + dev: true + /@open-draft/until@1.0.3: resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==} dev: true + /@open-draft/until@2.1.0: + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + dev: true + /@opentelemetry/api@1.4.1: resolution: {integrity: sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==} engines: {node: '>=8.0.0'}