diff --git a/README.md b/README.md index 4a9e431..1dde111 100644 --- a/README.md +++ b/README.md @@ -300,8 +300,11 @@ None! Support ------- -Pace supports Chrome and Edge 96+, Firefox 95+, and Safari 15.2+ on macOS and iOS, plus browsers built -on the same engines, such as Opera and Samsung Internet. For older browsers, use pace-js 1.x. +Pace supports Chrome and Edge 96+, Firefox 95+, Safari 15.2+ on macOS and iOS, Opera 82+, Samsung +Internet 17+, and other browsers built on Chromium 96 or later. For older browsers, use pace-js 1.x. + +In tests that run on jsdom, turn on its `pretendToBeVisual` option (Jest and Vitest already do): without +it, jsdom has no `requestAnimationFrame`. If you run into a compatibility issue, please create an issue. diff --git a/pace.js b/pace.js index cf6c0d3..44bfac2 100644 --- a/pace.js +++ b/pace.js @@ -328,7 +328,10 @@ if (!ignoreStack.length && options.ajax) { if (method === 'socket' && options.ajax.trackWebSockets) { return true; - } else if (options.ajax.trackMethods.includes(method.toUpperCase())) { + } + // Array.prototype.includes, so a string or number from a misconfiguration + // matches nothing instead of matching substrings or throwing. + if (Array.prototype.includes.call(options.ajax.trackMethods, method.toUpperCase())) { return true; } } diff --git a/pace.min.js b/pace.min.js index b8b27d7..b204306 100644 --- a/pace.min.js +++ b/pace.min.js @@ -2,4 +2,4 @@ * pace.js v1.3.1 * https://github.com/CodeByZach/pace/ * Licensed MIT © HubSpot, Inc. - */(function(){const x={className:"",catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},l=()=>performance.now(),H=window.requestAnimationFrame,W=window.cancelAnimationFrame,X=s=>{let e=l();const t=()=>{const r=l()-e;return r>=33?(e=l(),s(r,()=>H(t))):setTimeout(t,33-r)};return t()},v=(s,e,...t)=>typeof s[e]=="function"?s[e].apply(s,t):s[e],y=(s,...e)=>{for(const t of e)if(t)for(const r in t){if(!Object.prototype.hasOwnProperty.call(t,r)||r==="__proto__"||r==="constructor"||r==="prototype")continue;const n=t[r];s[r]!=null&&typeof s[r]=="object"&&n!=null&&typeof n=="object"?y(s[r],n):s[r]=n}return s},$=s=>{let e=0,t=0;for(const r of s)e+=Math.abs(r),t++;return e/t},B=(s="options",e=!0)=>{const t=document.querySelector(`script[data-pace-${s}]`);if(!t)return;const r=t.getAttribute(`data-pace-${s}`);if(!e)return r;try{return JSON.parse(r)}catch(n){console.error("Error parsing inline pace options",n)}},o=window.Pace||{};window.Pace=o;const L=(s,e,t)=>{const r=s.bindings?.[e];r&&(s.bindings[e]=r.filter(n=>t(n)?(n.removed=!0,!1):!0))},D=s=>{typeof reportError=="function"?reportError(s):queueMicrotask(()=>{throw s})};Object.assign(o,{on(s,e,t,r){r??=!1,this.bindings??=Object.create(null),this.bindings[s]??=[],this.bindings[s].push({handler:e,ctx:t,once:r})},once(s,e,t){this.on(s,e,t,!0)},off(s,e){L(this,s,t=>e==null||t.handler===e)},trigger(s,...e){const t=this.bindings?.[s];if(t){for(const r of t.slice())if(!r.removed){r.once&&L(this,s,n=>n===r);try{r.handler.apply(r.ctx??this,e)}catch(n){D(n)}}}}});const i=o.options=y({},x,window.paceOptions,B());for(const s of["ajax","document","eventLag","elements"])i[s]===!0&&(i[s]=x[s]);class w extends Error{}class G{constructor(){this.progress=0}getElement(){if(this.el==null){const e=document.querySelector(i.target);if(!e)throw new w;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/(pace-done )|/,"pace-running ");const t=document.createElement("div");t.className="pace-progress"+(i.className!==""?" "+i.className:"");const r=document.createElement("div");r.className="pace-progress-inner",t.appendChild(r);const n=document.createElement("div");n.className="pace-activity",this.el.appendChild(t),this.el.appendChild(n),e.firstChild!=null?e.insertBefore(this.el,e.firstChild):e.appendChild(this.el)}return this.el}finish(){const e=this.getElement();e.className=e.className.replace("pace-active","pace-inactive"),document.body.className=document.body.className.replace("pace-running ","pace-done ")}update(e){this.progress=e,o.trigger("progress",e),this.render()}destroy(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(e){w=e}this.el=void 0}render(){if(document.querySelector(i.target)==null)return;const e=this.getElement();if(e.children[0].style.transform=`translate3d(${this.progress}%, 0, 0)`,!this.lastRenderedProgress||this.lastRenderedProgress|this.progress!==0|0){e.children[0].setAttribute("data-progress-text",`${this.progress|0}%`);let t;this.progress>=100?t="99":(t=this.progress<10?"0":"",t+=this.progress|0),e.children[0].setAttribute("data-progress",t)}o.trigger("change",this.progress),this.lastRenderedProgress=this.progress}done(){return this.progress>=100}}class J{constructor(){this.bindings={}}trigger(e,t){if(this.bindings[e]!=null)for(const r of this.bindings[e].slice())r.call(this,t)}on(e,t){this.bindings[e]??=[],this.bindings[e].push(t)}}const q=window.XMLHttpRequest,b=window.WebSocket,R=(s,e)=>{for(const t in e.prototype)try{s[t]==null&&typeof e[t]!="function"&&Object.defineProperty(s,t,{get:()=>e.prototype[t],configurable:!0,enumerable:!0})}catch{}},p=[];o.ignore=(s,...e)=>{p.unshift("ignore");const t=s.apply(null,e);return p.shift(),t},o.track=(s,...e)=>{p.unshift("track");const t=s.apply(null,e);return p.shift(),t};const k=s=>{if(s??="GET",p[0]==="track")return"force";if(!p.length&&i.ajax){if(s==="socket"&&i.ajax.trackWebSockets)return!0;if(i.ajax.trackMethods.includes(s.toUpperCase()))return!0}return!1},K=s=>{const e=t=>{const r=t.open;t.open=(...n)=>{const[c,a]=n;return k(c)&&s.trigger("request",{type:c,url:a,request:t}),r.apply(t,n)}};window.XMLHttpRequest=function(t){const r=new q(t);return e(r),r};try{R(window.XMLHttpRequest,q)}catch{}if(i.ajax.trackWebSockets){window.WebSocket=function(t,r){let n;return r!=null?n=new b(t,r):n=new b(t),k("socket")&&s.trigger("request",{type:"socket",url:t,protocols:r,request:n}),n};try{R(window.WebSocket,b)}catch{}}};class Y extends J{constructor(){super(),K(this)}}let P=null;const M=()=>(P??=new Y,P),O=s=>{const e=i.ajax.ignoreURLs;for(let t=0,r=e.length;t{const{type:e,request:t,url:r}=s;if(!O(r)&&!o.running&&(i.restartOnRequestAfter!==!1||k(e)==="force")){let n=i.restartOnRequestAfter||0;typeof n=="boolean"&&(n=0),setTimeout(()=>{let c;if(e==="socket"?c=t.readyState<1:c=0this.watch(e))}watch({type:e,request:t,url:r}){if(O(r))return;let n;e==="socket"?n=new Q(t,this.complete):n=new z(t,this.complete),this.elements.push(n)}complete(e){this.elements=this.elements.filter(t=>t!==e)}}class z{constructor(e,t){this.progress=0,e.addEventListener("progress",r=>{r.lengthComputable?this.progress=100*r.loaded/r.total:this.progress=this.progress+(100-this.progress)/2},!1);for(const r of["load","abort","timeout","error"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class Q{constructor(e,t){this.progress=0;for(const r of["error","open"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class V{constructor(e){e??={},this.complete=this.complete.bind(this),this.elements=[],e.selectors??=[];const t=e.selectors;for(let r=0,n=t.length;rt!==e)}}class Z{constructor(e,t){this.selector=e,this.completeCallback=t,this.progress=0,this.check()}check(){document.querySelector(this.selector)?this.done():setTimeout(()=>this.check(),i.elements.checkInterval)}done(){this.completeCallback(this),this.completeCallback=null,this.progress=100}}class T{constructor(){this.progress=this.states[document.readyState]??100;const e=document.onreadystatechange;document.onreadystatechange=(...t)=>(this.states[document.readyState]!=null&&(this.progress=this.states[document.readyState]),typeof e=="function"?e.apply(null,t):void 0)}}T.prototype.states={loading:0,interactive:50,complete:100};class ee{constructor(e){this.progress=0;let t=0;const r=[];let n=0,c=l();const a=setInterval(()=>{const h=l()-c-50;c=l(),r.push(h),r.length>e.sampleCount&&r.shift(),t=$(r),++n>=e.minSamples&&t=100&&(this.done=!0),t===this.last?this.sinceLastUpdate+=e:(this.sinceLastUpdate&&(this.rate=(t-this.last)/this.sinceLastUpdate),this.catchup=(t-this.progress)/i.catchupTime,this.sinceLastUpdate=0,this.last=t),t>this.progress&&(this.progress+=this.catchup*e);const r=1-Math.pow(this.progress/100,i.easeFactor);return this.progress+=r*this.rate*e,this.progress=Math.min(this.lastProgress+i.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress}}let d=null,A=null,u=null,C=null,g=null,S=null;o.running=!1;const _=()=>{i.restartOnPushState&&o.restart()},te=window.history.pushState;window.history.pushState=(...s)=>(_(),te.apply(window.history,s));const se=window.history.replaceState;window.history.replaceState=(...s)=>(_(),se.apply(window.history,s));const re={ajax:N,elements:V,document:T,eventLag:ee},U=()=>{o.sources=d=[];for(const e of["ajax","elements","document","eventLag"])i[e]!==!1&&d.push(new re[e](i[e]));const s=i.extraSources??[];for(let e=0,t=s.length;e{o.trigger("stop"),o.running=!1,u.destroy(),S=!0,g!=null&&(W(g),g=null),U()},o.restart=()=>{o.trigger("restart"),o.stop(),o.start()},o.go=()=>{o.running=!0,u.render();const s=l();S=!1,g=X((e,t)=>{let r=0,n=0,c=!0;for(let h=0,f=d.length;h{u.finish(),o.running=!1,o.trigger("hide")},Math.max(i.ghostTime,Math.max(i.minTime-(l()-s),0)))):t()})},o.start=s=>{y(i,s),o.running=!0;try{u.render()}catch(e){w=e}document.querySelector(".pace")?(o.trigger("start"),o.go()):setTimeout(o.start,50)},typeof define=="function"&&define.amd?define(()=>o):typeof exports=="object"?module.exports=o:i.startOnPageLoad&&o.start()})(); + */(function(){const x={className:"",catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},l=()=>performance.now(),H=window.requestAnimationFrame,W=window.cancelAnimationFrame,X=s=>{let e=l();const t=()=>{const r=l()-e;return r>=33?(e=l(),s(r,()=>H(t))):setTimeout(t,33-r)};return t()},v=(s,e,...t)=>typeof s[e]=="function"?s[e].apply(s,t):s[e],y=(s,...e)=>{for(const t of e)if(t)for(const r in t){if(!Object.prototype.hasOwnProperty.call(t,r)||r==="__proto__"||r==="constructor"||r==="prototype")continue;const n=t[r];s[r]!=null&&typeof s[r]=="object"&&n!=null&&typeof n=="object"?y(s[r],n):s[r]=n}return s},$=s=>{let e=0,t=0;for(const r of s)e+=Math.abs(r),t++;return e/t},B=(s="options",e=!0)=>{const t=document.querySelector(`script[data-pace-${s}]`);if(!t)return;const r=t.getAttribute(`data-pace-${s}`);if(!e)return r;try{return JSON.parse(r)}catch(n){console.error("Error parsing inline pace options",n)}},o=window.Pace||{};window.Pace=o;const L=(s,e,t)=>{const r=s.bindings?.[e];r&&(s.bindings[e]=r.filter(n=>t(n)?(n.removed=!0,!1):!0))},D=s=>{typeof reportError=="function"?reportError(s):queueMicrotask(()=>{throw s})};Object.assign(o,{on(s,e,t,r){r??=!1,this.bindings??=Object.create(null),this.bindings[s]??=[],this.bindings[s].push({handler:e,ctx:t,once:r})},once(s,e,t){this.on(s,e,t,!0)},off(s,e){L(this,s,t=>e==null||t.handler===e)},trigger(s,...e){const t=this.bindings?.[s];if(t){for(const r of t.slice())if(!r.removed){r.once&&L(this,s,n=>n===r);try{r.handler.apply(r.ctx??this,e)}catch(n){D(n)}}}}});const i=o.options=y({},x,window.paceOptions,B());for(const s of["ajax","document","eventLag","elements"])i[s]===!0&&(i[s]=x[s]);class w extends Error{}class G{constructor(){this.progress=0}getElement(){if(this.el==null){const e=document.querySelector(i.target);if(!e)throw new w;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/(pace-done )|/,"pace-running ");const t=document.createElement("div");t.className="pace-progress"+(i.className!==""?" "+i.className:"");const r=document.createElement("div");r.className="pace-progress-inner",t.appendChild(r);const n=document.createElement("div");n.className="pace-activity",this.el.appendChild(t),this.el.appendChild(n),e.firstChild!=null?e.insertBefore(this.el,e.firstChild):e.appendChild(this.el)}return this.el}finish(){const e=this.getElement();e.className=e.className.replace("pace-active","pace-inactive"),document.body.className=document.body.className.replace("pace-running ","pace-done ")}update(e){this.progress=e,o.trigger("progress",e),this.render()}destroy(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(e){w=e}this.el=void 0}render(){if(document.querySelector(i.target)==null)return;const e=this.getElement();if(e.children[0].style.transform=`translate3d(${this.progress}%, 0, 0)`,!this.lastRenderedProgress||this.lastRenderedProgress|this.progress!==0|0){e.children[0].setAttribute("data-progress-text",`${this.progress|0}%`);let t;this.progress>=100?t="99":(t=this.progress<10?"0":"",t+=this.progress|0),e.children[0].setAttribute("data-progress",t)}o.trigger("change",this.progress),this.lastRenderedProgress=this.progress}done(){return this.progress>=100}}class J{constructor(){this.bindings={}}trigger(e,t){if(this.bindings[e]!=null)for(const r of this.bindings[e].slice())r.call(this,t)}on(e,t){this.bindings[e]??=[],this.bindings[e].push(t)}}const q=window.XMLHttpRequest,b=window.WebSocket,R=(s,e)=>{for(const t in e.prototype)try{s[t]==null&&typeof e[t]!="function"&&Object.defineProperty(s,t,{get:()=>e.prototype[t],configurable:!0,enumerable:!0})}catch{}},p=[];o.ignore=(s,...e)=>{p.unshift("ignore");const t=s.apply(null,e);return p.shift(),t},o.track=(s,...e)=>{p.unshift("track");const t=s.apply(null,e);return p.shift(),t};const k=s=>(s??="GET",p[0]==="track"?"force":!!(!p.length&&i.ajax&&(s==="socket"&&i.ajax.trackWebSockets||Array.prototype.includes.call(i.ajax.trackMethods,s.toUpperCase())))),K=s=>{const e=t=>{const r=t.open;t.open=(...n)=>{const[c,a]=n;return k(c)&&s.trigger("request",{type:c,url:a,request:t}),r.apply(t,n)}};window.XMLHttpRequest=function(t){const r=new q(t);return e(r),r};try{R(window.XMLHttpRequest,q)}catch{}if(i.ajax.trackWebSockets){window.WebSocket=function(t,r){let n;return r!=null?n=new b(t,r):n=new b(t),k("socket")&&s.trigger("request",{type:"socket",url:t,protocols:r,request:n}),n};try{R(window.WebSocket,b)}catch{}}};class Y extends J{constructor(){super(),K(this)}}let P=null;const M=()=>(P??=new Y,P),O=s=>{const e=i.ajax.ignoreURLs;for(let t=0,r=e.length;t{const{type:e,request:t,url:r}=s;if(!O(r)&&!o.running&&(i.restartOnRequestAfter!==!1||k(e)==="force")){let n=i.restartOnRequestAfter||0;typeof n=="boolean"&&(n=0),setTimeout(()=>{let c;if(e==="socket"?c=t.readyState<1:c=0this.watch(e))}watch({type:e,request:t,url:r}){if(O(r))return;let n;e==="socket"?n=new Q(t,this.complete):n=new z(t,this.complete),this.elements.push(n)}complete(e){this.elements=this.elements.filter(t=>t!==e)}}class z{constructor(e,t){this.progress=0,e.addEventListener("progress",r=>{r.lengthComputable?this.progress=100*r.loaded/r.total:this.progress=this.progress+(100-this.progress)/2},!1);for(const r of["load","abort","timeout","error"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class Q{constructor(e,t){this.progress=0;for(const r of["error","open"])e.addEventListener(r,()=>{t(this),this.progress=100},!1)}}class V{constructor(e){e??={},this.complete=this.complete.bind(this),this.elements=[],e.selectors??=[];const t=e.selectors;for(let r=0,n=t.length;rt!==e)}}class Z{constructor(e,t){this.selector=e,this.completeCallback=t,this.progress=0,this.check()}check(){document.querySelector(this.selector)?this.done():setTimeout(()=>this.check(),i.elements.checkInterval)}done(){this.completeCallback(this),this.completeCallback=null,this.progress=100}}class T{constructor(){this.progress=this.states[document.readyState]??100;const e=document.onreadystatechange;document.onreadystatechange=(...t)=>(this.states[document.readyState]!=null&&(this.progress=this.states[document.readyState]),typeof e=="function"?e.apply(null,t):void 0)}}T.prototype.states={loading:0,interactive:50,complete:100};class ee{constructor(e){this.progress=0;let t=0;const r=[];let n=0,c=l();const a=setInterval(()=>{const h=l()-c-50;c=l(),r.push(h),r.length>e.sampleCount&&r.shift(),t=$(r),++n>=e.minSamples&&t=100&&(this.done=!0),t===this.last?this.sinceLastUpdate+=e:(this.sinceLastUpdate&&(this.rate=(t-this.last)/this.sinceLastUpdate),this.catchup=(t-this.progress)/i.catchupTime,this.sinceLastUpdate=0,this.last=t),t>this.progress&&(this.progress+=this.catchup*e);const r=1-Math.pow(this.progress/100,i.easeFactor);return this.progress+=r*this.rate*e,this.progress=Math.min(this.lastProgress+i.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress}}let d=null,j=null,u=null,C=null,g=null,S=null;o.running=!1;const _=()=>{i.restartOnPushState&&o.restart()},te=window.history.pushState;window.history.pushState=(...s)=>(_(),te.apply(window.history,s));const se=window.history.replaceState;window.history.replaceState=(...s)=>(_(),se.apply(window.history,s));const re={ajax:N,elements:V,document:T,eventLag:ee},U=()=>{o.sources=d=[];for(const e of["ajax","elements","document","eventLag"])i[e]!==!1&&d.push(new re[e](i[e]));const s=i.extraSources??[];for(let e=0,t=s.length;e{o.trigger("stop"),o.running=!1,u.destroy(),S=!0,g!=null&&(W(g),g=null),U()},o.restart=()=>{o.trigger("restart"),o.stop(),o.start()},o.go=()=>{o.running=!0,u.render();const s=l();S=!1,g=X((e,t)=>{let r=0,n=0,c=!0;for(let h=0,f=d.length;h{u.finish(),o.running=!1,o.trigger("hide")},Math.max(i.ghostTime,Math.max(i.minTime-(l()-s),0)))):t()})},o.start=s=>{y(i,s),o.running=!0;try{u.render()}catch(e){w=e}document.querySelector(".pace")?(o.trigger("start"),o.go()):setTimeout(o.start,50)},typeof define=="function"&&define.amd?define(()=>o):typeof exports=="object"?module.exports=o:i.startOnPageLoad&&o.start()})(); diff --git a/scripts/build.mjs b/scripts/build.mjs index c157fc1..4b159aa 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -19,8 +19,9 @@ const root = path.resolve(import.meta.dirname, '..'); // The oldest browser versions in .browserslistrc ("baseline 2021 with // downstream"). esbuild doesn't read Browserslist, so they're listed here: -// https://esbuild.github.io/api/#target. `npm test` checks the minified build -// against .browserslistrc, so the two can't drift apart unnoticed. +// https://esbuild.github.io/api/#target. `npm test` runs es-check with +// .browserslistrc on pace.js and pace.min.js, so syntax and many built-ins +// newer than these browsers fail the test. const TARGET = ['chrome96', 'edge96', 'firefox95', 'safari15.2', 'ios15.2']; // The 10 theme colors, from the original Gruntfile.coffee `themeColors`. diff --git a/tests/ajax.spec.mjs b/tests/ajax.spec.mjs index 11c19f1..2bd9320 100644 --- a/tests/ajax.spec.mjs +++ b/tests/ajax.spec.mjs @@ -58,3 +58,20 @@ test.describe('with ajax options', () => { expect(await page.evaluate(() => events)).toEqual([]); }); }); + +test.describe('with ajax.trackMethods that is not an array', () => { + test('a string matches nothing, not substrings', async ({ page, open }) => { + await open({ head: "" }); + await idle(page); + await clearEvents(page); + await request(page, { ms: 900 }); + expect(await page.evaluate(() => events)).toEqual([]); + }); + + test('a number does not break the page\'s requests', async ({ page, open }) => { + await open({ head: '' }); + await idle(page); + // Resolves only if the page's xhr.open() didn't throw. + await request(page, { ms: 100 }); + }); +});