@bloom-perf/yaml-pptr 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -177,6 +177,28 @@ try {
177
177
  }
178
178
  ```
179
179
 
180
+ ## Editor Support
181
+
182
+ The package ships a JSON Schema of the YAML configuration (`dist/schema.json`), generated from the very schema used to validate it at runtime. With the [YAML extension for VS Code](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) (or any editor using `yaml-language-server`), you get completion, documentation on hover, and errors (unknown key, wrong type...) while typing.
183
+
184
+ Add this first line to a scenario file:
185
+
186
+ ```yaml
187
+ # yaml-language-server: $schema=https://unpkg.com/@bloom-perf/yaml-pptr@3/dist/schema.json
188
+ scenarios:
189
+ - name: Login
190
+ ```
191
+
192
+ With the package installed, a local path works too (relative to the YAML file), e.g. `$schema=../node_modules/@bloom-perf/yaml-pptr/dist/schema.json`. To apply it to several files, map them in `.vscode/settings.json`:
193
+
194
+ ```json
195
+ {
196
+ "yaml.schemas": {
197
+ "https://unpkg.com/@bloom-perf/yaml-pptr@3/dist/schema.json": ["scenarios/*.yaml"]
198
+ }
199
+ }
200
+ ```
201
+
180
202
  ## YAML Reference
181
203
 
182
204
  ### Scenario Configuration
@@ -195,6 +217,28 @@ try {
195
217
 
196
218
  All the scenarios of a file start at the same time, whatever their browser: each one is a population of users with its own `workers`, `iterations` and start strategy. Unknown keys are rejected (see [Error handling](#error-handling)).
197
219
 
220
+ ### Selectors
221
+
222
+ Every selector (`click`, `input`, `CLICK`, `TYPE`, `WAIT_FOR_SELECTOR`) accepts a CSS selector or any [Puppeteer selector](https://pptr.dev/guides/page-interactions), so XPath, text and ARIA queries need no special syntax:
223
+
224
+ | Kind | Example |
225
+ | ---- | ------- |
226
+ | CSS | `#login-button`, `form .submit` |
227
+ | XPath | `::-p-xpath(//button[@type='submit'])` or `xpath///button[@type='submit']` |
228
+ | Text | `::-p-text(Sign in)` |
229
+ | ARIA | `::-p-aria(Sign in)`, `::-p-aria([name="Sign in"][role="button"])` |
230
+ | Shadow DOM | `my-app >>> button` (any depth), `my-app >>>> button` (direct shadow root) |
231
+
232
+ ```yaml
233
+ steps:
234
+ - input:
235
+ selector: "::-p-xpath(//input[@name='email'])"
236
+ text: "${APP_USER}"
237
+ - click: "::-p-text(Sign in)"
238
+ ```
239
+
240
+ `${VAR}` placeholders are resolved in selectors too.
241
+
198
242
  ### Steps (Simplified Syntax)
199
243
 
200
244
  #### click
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import*as B from"js-yaml";import*as F from"zod";import*as e from"zod";var b=e.number().int().nonnegative(),v=e.number().int().positive(),C=e.enum(["load","domcontentloaded","networkidle0","networkidle2"]),G=e.enum(["left","right","middle"]),N=e.enum(["chrome","firefox"]),M=e.strictObject({timeout:b.optional(),waitUntil:e.union([C,C.array()]).optional(),referer:e.string().optional()}).optional(),Z=e.strictObject({button:G.optional(),clickCount:v.optional(),delay:b.optional()}).optional(),D=e.strictObject({delay:b.optional()}).optional(),H=e.strictObject({visible:e.boolean().optional(),hidden:e.boolean().optional(),timeout:b.optional()}).optional(),K=e.strictObject({type:e.enum(["png","jpeg"]).optional(),quality:e.number().int().min(0).max(100).optional(),fullPage:e.boolean().optional(),clip:e.strictObject({x:e.number(),y:e.number(),width:e.number().positive(),height:e.number().positive()}).optional(),omitBackground:e.boolean().optional(),encoding:e.enum(["base64","binary"]).optional()}).optional(),q=e.strictObject({type:e.literal("GOTO"),url:e.string(),options:M}),Q=e.strictObject({type:e.literal("CLICK"),selector:e.string(),options:Z}),J=e.strictObject({type:e.literal("TYPE"),selector:e.string(),text:e.string(),options:D}),X=e.strictObject({type:e.literal("WAIT_FOR_SELECTOR"),selector:e.string(),options:H}),Y=e.strictObject({type:e.literal("WAIT_FOR_TIMEOUT"),timeout:b}),tt=e.strictObject({type:e.literal("WAIT"),milliseconds:b}),et=e.strictObject({type:e.literal("PAUSE")}),ot=e.strictObject({type:e.literal("SCREENSHOT"),path:e.string(),options:K}),rt=e.strictObject({type:e.literal("EVALUATE"),script:e.string()}),nt=e.strictObject({type:e.literal("SET_VIEWPORT"),width:v,height:v}),it=e.strictObject({type:e.literal("CLOSE")}),R=e.discriminatedUnion("type",[q,Q,J,X,Y,tt,et,ot,rt,nt,it]),st=e.strictObject({navigate:e.string()}),at=e.literal("waitForever"),ct=e.strictObject({wait:e.number().nonnegative()}),pt=e.strictObject({click:e.string()}),lt=e.strictObject({input:e.strictObject({selector:e.string(),text:e.string()})}),ut=e.union([st,at,ct,pt,lt,R]),gt=e.strictObject({name:e.string().optional(),browser:N.optional(),run:e.enum(["PARALLEL","SEQUENTIAL"]).optional(),initialDelaySeconds:e.number().nonnegative().optional(),workers:v.optional(),iterations:v.optional(),location:e.string().optional(),steps:e.array(ut).optional(),actions:e.array(R).optional()}),P=e.strictObject({scenarios:e.array(gt)});var L=r=>r instanceof Error?r.message:String(r),S=class extends Error{constructor(t,o){super(t,o),this.name="ConfigurationError"}},$=class extends Error{scenarioName;workerIndex;iteration;constructor(t,o,n,i){super(`Scenario "${t}" (worker ${o}, iteration ${n}) failed: ${L(i)}`,{cause:i}),this.name="WorkerError",this.scenarioName=t,this.workerIndex=o,this.iteration=n}},T=class extends Error{browser;constructor(t,o){super(`Failed to launch browser "${t}": ${L(o)}`,{cause:o}),this.name="BrowserLaunchError",this.browser=t}};var V=r=>{let t;try{t=B.load(r)}catch(n){throw new S(`Invalid YAML syntax: ${n instanceof Error?n.message:String(n)}`,{cause:n})}let o=P.safeParse(t);if(!o.success)throw new S(`Invalid scenario configuration:
2
- ${F.prettifyError(o.error)}`,{cause:o.error});return o.data};var A={debug(r){console.log(r)},info(r){console.log(r)},warn(r){console.log(r)},error(r){console.error(r)}};var dt=/^[A-Za-z_][A-Za-z0-9_]*$/,ht=/^\$([A-Za-z_][A-Za-z0-9_]*)$/,wt=/^\$([A-Za-z_][A-Za-z0-9_]*)\[workerIndex\]$/,St=/\$\$\{|\$\{([^}]*)\}/g,_='per-worker values ("$VAR[workerIndex]") are only supported as the whole value of location and navigate',O=class{envVarResolve;logger;constructor(t,o=A){this.envVarResolve=t,this.logger=o}resolve(t){return{scenarios:t.scenarios.map((o,n)=>this.resolveScenario(o,n+1))}}resolveScenario(t,o){let n={name:t.name||`Scenario ${o}`,workers:t.workers||1};this.logger.debug(`Resolving scenario "${n.name}" (${t.steps?.length??0} step(s), ${t.actions?.length??0} action(s))`);let i=[];try{t.location&&i.push(this.createNavigateAction(t.location,n));for(let s of t.steps??[])i.push(this.resolveStep(s,n));for(let s of t.actions??[])i.push(this.resolveAction(s))}catch(s){throw new S(`Scenario "${n.name}": ${s instanceof Error?s.message:String(s)}`,{cause:s})}let a=t.run==="SEQUENTIAL"?{delaySecondsBetweenWorkerInits:t.initialDelaySeconds||0}:{initialDelaySeconds:t.initialDelaySeconds||0};return{name:n.name,browser:t.browser||"chrome",run:a,workers:n.workers,iterations:t.iterations||1,actions:i}}resolveStep(t,o){return t==="waitForever"?{actionType:"WaitForever"}:"type"in t?this.resolveAction(t):"wait"in t?{actionType:"Wait",milliseconds:t.wait*1e3}:"navigate"in t?this.createNavigateAction(t.navigate,o):"click"in t?{actionType:"CLICK",selector:this.interpolate(t.click)}:{actionType:"TYPE",selector:this.interpolate(t.input.selector),text:this.interpolate(t.input.text)}}resolveAction(t){switch(this.logger.debug(`Resolving action: ${t.type}`),t.type){case"GOTO":return{actionType:"GOTO",url:this.resolveWholeValueOrInterpolate(t.url),options:t.options&&{...t.options,referer:t.options.referer&&this.interpolate(t.options.referer)}};case"WAIT":return{actionType:"Wait",milliseconds:t.milliseconds};case"CLICK":return{actionType:"CLICK",selector:this.interpolate(t.selector),options:t.options};case"TYPE":return{actionType:"TYPE",selector:this.interpolate(t.selector),text:this.interpolate(t.text),options:t.options};case"WAIT_FOR_SELECTOR":return{actionType:"WAIT_FOR_SELECTOR",selector:this.interpolate(t.selector),options:t.options};case"WAIT_FOR_TIMEOUT":return{actionType:"WAIT_FOR_TIMEOUT",timeout:t.timeout};case"PAUSE":return{actionType:"PAUSE"};case"SCREENSHOT":return{actionType:"SCREENSHOT",path:this.resolveWholeValueOrInterpolate(t.path),options:t.options};case"EVALUATE":return{actionType:"EVALUATE",script:t.script};case"SET_VIEWPORT":return{actionType:"SET_VIEWPORT",width:t.width,height:t.height};case"CLOSE":return{actionType:"CLOSE"};default:return t}}createNavigateAction(t,o){let n=wt.exec(t);if(n){let i=n[1],a=this.env(i).split(",").map(s=>s.trim());if(a.some(s=>s===""))throw new Error(`environment variable "${i}" (used as "${t}") contains an empty value`);if(a.length<o.workers)throw new Error(`environment variable "${i}" (used as "${t}") provides ${a.length} value(s) for ${o.workers} worker(s)`);return{actionType:"Navigate",location:{workerIndex:a}}}return{actionType:"Navigate",location:{url:this.resolveWholeValueOrInterpolate(t)}}}resolveWholeValueOrInterpolate(t){let o=ht.exec(t);if(o)return this.env(o[1]);if(/^\$[A-Za-z_]/.test(t))throw new Error(`unsupported environment variable reference "${t}": use "$VAR" for the whole value or "\${VAR}" inside a value`+(t.endsWith("[workerIndex]")?`; ${_}`:""));return this.interpolate(t)}interpolate(t){return t.replace(St,(o,n)=>{if(o==="$${")return"${";if(n===void 0||!dt.test(n))throw new Error(`invalid placeholder "${o}": expected \${VAR_NAME}`+(n?.endsWith("[workerIndex]")?`; ${_}`:""));return this.env(n)})}env(t){return this.logger.debug(`Resolving environment variable "${t}"`),this.envVarResolve(t)}};import{setTimeout as k}from"node:timers/promises";function yt(r,t){let o=t||"png";return r.endsWith(".png")||r.endsWith(".jpeg")||r.endsWith(".jpg")||r.endsWith(".webp")?r:`${r}.${o}`}var At=(r,t)=>"delaySecondsBetweenWorkerInits"in r?r.delaySecondsBetweenWorkerInits*t:r.initialDelaySeconds,j=async(r,t,o=A,n)=>{let i=[],a=async s=>{let p=At(t.run,s),h="delaySecondsBetweenWorkerInits"in t.run?"SEQUENTIAL":"PARALLEL";o.info(`Scenario "${t.name}" (worker ${s}) started (${h}, starting in ${p}s).`),await k(p*1e3);for(let u=0;u<t.iterations;u++)try{await ft(r,t,s,u,o,n)}catch(y){let m=new $(t.name,s,u,y);o.error(m.message),i.push(m)}};if(await Promise.all(Array.from({length:t.workers},(s,p)=>a(p))),i.length>0){i.sort((p,h)=>p.workerIndex-h.workerIndex||p.iteration-h.iteration);let s=t.workers*t.iterations;throw new AggregateError(i,`Scenario "${t.name}": ${i.length}/${s} iteration(s) failed`)}},ft=async(r,t,o,n,i,a)=>{let s;try{s=await r.newPage(),a&&await a(s,{scenarioName:t.name,workerIndex:o,iteration:n}),await bt(t.name,t.actions,s,o,i)}finally{s&&!s.isClosed()&&await s.close().catch(p=>i.warn(`Scenario "${t.name}" (worker ${o}, iteration ${n}) > failed to close page: ${p}`))}},bt=async(r,t,o,n,i=A)=>{for(let a of t){let{actionType:s}=a;switch(s){case"Navigate":let p=a;"url"in p.location?(i.info(`Scenario "${r}" (worker ${n}) > Navigating to location "${p.location.url}"`),await o.goto(p.location.url)):(i.info(`Scenario "${r}" (worker ${n}) > Navigating to indexed location "${p.location.workerIndex[n]}"`),await o.goto(p.location.workerIndex[n]));break;case"Wait":let h=a;i.info(`Scenario "${r}" (worker ${n}) > Waiting ${h.milliseconds} milliseconds...`),await k(h.milliseconds);break;case"WaitForever":for(i.info(`Scenario "${r}" (worker ${n}) > Waiting forever...`);;)await k(3600*1e3);break;case"GOTO":let u=a;i.info(`Scenario "${r}" (worker ${n}) > GOTO "${u.url}"`);let y={timeout:u.options?.timeout,waitUntil:u.options?.waitUntil,referer:u.options?.referer};await o.goto(u.url,y);break;case"CLICK":let m=a;i.info(`Scenario "${r}" (worker ${n}) > CLICK on "${m.selector}"`);let W={button:m.options?.button,clickCount:m.options?.clickCount,delay:m.options?.delay};await o.click(m.selector,W);break;case"TYPE":let w=a;i.info(`Scenario "${r}" (worker ${n}) > TYPE into "${w.selector}"`),await o.type(w.selector,w.text,w.options);break;case"WAIT_FOR_SELECTOR":let c=a;i.info(`Scenario "${r}" (worker ${n}) > WAIT_FOR_SELECTOR "${c.selector}"`),await o.waitForSelector(c.selector,c.options);break;case"WAIT_FOR_TIMEOUT":let g=a;i.info(`Scenario "${r}" (worker ${n}) > WAIT_FOR_TIMEOUT ${g.timeout} milliseconds`),await k(g.timeout);break;case"PAUSE":if(!process.stdin.isTTY){i.warn(`Scenario "${r}" (worker ${n}) > PAUSE ignored: no interactive terminal`);break}i.info(`Scenario "${r}" (worker ${n}) > PAUSE, press Enter to continue...`),await new Promise(f=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),f()})});break;case"SCREENSHOT":let l=a;i.info(`Scenario "${r}" (worker ${n}) > SCREENSHOT "${l.path}"`);let z={path:yt(l.path,l.options?.type),type:l.options?.type,quality:l.options?.quality,fullPage:l.options?.fullPage,clip:l.options?.clip,omitBackground:l.options?.omitBackground,encoding:l.options?.encoding};await o.screenshot(z);break;case"EVALUATE":let E=a;i.info(`Scenario "${r}" (worker ${n}) > EVALUATE script`),await o.evaluate(E.script);break;case"SET_VIEWPORT":let d=a;i.info(`Scenario "${r}" (worker ${n}) > SET_VIEWPORT width=${d.width}, height=${d.height}`),await o.setViewport({width:d.width,height:d.height});break;case"CLOSE":i.info(`Scenario "${r}" (worker ${n}) > CLOSE page`),await o.close();break;default:throw new Error(`Unhandled action type "${s}" while evaluating scenario "${r}" on worker ${n}`)}}};import*as I from"puppeteer";var vt=(r,t)=>{if(r==="firefox")return[];let o=["--no-sandbox","--disable-setuid-sandbox","--disable-dev-shm-usage","--disable-accelerated-2d-canvas","--disable-gpu","--ignore-certificate-errors"];return t||o.push("--start-maximized"),o},$t=(r,t,o={})=>({browser:r,headless:t,acceptInsecureCerts:!0,...o,args:[...vt(r,t),...o.args??[]]}),U=(r,t,o)=>I.launch($t(r,t,o));var he=async(r,t={})=>{let{browsers:o={},logger:n=A,onPage:i,headless:a=!0,launchOptions:s={}}=t,p=V(r),u=new O(c=>{let g=process.env[c];if(g)return g;throw new Error(`environment variable "${c}" is not set (or empty)`)},n).resolve(p),y=new Map;for(let c of u.scenarios){let g=y.get(c.browser)??[];g.push(c),y.set(c.browser,g)}let m=async(c,g)=>{let l=o[c],z=!l;if(!l)try{n.info(`Launching ${c} (headless: ${a})`),l=await U(c,a,s[c])}catch(d){let f=new T(c,d);return n.error(f.message),[f]}let E=l;try{return(await Promise.all(g.map(async f=>{try{return await j(E,f,n,i),[]}catch(x){return x instanceof AggregateError?x.errors:[x]}}))).flat()}finally{z&&await E.close().catch(d=>n.warn(`Failed to close ${c}: ${d}`))}},w=(await Promise.all([...y].map(([c,g])=>m(c,g)))).flat();if(w.length>0)throw new AggregateError(w,`${w.length} error(s) occurred while running scenarios`)};export{T as BrowserLaunchError,S as ConfigurationError,$ as WorkerError,he as readYamlAndInterpret};
1
+ import*as F from"js-yaml";import*as _ from"zod";import*as t from"zod";var y=t.number().int().nonnegative(),v=t.number().int().positive(),$=t.string().describe("CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."),W=t.string().describe('URL. "$VAR" as the whole value, or ${VAR} placeholders anywhere.'),L=t.enum(["load","domcontentloaded","networkidle0","networkidle2"]),M=t.enum(["left","right","middle"]),D=t.enum(["chrome","firefox"]),Z=t.strictObject({timeout:y.optional().describe("Navigation timeout in milliseconds."),waitUntil:t.union([L,L.array()]).optional().describe("When to consider the navigation finished."),referer:t.string().optional().describe("Referer header. ${VAR} placeholders are resolved.")}).optional(),H=t.strictObject({button:M.optional(),clickCount:v.optional().describe("2 for a double-click."),delay:y.optional().describe("Milliseconds between mousedown and mouseup.")}).optional(),K=t.strictObject({delay:y.optional().describe("Milliseconds between keystrokes.")}).optional(),q=t.strictObject({visible:t.boolean().optional(),hidden:t.boolean().optional(),timeout:y.optional().describe("Timeout in milliseconds.")}).optional(),Q=t.strictObject({type:t.enum(["png","jpeg"]).optional(),quality:t.number().int().min(0).max(100).optional().describe("JPEG quality (0-100)."),fullPage:t.boolean().optional(),clip:t.strictObject({x:t.number(),y:t.number(),width:t.number().positive(),height:t.number().positive()}).optional(),omitBackground:t.boolean().optional(),encoding:t.enum(["base64","binary"]).optional()}).optional(),J=t.strictObject({type:t.literal("GOTO"),url:W,options:Z}).describe("Navigate to a URL, with navigation options."),X=t.strictObject({type:t.literal("CLICK"),selector:$,options:H}).describe("Click on an element."),Y=t.strictObject({type:t.literal("TYPE"),selector:$,text:t.string().describe("Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged."),options:K}).describe("Type text into an element."),ee=t.strictObject({type:t.literal("WAIT_FOR_SELECTOR"),selector:$,options:q}).describe("Wait for an element to appear (or to be visible / hidden)."),te=t.strictObject({type:t.literal("WAIT_FOR_TIMEOUT"),timeout:y.describe("Milliseconds.")}).describe("Wait for a duration in milliseconds."),oe=t.strictObject({type:t.literal("WAIT"),milliseconds:y}).describe("Wait for a duration in milliseconds."),re=t.strictObject({type:t.literal("PAUSE")}).describe("Wait for Enter in an interactive terminal (debugging). Ignored with a warning when there is none."),ie=t.strictObject({type:t.literal("SCREENSHOT"),path:t.string().describe('File path. "$VAR" as the whole value, or ${VAR} placeholders anywhere.'),options:Q}).describe("Take a screenshot."),ne=t.strictObject({type:t.literal("EVALUATE"),script:t.string().describe("JavaScript evaluated in the page. Never interpolated.")}).describe("Evaluate JavaScript in the page."),se=t.strictObject({type:t.literal("SET_VIEWPORT"),width:v,height:v}).describe("Set the page viewport size."),ae=t.strictObject({type:t.literal("CLOSE")}).describe("Close the page."),P=t.discriminatedUnion("type",[J,X,Y,ee,te,oe,re,ie,ne,se,ae]),ce=t.strictObject({navigate:W}).describe('Navigate to a URL ("$VAR[workerIndex]" gives one URL per worker).'),pe=t.literal("waitForever").describe("Keep the page open forever."),le=t.strictObject({wait:t.number().nonnegative().describe("Seconds.")}).describe("Wait for a duration in seconds."),ue=t.strictObject({click:$}).describe("Click on an element."),de=t.strictObject({input:t.strictObject({selector:$,text:t.string().describe("Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged.")})}).describe("Type text into an element."),ge=t.union([ce,pe,le,ue,de,P]),he=t.strictObject({name:t.string().optional().describe('Scenario name (default: "Scenario <n>").'),browser:D.optional().describe("Browser to use (default: chrome)."),run:t.enum(["PARALLEL","SEQUENTIAL"]).optional().describe("Worker start strategy (default: PARALLEL). PARALLEL: all workers start after initialDelaySeconds. SEQUENTIAL: worker n starts after n \xD7 initialDelaySeconds."),initialDelaySeconds:t.number().nonnegative().optional().describe("Start delay, applied once per worker before its first iteration (see run)."),workers:v.optional().describe("Number of parallel workers (default: 1)."),iterations:v.optional().describe("Repetitions per worker (default: 1)."),location:W.optional().describe('Starting URL. "$VAR", ${VAR} placeholders, or "$VAR[workerIndex]" for one URL per worker.'),steps:t.array(ge).optional().describe("Shorthand steps; full actions are accepted too, to mix both syntaxes in order."),actions:t.array(P).optional().describe("Full actions, run after steps.")}).describe("A population of users: its workers run the steps and actions for the given number of iterations."),B=t.strictObject({scenarios:t.array(he).describe("Scenarios, all started at the same time.")});var V=r=>r instanceof Error?r.message:String(r),b=class extends Error{constructor(e,o){super(e,o),this.name="ConfigurationError"}},T=class extends Error{scenarioName;workerIndex;iteration;constructor(e,o,i,n){super(`Scenario "${e}" (worker ${o}, iteration ${i}) failed: ${V(n)}`,{cause:n}),this.name="WorkerError",this.scenarioName=e,this.workerIndex=o,this.iteration=i}},E=class extends Error{browser;constructor(e,o){super(`Failed to launch browser "${e}": ${V(o)}`,{cause:o}),this.name="BrowserLaunchError",this.browser=e}};var I=r=>{let e;try{e=F.load(r)}catch(i){throw new b(`Invalid YAML syntax: ${i instanceof Error?i.message:String(i)}`,{cause:i})}let o=B.safeParse(e);if(!o.success)throw new b(`Invalid scenario configuration:
2
+ ${_.prettifyError(o.error)}`,{cause:o.error});return o.data};var f={debug(r){console.log(r)},info(r){console.log(r)},warn(r){console.log(r)},error(r){console.error(r)}};var we=/^[A-Za-z_][A-Za-z0-9_]*$/,be=/^\$([A-Za-z_][A-Za-z0-9_]*)$/,Se=/^\$([A-Za-z_][A-Za-z0-9_]*)\[workerIndex\]$/,fe=/\$\$\{|\$\{([^}]*)\}/g,U='per-worker values ("$VAR[workerIndex]") are only supported as the whole value of location and navigate',O=class{envVarResolve;logger;constructor(e,o=f){this.envVarResolve=e,this.logger=o}resolve(e){return{scenarios:e.scenarios.map((o,i)=>this.resolveScenario(o,i+1))}}resolveScenario(e,o){let i={name:e.name||`Scenario ${o}`,workers:e.workers||1};this.logger.debug(`Resolving scenario "${i.name}" (${e.steps?.length??0} step(s), ${e.actions?.length??0} action(s))`);let n=[];try{e.location&&n.push(this.createNavigateAction(e.location,i));for(let s of e.steps??[])n.push(this.resolveStep(s,i));for(let s of e.actions??[])n.push(this.resolveAction(s))}catch(s){throw new b(`Scenario "${i.name}": ${s instanceof Error?s.message:String(s)}`,{cause:s})}let a=e.run==="SEQUENTIAL"?{delaySecondsBetweenWorkerInits:e.initialDelaySeconds||0}:{initialDelaySeconds:e.initialDelaySeconds||0};return{name:i.name,browser:e.browser||"chrome",run:a,workers:i.workers,iterations:e.iterations||1,actions:n}}resolveStep(e,o){return e==="waitForever"?{actionType:"WaitForever"}:"type"in e?this.resolveAction(e):"wait"in e?{actionType:"Wait",milliseconds:e.wait*1e3}:"navigate"in e?this.createNavigateAction(e.navigate,o):"click"in e?{actionType:"CLICK",selector:this.interpolate(e.click)}:{actionType:"TYPE",selector:this.interpolate(e.input.selector),text:this.interpolate(e.input.text)}}resolveAction(e){switch(this.logger.debug(`Resolving action: ${e.type}`),e.type){case"GOTO":return{actionType:"GOTO",url:this.resolveWholeValueOrInterpolate(e.url),options:e.options&&{...e.options,referer:e.options.referer&&this.interpolate(e.options.referer)}};case"WAIT":return{actionType:"Wait",milliseconds:e.milliseconds};case"CLICK":return{actionType:"CLICK",selector:this.interpolate(e.selector),options:e.options};case"TYPE":return{actionType:"TYPE",selector:this.interpolate(e.selector),text:this.interpolate(e.text),options:e.options};case"WAIT_FOR_SELECTOR":return{actionType:"WAIT_FOR_SELECTOR",selector:this.interpolate(e.selector),options:e.options};case"WAIT_FOR_TIMEOUT":return{actionType:"WAIT_FOR_TIMEOUT",timeout:e.timeout};case"PAUSE":return{actionType:"PAUSE"};case"SCREENSHOT":return{actionType:"SCREENSHOT",path:this.resolveWholeValueOrInterpolate(e.path),options:e.options};case"EVALUATE":return{actionType:"EVALUATE",script:e.script};case"SET_VIEWPORT":return{actionType:"SET_VIEWPORT",width:e.width,height:e.height};case"CLOSE":return{actionType:"CLOSE"};default:return e}}createNavigateAction(e,o){let i=Se.exec(e);if(i){let n=i[1],a=this.env(n).split(",").map(s=>s.trim());if(a.some(s=>s===""))throw new Error(`environment variable "${n}" (used as "${e}") contains an empty value`);if(a.length<o.workers)throw new Error(`environment variable "${n}" (used as "${e}") provides ${a.length} value(s) for ${o.workers} worker(s)`);return{actionType:"Navigate",location:{workerIndex:a}}}return{actionType:"Navigate",location:{url:this.resolveWholeValueOrInterpolate(e)}}}resolveWholeValueOrInterpolate(e){let o=be.exec(e);if(o)return this.env(o[1]);if(/^\$[A-Za-z_]/.test(e))throw new Error(`unsupported environment variable reference "${e}": use "$VAR" for the whole value or "\${VAR}" inside a value`+(e.endsWith("[workerIndex]")?`; ${U}`:""));return this.interpolate(e)}interpolate(e){return e.replace(fe,(o,i)=>{if(o==="$${")return"${";if(i===void 0||!we.test(i))throw new Error(`invalid placeholder "${o}": expected \${VAR_NAME}`+(i?.endsWith("[workerIndex]")?`; ${U}`:""));return this.env(i)})}env(e){return this.logger.debug(`Resolving environment variable "${e}"`),this.envVarResolve(e)}};import{setTimeout as x}from"node:timers/promises";function Ae(r,e){let o=e||"png";return r.endsWith(".png")||r.endsWith(".jpeg")||r.endsWith(".jpg")||r.endsWith(".webp")?r:`${r}.${o}`}var ye=(r,e)=>"delaySecondsBetweenWorkerInits"in r?r.delaySecondsBetweenWorkerInits*e:r.initialDelaySeconds,j=async(r,e,o=f,i)=>{let n=[],a=async s=>{let p=ye(e.run,s),m="delaySecondsBetweenWorkerInits"in e.run?"SEQUENTIAL":"PARALLEL";o.info(`Scenario "${e.name}" (worker ${s}) started (${m}, starting in ${p}s).`),await x(p*1e3);for(let u=0;u<e.iterations;u++)try{await ve(r,e,s,u,o,i)}catch(S){let g=new T(e.name,s,u,S);o.error(g.message),n.push(g)}};if(await Promise.all(Array.from({length:e.workers},(s,p)=>a(p))),n.length>0){n.sort((p,m)=>p.workerIndex-m.workerIndex||p.iteration-m.iteration);let s=e.workers*e.iterations;throw new AggregateError(n,`Scenario "${e.name}": ${n.length}/${s} iteration(s) failed`)}},ve=async(r,e,o,i,n,a)=>{let s;try{s=await r.newPage(),a&&await a(s,{scenarioName:e.name,workerIndex:o,iteration:i}),await $e(e.name,e.actions,s,o,n)}finally{s&&!s.isClosed()&&await s.close().catch(p=>n.warn(`Scenario "${e.name}" (worker ${o}, iteration ${i}) > failed to close page: ${p}`))}},$e=async(r,e,o,i,n=f)=>{for(let a of e){let{actionType:s}=a;switch(s){case"Navigate":let p=a;"url"in p.location?(n.info(`Scenario "${r}" (worker ${i}) > Navigating to location "${p.location.url}"`),await o.goto(p.location.url)):(n.info(`Scenario "${r}" (worker ${i}) > Navigating to indexed location "${p.location.workerIndex[i]}"`),await o.goto(p.location.workerIndex[i]));break;case"Wait":let m=a;n.info(`Scenario "${r}" (worker ${i}) > Waiting ${m.milliseconds} milliseconds...`),await x(m.milliseconds);break;case"WaitForever":for(n.info(`Scenario "${r}" (worker ${i}) > Waiting forever...`);;)await x(3600*1e3);break;case"GOTO":let u=a;n.info(`Scenario "${r}" (worker ${i}) > GOTO "${u.url}"`);let S={timeout:u.options?.timeout,waitUntil:u.options?.waitUntil,referer:u.options?.referer};await o.goto(u.url,S);break;case"CLICK":let g=a;n.info(`Scenario "${r}" (worker ${i}) > CLICK on "${g.selector}"`);let C={button:g.options?.button,clickCount:g.options?.clickCount,delay:g.options?.delay};await o.click(g.selector,C);break;case"TYPE":let w=a;n.info(`Scenario "${r}" (worker ${i}) > TYPE into "${w.selector}"`),await o.type(w.selector,w.text,w.options);break;case"WAIT_FOR_SELECTOR":let c=a;n.info(`Scenario "${r}" (worker ${i}) > WAIT_FOR_SELECTOR "${c.selector}"`),await o.waitForSelector(c.selector,c.options);break;case"WAIT_FOR_TIMEOUT":let d=a;n.info(`Scenario "${r}" (worker ${i}) > WAIT_FOR_TIMEOUT ${d.timeout} milliseconds`),await x(d.timeout);break;case"PAUSE":if(!process.stdin.isTTY){n.warn(`Scenario "${r}" (worker ${i}) > PAUSE ignored: no interactive terminal`);break}n.info(`Scenario "${r}" (worker ${i}) > PAUSE, press Enter to continue...`),await new Promise(A=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),A()})});break;case"SCREENSHOT":let l=a;n.info(`Scenario "${r}" (worker ${i}) > SCREENSHOT "${l.path}"`);let R={path:Ae(l.path,l.options?.type),type:l.options?.type,quality:l.options?.quality,fullPage:l.options?.fullPage,clip:l.options?.clip,omitBackground:l.options?.omitBackground,encoding:l.options?.encoding};await o.screenshot(R);break;case"EVALUATE":let k=a;n.info(`Scenario "${r}" (worker ${i}) > EVALUATE script`),await o.evaluate(k.script);break;case"SET_VIEWPORT":let h=a;n.info(`Scenario "${r}" (worker ${i}) > SET_VIEWPORT width=${h.width}, height=${h.height}`),await o.setViewport({width:h.width,height:h.height});break;case"CLOSE":n.info(`Scenario "${r}" (worker ${i}) > CLOSE page`),await o.close();break;default:throw new Error(`Unhandled action type "${s}" while evaluating scenario "${r}" on worker ${i}`)}}};import*as N from"puppeteer";var Te=(r,e)=>{if(r==="firefox")return[];let o=["--no-sandbox","--disable-setuid-sandbox","--disable-dev-shm-usage","--disable-accelerated-2d-canvas","--disable-gpu","--ignore-certificate-errors"];return e||o.push("--start-maximized"),o},Ee=(r,e,o={})=>({browser:r,headless:e,acceptInsecureCerts:!0,...o,args:[...Te(r,e),...o.args??[]]}),G=(r,e,o)=>N.launch(Ee(r,e,o));var bt=async(r,e={})=>{let{browsers:o={},logger:i=f,onPage:n,headless:a=!0,launchOptions:s={}}=e,p=I(r),u=new O(c=>{let d=process.env[c];if(d)return d;throw new Error(`environment variable "${c}" is not set (or empty)`)},i).resolve(p),S=new Map;for(let c of u.scenarios){let d=S.get(c.browser)??[];d.push(c),S.set(c.browser,d)}let g=async(c,d)=>{let l=o[c],R=!l;if(!l)try{i.info(`Launching ${c} (headless: ${a})`),l=await G(c,a,s[c])}catch(h){let A=new E(c,h);return i.error(A.message),[A]}let k=l;try{return(await Promise.all(d.map(async A=>{try{return await j(k,A,i,n),[]}catch(z){return z instanceof AggregateError?z.errors:[z]}}))).flat()}finally{R&&await k.close().catch(h=>i.warn(`Failed to close ${c}: ${h}`))}},w=(await Promise.all([...S].map(([c,d])=>g(c,d)))).flat();if(w.length>0)throw new AggregateError(w,`${w.length} error(s) occurred while running scenarios`)};export{E as BrowserLaunchError,b as ConfigurationError,T as WorkerError,bt as readYamlAndInterpret};
@@ -0,0 +1,900 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "yaml-pptr scenarios",
4
+ "description": "Scenarios run by @bloom-perf/yaml-pptr — https://github.com/Bloom-Perf/yaml-pptr",
5
+ "type": "object",
6
+ "properties": {
7
+ "scenarios": {
8
+ "type": "array",
9
+ "items": {
10
+ "type": "object",
11
+ "properties": {
12
+ "name": {
13
+ "description": "Scenario name (default: \"Scenario <n>\").",
14
+ "type": "string"
15
+ },
16
+ "browser": {
17
+ "description": "Browser to use (default: chrome).",
18
+ "type": "string",
19
+ "enum": [
20
+ "chrome",
21
+ "firefox"
22
+ ]
23
+ },
24
+ "run": {
25
+ "description": "Worker start strategy (default: PARALLEL). PARALLEL: all workers start after initialDelaySeconds. SEQUENTIAL: worker n starts after n × initialDelaySeconds.",
26
+ "type": "string",
27
+ "enum": [
28
+ "PARALLEL",
29
+ "SEQUENTIAL"
30
+ ]
31
+ },
32
+ "initialDelaySeconds": {
33
+ "description": "Start delay, applied once per worker before its first iteration (see run).",
34
+ "type": "number",
35
+ "minimum": 0
36
+ },
37
+ "workers": {
38
+ "description": "Number of parallel workers (default: 1).",
39
+ "type": "integer",
40
+ "exclusiveMinimum": 0,
41
+ "maximum": 9007199254740991
42
+ },
43
+ "iterations": {
44
+ "description": "Repetitions per worker (default: 1).",
45
+ "type": "integer",
46
+ "exclusiveMinimum": 0,
47
+ "maximum": 9007199254740991
48
+ },
49
+ "location": {
50
+ "description": "Starting URL. \"$VAR\", ${VAR} placeholders, or \"$VAR[workerIndex]\" for one URL per worker.",
51
+ "type": "string"
52
+ },
53
+ "steps": {
54
+ "description": "Shorthand steps; full actions are accepted too, to mix both syntaxes in order.",
55
+ "type": "array",
56
+ "items": {
57
+ "anyOf": [
58
+ {
59
+ "type": "object",
60
+ "properties": {
61
+ "navigate": {
62
+ "type": "string",
63
+ "description": "URL. \"$VAR\" as the whole value, or ${VAR} placeholders anywhere."
64
+ }
65
+ },
66
+ "required": [
67
+ "navigate"
68
+ ],
69
+ "additionalProperties": false,
70
+ "description": "Navigate to a URL (\"$VAR[workerIndex]\" gives one URL per worker)."
71
+ },
72
+ {
73
+ "type": "string",
74
+ "const": "waitForever",
75
+ "description": "Keep the page open forever."
76
+ },
77
+ {
78
+ "type": "object",
79
+ "properties": {
80
+ "wait": {
81
+ "type": "number",
82
+ "minimum": 0,
83
+ "description": "Seconds."
84
+ }
85
+ },
86
+ "required": [
87
+ "wait"
88
+ ],
89
+ "additionalProperties": false,
90
+ "description": "Wait for a duration in seconds."
91
+ },
92
+ {
93
+ "type": "object",
94
+ "properties": {
95
+ "click": {
96
+ "type": "string",
97
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
98
+ }
99
+ },
100
+ "required": [
101
+ "click"
102
+ ],
103
+ "additionalProperties": false,
104
+ "description": "Click on an element."
105
+ },
106
+ {
107
+ "type": "object",
108
+ "properties": {
109
+ "input": {
110
+ "type": "object",
111
+ "properties": {
112
+ "selector": {
113
+ "type": "string",
114
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
115
+ },
116
+ "text": {
117
+ "type": "string",
118
+ "description": "Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged."
119
+ }
120
+ },
121
+ "required": [
122
+ "selector",
123
+ "text"
124
+ ],
125
+ "additionalProperties": false
126
+ }
127
+ },
128
+ "required": [
129
+ "input"
130
+ ],
131
+ "additionalProperties": false,
132
+ "description": "Type text into an element."
133
+ },
134
+ {
135
+ "oneOf": [
136
+ {
137
+ "type": "object",
138
+ "properties": {
139
+ "type": {
140
+ "type": "string",
141
+ "const": "GOTO"
142
+ },
143
+ "url": {
144
+ "type": "string",
145
+ "description": "URL. \"$VAR\" as the whole value, or ${VAR} placeholders anywhere."
146
+ },
147
+ "options": {
148
+ "type": "object",
149
+ "properties": {
150
+ "timeout": {
151
+ "description": "Navigation timeout in milliseconds.",
152
+ "type": "integer",
153
+ "minimum": 0,
154
+ "maximum": 9007199254740991
155
+ },
156
+ "waitUntil": {
157
+ "description": "When to consider the navigation finished.",
158
+ "anyOf": [
159
+ {
160
+ "type": "string",
161
+ "enum": [
162
+ "load",
163
+ "domcontentloaded",
164
+ "networkidle0",
165
+ "networkidle2"
166
+ ]
167
+ },
168
+ {
169
+ "type": "array",
170
+ "items": {
171
+ "type": "string",
172
+ "enum": [
173
+ "load",
174
+ "domcontentloaded",
175
+ "networkidle0",
176
+ "networkidle2"
177
+ ]
178
+ }
179
+ }
180
+ ]
181
+ },
182
+ "referer": {
183
+ "description": "Referer header. ${VAR} placeholders are resolved.",
184
+ "type": "string"
185
+ }
186
+ },
187
+ "additionalProperties": false
188
+ }
189
+ },
190
+ "required": [
191
+ "type",
192
+ "url"
193
+ ],
194
+ "additionalProperties": false,
195
+ "description": "Navigate to a URL, with navigation options."
196
+ },
197
+ {
198
+ "type": "object",
199
+ "properties": {
200
+ "type": {
201
+ "type": "string",
202
+ "const": "CLICK"
203
+ },
204
+ "selector": {
205
+ "type": "string",
206
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
207
+ },
208
+ "options": {
209
+ "type": "object",
210
+ "properties": {
211
+ "button": {
212
+ "type": "string",
213
+ "enum": [
214
+ "left",
215
+ "right",
216
+ "middle"
217
+ ]
218
+ },
219
+ "clickCount": {
220
+ "description": "2 for a double-click.",
221
+ "type": "integer",
222
+ "exclusiveMinimum": 0,
223
+ "maximum": 9007199254740991
224
+ },
225
+ "delay": {
226
+ "description": "Milliseconds between mousedown and mouseup.",
227
+ "type": "integer",
228
+ "minimum": 0,
229
+ "maximum": 9007199254740991
230
+ }
231
+ },
232
+ "additionalProperties": false
233
+ }
234
+ },
235
+ "required": [
236
+ "type",
237
+ "selector"
238
+ ],
239
+ "additionalProperties": false,
240
+ "description": "Click on an element."
241
+ },
242
+ {
243
+ "type": "object",
244
+ "properties": {
245
+ "type": {
246
+ "type": "string",
247
+ "const": "TYPE"
248
+ },
249
+ "selector": {
250
+ "type": "string",
251
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
252
+ },
253
+ "text": {
254
+ "type": "string",
255
+ "description": "Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged."
256
+ },
257
+ "options": {
258
+ "type": "object",
259
+ "properties": {
260
+ "delay": {
261
+ "description": "Milliseconds between keystrokes.",
262
+ "type": "integer",
263
+ "minimum": 0,
264
+ "maximum": 9007199254740991
265
+ }
266
+ },
267
+ "additionalProperties": false
268
+ }
269
+ },
270
+ "required": [
271
+ "type",
272
+ "selector",
273
+ "text"
274
+ ],
275
+ "additionalProperties": false,
276
+ "description": "Type text into an element."
277
+ },
278
+ {
279
+ "type": "object",
280
+ "properties": {
281
+ "type": {
282
+ "type": "string",
283
+ "const": "WAIT_FOR_SELECTOR"
284
+ },
285
+ "selector": {
286
+ "type": "string",
287
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
288
+ },
289
+ "options": {
290
+ "type": "object",
291
+ "properties": {
292
+ "visible": {
293
+ "type": "boolean"
294
+ },
295
+ "hidden": {
296
+ "type": "boolean"
297
+ },
298
+ "timeout": {
299
+ "description": "Timeout in milliseconds.",
300
+ "type": "integer",
301
+ "minimum": 0,
302
+ "maximum": 9007199254740991
303
+ }
304
+ },
305
+ "additionalProperties": false
306
+ }
307
+ },
308
+ "required": [
309
+ "type",
310
+ "selector"
311
+ ],
312
+ "additionalProperties": false,
313
+ "description": "Wait for an element to appear (or to be visible / hidden)."
314
+ },
315
+ {
316
+ "type": "object",
317
+ "properties": {
318
+ "type": {
319
+ "type": "string",
320
+ "const": "WAIT_FOR_TIMEOUT"
321
+ },
322
+ "timeout": {
323
+ "type": "integer",
324
+ "minimum": 0,
325
+ "maximum": 9007199254740991,
326
+ "description": "Milliseconds."
327
+ }
328
+ },
329
+ "required": [
330
+ "type",
331
+ "timeout"
332
+ ],
333
+ "additionalProperties": false,
334
+ "description": "Wait for a duration in milliseconds."
335
+ },
336
+ {
337
+ "type": "object",
338
+ "properties": {
339
+ "type": {
340
+ "type": "string",
341
+ "const": "WAIT"
342
+ },
343
+ "milliseconds": {
344
+ "type": "integer",
345
+ "minimum": 0,
346
+ "maximum": 9007199254740991
347
+ }
348
+ },
349
+ "required": [
350
+ "type",
351
+ "milliseconds"
352
+ ],
353
+ "additionalProperties": false,
354
+ "description": "Wait for a duration in milliseconds."
355
+ },
356
+ {
357
+ "type": "object",
358
+ "properties": {
359
+ "type": {
360
+ "type": "string",
361
+ "const": "PAUSE"
362
+ }
363
+ },
364
+ "required": [
365
+ "type"
366
+ ],
367
+ "additionalProperties": false,
368
+ "description": "Wait for Enter in an interactive terminal (debugging). Ignored with a warning when there is none."
369
+ },
370
+ {
371
+ "type": "object",
372
+ "properties": {
373
+ "type": {
374
+ "type": "string",
375
+ "const": "SCREENSHOT"
376
+ },
377
+ "path": {
378
+ "type": "string",
379
+ "description": "File path. \"$VAR\" as the whole value, or ${VAR} placeholders anywhere."
380
+ },
381
+ "options": {
382
+ "type": "object",
383
+ "properties": {
384
+ "type": {
385
+ "type": "string",
386
+ "enum": [
387
+ "png",
388
+ "jpeg"
389
+ ]
390
+ },
391
+ "quality": {
392
+ "description": "JPEG quality (0-100).",
393
+ "type": "integer",
394
+ "minimum": 0,
395
+ "maximum": 100
396
+ },
397
+ "fullPage": {
398
+ "type": "boolean"
399
+ },
400
+ "clip": {
401
+ "type": "object",
402
+ "properties": {
403
+ "x": {
404
+ "type": "number"
405
+ },
406
+ "y": {
407
+ "type": "number"
408
+ },
409
+ "width": {
410
+ "type": "number",
411
+ "exclusiveMinimum": 0
412
+ },
413
+ "height": {
414
+ "type": "number",
415
+ "exclusiveMinimum": 0
416
+ }
417
+ },
418
+ "required": [
419
+ "x",
420
+ "y",
421
+ "width",
422
+ "height"
423
+ ],
424
+ "additionalProperties": false
425
+ },
426
+ "omitBackground": {
427
+ "type": "boolean"
428
+ },
429
+ "encoding": {
430
+ "type": "string",
431
+ "enum": [
432
+ "base64",
433
+ "binary"
434
+ ]
435
+ }
436
+ },
437
+ "additionalProperties": false
438
+ }
439
+ },
440
+ "required": [
441
+ "type",
442
+ "path"
443
+ ],
444
+ "additionalProperties": false,
445
+ "description": "Take a screenshot."
446
+ },
447
+ {
448
+ "type": "object",
449
+ "properties": {
450
+ "type": {
451
+ "type": "string",
452
+ "const": "EVALUATE"
453
+ },
454
+ "script": {
455
+ "type": "string",
456
+ "description": "JavaScript evaluated in the page. Never interpolated."
457
+ }
458
+ },
459
+ "required": [
460
+ "type",
461
+ "script"
462
+ ],
463
+ "additionalProperties": false,
464
+ "description": "Evaluate JavaScript in the page."
465
+ },
466
+ {
467
+ "type": "object",
468
+ "properties": {
469
+ "type": {
470
+ "type": "string",
471
+ "const": "SET_VIEWPORT"
472
+ },
473
+ "width": {
474
+ "type": "integer",
475
+ "exclusiveMinimum": 0,
476
+ "maximum": 9007199254740991
477
+ },
478
+ "height": {
479
+ "type": "integer",
480
+ "exclusiveMinimum": 0,
481
+ "maximum": 9007199254740991
482
+ }
483
+ },
484
+ "required": [
485
+ "type",
486
+ "width",
487
+ "height"
488
+ ],
489
+ "additionalProperties": false,
490
+ "description": "Set the page viewport size."
491
+ },
492
+ {
493
+ "type": "object",
494
+ "properties": {
495
+ "type": {
496
+ "type": "string",
497
+ "const": "CLOSE"
498
+ }
499
+ },
500
+ "required": [
501
+ "type"
502
+ ],
503
+ "additionalProperties": false,
504
+ "description": "Close the page."
505
+ }
506
+ ]
507
+ }
508
+ ]
509
+ }
510
+ },
511
+ "actions": {
512
+ "description": "Full actions, run after steps.",
513
+ "type": "array",
514
+ "items": {
515
+ "oneOf": [
516
+ {
517
+ "type": "object",
518
+ "properties": {
519
+ "type": {
520
+ "type": "string",
521
+ "const": "GOTO"
522
+ },
523
+ "url": {
524
+ "type": "string",
525
+ "description": "URL. \"$VAR\" as the whole value, or ${VAR} placeholders anywhere."
526
+ },
527
+ "options": {
528
+ "type": "object",
529
+ "properties": {
530
+ "timeout": {
531
+ "description": "Navigation timeout in milliseconds.",
532
+ "type": "integer",
533
+ "minimum": 0,
534
+ "maximum": 9007199254740991
535
+ },
536
+ "waitUntil": {
537
+ "description": "When to consider the navigation finished.",
538
+ "anyOf": [
539
+ {
540
+ "type": "string",
541
+ "enum": [
542
+ "load",
543
+ "domcontentloaded",
544
+ "networkidle0",
545
+ "networkidle2"
546
+ ]
547
+ },
548
+ {
549
+ "type": "array",
550
+ "items": {
551
+ "type": "string",
552
+ "enum": [
553
+ "load",
554
+ "domcontentloaded",
555
+ "networkidle0",
556
+ "networkidle2"
557
+ ]
558
+ }
559
+ }
560
+ ]
561
+ },
562
+ "referer": {
563
+ "description": "Referer header. ${VAR} placeholders are resolved.",
564
+ "type": "string"
565
+ }
566
+ },
567
+ "additionalProperties": false
568
+ }
569
+ },
570
+ "required": [
571
+ "type",
572
+ "url"
573
+ ],
574
+ "additionalProperties": false,
575
+ "description": "Navigate to a URL, with navigation options."
576
+ },
577
+ {
578
+ "type": "object",
579
+ "properties": {
580
+ "type": {
581
+ "type": "string",
582
+ "const": "CLICK"
583
+ },
584
+ "selector": {
585
+ "type": "string",
586
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
587
+ },
588
+ "options": {
589
+ "type": "object",
590
+ "properties": {
591
+ "button": {
592
+ "type": "string",
593
+ "enum": [
594
+ "left",
595
+ "right",
596
+ "middle"
597
+ ]
598
+ },
599
+ "clickCount": {
600
+ "description": "2 for a double-click.",
601
+ "type": "integer",
602
+ "exclusiveMinimum": 0,
603
+ "maximum": 9007199254740991
604
+ },
605
+ "delay": {
606
+ "description": "Milliseconds between mousedown and mouseup.",
607
+ "type": "integer",
608
+ "minimum": 0,
609
+ "maximum": 9007199254740991
610
+ }
611
+ },
612
+ "additionalProperties": false
613
+ }
614
+ },
615
+ "required": [
616
+ "type",
617
+ "selector"
618
+ ],
619
+ "additionalProperties": false,
620
+ "description": "Click on an element."
621
+ },
622
+ {
623
+ "type": "object",
624
+ "properties": {
625
+ "type": {
626
+ "type": "string",
627
+ "const": "TYPE"
628
+ },
629
+ "selector": {
630
+ "type": "string",
631
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
632
+ },
633
+ "text": {
634
+ "type": "string",
635
+ "description": "Text to type. ${VAR} placeholders are resolved (keep credentials in environment variables). Never logged."
636
+ },
637
+ "options": {
638
+ "type": "object",
639
+ "properties": {
640
+ "delay": {
641
+ "description": "Milliseconds between keystrokes.",
642
+ "type": "integer",
643
+ "minimum": 0,
644
+ "maximum": 9007199254740991
645
+ }
646
+ },
647
+ "additionalProperties": false
648
+ }
649
+ },
650
+ "required": [
651
+ "type",
652
+ "selector",
653
+ "text"
654
+ ],
655
+ "additionalProperties": false,
656
+ "description": "Type text into an element."
657
+ },
658
+ {
659
+ "type": "object",
660
+ "properties": {
661
+ "type": {
662
+ "type": "string",
663
+ "const": "WAIT_FOR_SELECTOR"
664
+ },
665
+ "selector": {
666
+ "type": "string",
667
+ "description": "CSS selector, or Puppeteer selector: ::-p-xpath(...), ::-p-text(...), ::-p-aria(...), xpath/..., >>> (shadow DOM). ${VAR} placeholders are resolved."
668
+ },
669
+ "options": {
670
+ "type": "object",
671
+ "properties": {
672
+ "visible": {
673
+ "type": "boolean"
674
+ },
675
+ "hidden": {
676
+ "type": "boolean"
677
+ },
678
+ "timeout": {
679
+ "description": "Timeout in milliseconds.",
680
+ "type": "integer",
681
+ "minimum": 0,
682
+ "maximum": 9007199254740991
683
+ }
684
+ },
685
+ "additionalProperties": false
686
+ }
687
+ },
688
+ "required": [
689
+ "type",
690
+ "selector"
691
+ ],
692
+ "additionalProperties": false,
693
+ "description": "Wait for an element to appear (or to be visible / hidden)."
694
+ },
695
+ {
696
+ "type": "object",
697
+ "properties": {
698
+ "type": {
699
+ "type": "string",
700
+ "const": "WAIT_FOR_TIMEOUT"
701
+ },
702
+ "timeout": {
703
+ "type": "integer",
704
+ "minimum": 0,
705
+ "maximum": 9007199254740991,
706
+ "description": "Milliseconds."
707
+ }
708
+ },
709
+ "required": [
710
+ "type",
711
+ "timeout"
712
+ ],
713
+ "additionalProperties": false,
714
+ "description": "Wait for a duration in milliseconds."
715
+ },
716
+ {
717
+ "type": "object",
718
+ "properties": {
719
+ "type": {
720
+ "type": "string",
721
+ "const": "WAIT"
722
+ },
723
+ "milliseconds": {
724
+ "type": "integer",
725
+ "minimum": 0,
726
+ "maximum": 9007199254740991
727
+ }
728
+ },
729
+ "required": [
730
+ "type",
731
+ "milliseconds"
732
+ ],
733
+ "additionalProperties": false,
734
+ "description": "Wait for a duration in milliseconds."
735
+ },
736
+ {
737
+ "type": "object",
738
+ "properties": {
739
+ "type": {
740
+ "type": "string",
741
+ "const": "PAUSE"
742
+ }
743
+ },
744
+ "required": [
745
+ "type"
746
+ ],
747
+ "additionalProperties": false,
748
+ "description": "Wait for Enter in an interactive terminal (debugging). Ignored with a warning when there is none."
749
+ },
750
+ {
751
+ "type": "object",
752
+ "properties": {
753
+ "type": {
754
+ "type": "string",
755
+ "const": "SCREENSHOT"
756
+ },
757
+ "path": {
758
+ "type": "string",
759
+ "description": "File path. \"$VAR\" as the whole value, or ${VAR} placeholders anywhere."
760
+ },
761
+ "options": {
762
+ "type": "object",
763
+ "properties": {
764
+ "type": {
765
+ "type": "string",
766
+ "enum": [
767
+ "png",
768
+ "jpeg"
769
+ ]
770
+ },
771
+ "quality": {
772
+ "description": "JPEG quality (0-100).",
773
+ "type": "integer",
774
+ "minimum": 0,
775
+ "maximum": 100
776
+ },
777
+ "fullPage": {
778
+ "type": "boolean"
779
+ },
780
+ "clip": {
781
+ "type": "object",
782
+ "properties": {
783
+ "x": {
784
+ "type": "number"
785
+ },
786
+ "y": {
787
+ "type": "number"
788
+ },
789
+ "width": {
790
+ "type": "number",
791
+ "exclusiveMinimum": 0
792
+ },
793
+ "height": {
794
+ "type": "number",
795
+ "exclusiveMinimum": 0
796
+ }
797
+ },
798
+ "required": [
799
+ "x",
800
+ "y",
801
+ "width",
802
+ "height"
803
+ ],
804
+ "additionalProperties": false
805
+ },
806
+ "omitBackground": {
807
+ "type": "boolean"
808
+ },
809
+ "encoding": {
810
+ "type": "string",
811
+ "enum": [
812
+ "base64",
813
+ "binary"
814
+ ]
815
+ }
816
+ },
817
+ "additionalProperties": false
818
+ }
819
+ },
820
+ "required": [
821
+ "type",
822
+ "path"
823
+ ],
824
+ "additionalProperties": false,
825
+ "description": "Take a screenshot."
826
+ },
827
+ {
828
+ "type": "object",
829
+ "properties": {
830
+ "type": {
831
+ "type": "string",
832
+ "const": "EVALUATE"
833
+ },
834
+ "script": {
835
+ "type": "string",
836
+ "description": "JavaScript evaluated in the page. Never interpolated."
837
+ }
838
+ },
839
+ "required": [
840
+ "type",
841
+ "script"
842
+ ],
843
+ "additionalProperties": false,
844
+ "description": "Evaluate JavaScript in the page."
845
+ },
846
+ {
847
+ "type": "object",
848
+ "properties": {
849
+ "type": {
850
+ "type": "string",
851
+ "const": "SET_VIEWPORT"
852
+ },
853
+ "width": {
854
+ "type": "integer",
855
+ "exclusiveMinimum": 0,
856
+ "maximum": 9007199254740991
857
+ },
858
+ "height": {
859
+ "type": "integer",
860
+ "exclusiveMinimum": 0,
861
+ "maximum": 9007199254740991
862
+ }
863
+ },
864
+ "required": [
865
+ "type",
866
+ "width",
867
+ "height"
868
+ ],
869
+ "additionalProperties": false,
870
+ "description": "Set the page viewport size."
871
+ },
872
+ {
873
+ "type": "object",
874
+ "properties": {
875
+ "type": {
876
+ "type": "string",
877
+ "const": "CLOSE"
878
+ }
879
+ },
880
+ "required": [
881
+ "type"
882
+ ],
883
+ "additionalProperties": false,
884
+ "description": "Close the page."
885
+ }
886
+ ]
887
+ }
888
+ }
889
+ },
890
+ "additionalProperties": false,
891
+ "description": "A population of users: its workers run the steps and actions for the given number of iterations."
892
+ },
893
+ "description": "Scenarios, all started at the same time."
894
+ }
895
+ },
896
+ "required": [
897
+ "scenarios"
898
+ ],
899
+ "additionalProperties": false
900
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * JSON Schema (draft 2020-12) of the YAML configuration, generated from the
3
+ * zod schema used at runtime, so both always agree. Published as
4
+ * `dist/schema.json` for editors (completion, hover documentation, validation).
5
+ */
6
+ export declare const buildJsonSchema: () => Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloom-perf/yaml-pptr",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "A library to generate puppeteer browser controls through a YAML api.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,6 +15,7 @@
15
15
  "@types/chai": "5.2.3",
16
16
  "@types/mocha": "10.0.10",
17
17
  "@types/node": "22.20.5",
18
+ "ajv": "8.20.0",
18
19
  "c8": "12.0.0",
19
20
  "chai": "6.3.0",
20
21
  "esbuild": "0.28.2",
@@ -33,7 +34,7 @@
33
34
  "puppeteer": "^25.0.0"
34
35
  },
35
36
  "scripts": {
36
- "build": "rm -rf dist && node esbuild.js && tsc -p tsconfig.build.json",
37
+ "build": "rm -rf dist && node esbuild.js && tsc -p tsconfig.build.json && tsx scripts/generate-schema.ts",
37
38
  "typecheck": "tsc -p tsconfig.json",
38
39
  "test": "mocha",
39
40
  "test:coverage": "c8 mocha",