0.11.0-alpha.2 • Published 3 years ago

puppeteer-scenario v0.11.0-alpha.2

Weekly downloads
30
License
MIT
Repository
github
Last release
3 years ago

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 namedefault valuedescription
name— (required)scenario name to show in logs
screenshot{ takeScreenshot:false }screenshotOptions to configure on-failure screenshots
compareUrlnew RegExp(referenceUrl).test(requestUrl)see interception section

screenshotOptions:

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

namedefault valuedescription
takeScreenshottrueif false, screenshot will not be taken
pathResolversee default filename belowsignature: 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 namedefault valuedescription
pageupdate current puppeteer page, if provided
urlnavigate to url, if provided
scenesetup current scene, if provided (see Scene section for details)
interceptglobal interception rules, see Interception section
contextobject 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 namedefault valuedescription
expect'toEqual'jest matcher name (expectationName)
expectedValuevalue to compare with
evaluationParams[]params that will be passed to scene evaluation
assertionsCount1how 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 namedefault valuedescription
pageglobal.pageinitial 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 namedefault valuedescription
arrangeScenarioscenario 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 namedefault valuedescription
selectorIndex0to query all buttons by selector and click on specified by index
visiblepage.waitForSelector option
hiddenpage.waitForSelector option
waitTimeoutpage.waitForSelector option
buttonpage.click option
clickCountpage.click option
clickDelaypage.click option

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

option namedefault valuedescription
selection: {start, end}select text on input before start to type
typeDelaypage.type option
visiblepage.waitForSelector option
hiddenpage.waitForSelector option
waitTimeoutpage.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

0.11.0-alpha.2

3 years ago

0.11.0-alpha.0

3 years ago

0.11.0-alpha.1

3 years ago

0.10.0

4 years ago

0.9.0

4 years ago

0.8.0

4 years ago

0.7.1

4 years ago

0.7.0

4 years ago

0.5.0

4 years ago

0.6.0

4 years ago

0.5.1

4 years ago

0.4.0

4 years ago

0.3.0

4 years ago

0.2.1

4 years ago

0.2.0

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago