npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

puppeteer-scenario

v0.11.0-alpha.2

Published

Small lib that helps write declarative tests with puppeteer

Downloads

26

Readme

npm

Install

npm i -D puppeteer-scenario

Description

Allow writing declarative and reusable scenarios for tests in puppeteer in AAA (Arrange-Act-Assert) pattern.

Idea is, that tests decomposed into two parts. High-level "scenario" part, where intentions are described. And low-level "scenes", where the actual puppeteer manipulations are performed

Example

describe("user scenarios", () => {
  it("should login, create trip, visits and rides", async () => {
    return new Scenario(page)
      .arrange({
        scene: LoginScene,
        url: "http://localhost:8080/mine/hello"
      })
      .act("login", {
        login: process.env.TEST_LOGIN,
        password: process.env.TEST_PASSWORD
      })
      .assert(evaluate(() => window.isAuthenticated), {
        expect: { toBe: true }
      })

      .arrange({ scene: VisitsScene, userLogin: process.env.TEST_LOGIN })
      .act("clickCreateTrip")

      .arrange({ scene: CreateTripScene })
      .act("createTrip")

      .arrange({ scene: EditTripScene })
      .act("createVisit")
      .act("createVisit")
      .act("createRide")
      .act("createRide")
      .act("createRide")
      .assert(
        async () => {
          expect(
            await page.$$(toSelector(tripEditPageLocators.VISIT_BLOCK))
          ).toHaveLength(2);
          expect(
            await page.$$(toSelector(tripEditPageLocators.RIDE_BLOCK))
          ).toHaveLength(3);
        },
        { assertionsCount: 2 }
      )

      .play();
  });
});

// VisitsScene.js, for example
export default class VisitsScene extends Scene {
  async arrange({ userLogin }) {
    await this.page.goto(
      `${process.env.APP_ORIGIN}/mine/travel/${userLogin}/visits/trips`,
      { waitUntil: "networkidle2" }
    );
  }

  async clickCreateTrip() {
    const addTripButtonSelector = toSelector(
      visitsPageLocators.ADD_TRIP_BUTTON
    );

    await this.page.waitFor(addTripButtonSelector);
    await this.page.click(addTripButtonSelector);
  }
}

Check more examples

Usage

import { Scenario } from "puppeteer-scenario";

describe("MyScenario", () => {
  it("should behave well", () => {
    return new Scenario({ name: "nameForLogs" })
      .include("...")

      .arrange("...")
      .act("...")
      .assert("...")

      .play("...");
  });
});

constructor options:

| option name | default value | description | | ----------- | ----------------------------------------- | ----------------------------------------------------- | | name | — (required) | scenario name to show in logs | | screenshot | { takeScreenshot:false } | screenshotOptions to configure on-failure screenshots | | compareUrl | new RegExp(referenceUrl).test(requestUrl) | see interception section |

screenshotOptions:

Has shortcut: true, equals to { takeScreenshot:true }

| name | default value | description | | -------------- | -------------------------- | ------------------------------------------------------------- | | takeScreenshot | true | if false, screenshot will not be taken | | pathResolver | see default filename below | signature: pathResolver(context, { scenarioName, sceneName }) | | ...rest | {} | options that passed to puppeteer page.screenshot(options) |

Default file name for screenshots: .screenshots/${scenarioName}__${sceneName}__${uniqKey}.png

API

include

include(otherScenario) — copy all steps from other scenario to the current one (in place of the current step). Useful to include authorization steps. The other scenario remains unchanged

arrange

arrange(options) — prepare page for test

available options:

| option name | default value | description | | ------------------ | ------------- | ---------------------------------------------------------------- | | page | — | update current puppeteer page, if provided | | url | — | navigate to url, if provided | | scene | — | setup current scene, if provided (see Scene section for details) | | intercept | — | global interception rules, see Interception section | | context | — | object of key/value pairs to populate scenario context | | ...sceneProperties | {} | params that forwarder to Scene instance arrange method |

act

act(actionName, ...actionArgs) — perform low-level action, that coded inside Scene class. actionName is the method name of Scene instance, args are arguments that will be passed to this method

assert

assert(evaluation, options) — place to make assertions

evaluation could be function (callback), string, object, array or postponedValue

available options:

| option name | default value | description | | ---------------- | ------------- | --------------------------------------------------------------------------------------------------- | | expect | 'toEqual' | jest matcher name (expectationName) | | expectedValue | — | value to compare with | | evaluationParams | [] | params that will be passed to scene evaluation | | assertionsCount | 1 | how much assertions made by callback,required for callback, calculated automatically in other cases |

evaluation the evaluation has 3 cases of resolution: — if it is an object, array, or postponed value it remains as is — if it is a string, then current scene will be addressed, it evaluations[evaluation] method will be called, and return value will be used — if it is function, then it will be called with ({page, scene, context}) => {/*...*/} signature, and return value will be used NOTE: in last case, assertionsCount parameter is required. Because otherwise it's to easy to forget about it. And in that case tests could be successful, just because jest expect less assertions, that exist in fact

All postponed values and promises will be resolved in the return value, on all nested levels for array or object (doesn't matter if it's a primitive or complex object). And the result passed to jest "expect" check as in example:

expect(resolvedEvaluation)[expectationName](expectedValue);

play

play(options) — perform scenario and call all async actions

available options:

| option name | default value | description | | ----------- | ------------- | ---------------------- | | page | global.page | initial puppeteer page |

Scene

Scene is a representation of an application page (a view, not a puppeteer one) in the form of a class written by users of the library. Scene has such structure:

export default class MyScene extends Scene {
  intercept = {
    regexp: request => ({
      /* puppeteer response https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#httprequestrespondresponse */
    })
  };

  evaluations = {
    // see "evaluation" section
    bodyInnerHTML: evalSelector("body", body => body.innerHTML)
  };

  // optional method, if present, will be called automatically after arrange call with new Scene in scenario
  async arrange(sceneProperties) {}

  async myMethod(...actionArgs) {
    // use this.page to execute puppeteer commands here
  }
}

Also base Scene class provides helpful utils. — this.click — this.type — this.batchType

Detailed information could be found in utils section

Check example

Library also exports a very simple Scene class, that just assign page and context fields in constructor. It possible to inherit Scenes from it, but not required. Source

Context

Context is key-value in-memory storage, that could be used to pass some data through steps

Interception

puppeteer-scenario use puppeteer page "request" event to subscribe and intercept requests. The usual purposes are to mock requests or to simulate the erroneous response

interceptions could be set by passing interceptions config in two ways:

  1. "global" for the scenario, through .arrange({ intercept }) parameter. Interceptions added this way will work till the scenario end, if not overridden by further ones with the same keys. I.e. each next "global" config will be merged into existing
  2. "local" for the scene. These interceptions are set in scene instance "intercept" field (see scene example). And works only for the scene where it was set. After scene change, such interceptions will be removed. I.e. each next "local" config will be substitute existing one

"local" interceptions have precedence over "global"

interceptions config is an object, which keys is representing URL and values:

const interceptionsConfig = {
  "/api/request/": function interceptionFn(
    // https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#class-httprequest
    request,
    // puppeteer-scenario context, see "context" section
    context
  ) {
    return {
      content: "application/json",
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ value: 32 })
      // ...response
      // https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#httprequestrespondresponse
    };
  }
};

interception will be ignored if the function returns a null or undefined value

interception keys by default is treated as regexp, used to compare with requested urls:

(requestUrl, referenceUrl) => new RegExp(referenceUrl).test(requestUrl),

This behavior could be overridden by compareUrl param in Scenario constructor

Advanced

Scenario preset

It is possible to make scenario preset:

const MyScenarioConstructor = Scenario.preset({
  arrangeScenario,
  ...scenarioOptions
});

| option name | default value | description | | --------------- | ------------- | ---------------------------------------------------------------------------- | | arrangeScenario | — | scenario to include by default can be convenient for authorization | | scenarioOptions | {} | options applicable to Scenario constructor (see constructor options section) |

Options passed as scenarioOptions will be treated as default values.

Note: instances created by Scenario preset nevertheless are instances of Scenario class:

const scenario = new MyScenarioConstructor() ;
console.log(scenario instanceof Scenario) // true

Postponed values

Postponed values are a mechanism that helps to write simple and concise references to often required evaluations, such as context values or page evaluations. Postponed values can be used inside expect.arrayContaining and expect.objectContaining objects

import {
  contextValue,
  evaluate,
  evalSelector,
  evalSelectorAll
} from "puppeteer-scenario";

new Scenario("test")
  .arrange({ context: { requiredBonus: "health" } })
  .act(/*...*/)
  .assert(contextValue("myContextValue"), { expectedValue: "value" })
  .assert(evaluate(() => window.location.host), { expectedValue: "google.com" })
  .assert(evalSelector("body", body => body.innerHTML), {
    expectedValue: "hello"
  })
  .assert(evalSelectorAll(".player"), {
    expect: "toHaveLength",
    expectedValue: 4
  })
  .assert(evalSelector(".bonus"), {
    expectedValue: expect.arrayContaining([contextValue("requiredBonus")])
  });

Utils

import { click, type, batchType } from "puppeteer-scenario";

click(page, ".selector");

or

import { Scene } from "puppeteer-scenario";

class MyScene extends Scene {
  async myMethod() {
    await this.click(".selector");
    await this.type(".selector input", "abc");
  }
}

— click(page, selector, options) — will be wait for element and click on it

| option name | default value | description | | ------------- | ------------- | ---------------------------------------------------------------- | | selectorIndex | 0 | to query all buttons by selector and click on specified by index | | visible | | page.waitForSelector option | | hidden | | page.waitForSelector option | | waitTimeout | | page.waitForSelector option | | button | | page.click option | | clickCount | | page.click option | | clickDelay | | page.click option |

— type(page, selector, value, options) — will be wait for element and type

| option name | default value | description | | ----------------------- | ------------- | ----------------------------------------- | | selection: {start, end} | — | select text on input before start to type | | typeDelay | | page.type option | | visible | | page.waitForSelector option | | hidden | | page.waitForSelector option | | waitTimeout | | page.waitForSelector option |

— batchType(page, batchParams, options) — will be wait and type for array of elements

batchParams is array of shape { selector, value, options }, which is passed to type util both options is merged, with precedence of options from batchParam