Skip to content

Commit

Permalink
[#16] Support INP (#25)
Browse files Browse the repository at this point in the history
* feat: support inp / customer journey tests
  • Loading branch information
cmorten authored Jul 23, 2023
1 parent b153224 commit 6f81620
Show file tree
Hide file tree
Showing 38 changed files with 2,455 additions and 1,255 deletions.
102 changes: 84 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h1 align="center">cypress-web-vitals</h1>
</p>
<p align="center">
A <a href="https://github.com/GoogleChrome/web-vitals">web-vitals</a> command for <a href="https://github.com/cypress-io/cypress">cypress</a>.
A <a href="https://github.com/GoogleChrome/web-vitals">Web Vitals</a> command for <a href="https://github.com/cypress-io/cypress">Cypress</a>.
</p>
<p align="center">
<a href="https://github.com/cmorten/cypress-web-vitals/tags/"><img src="https://img.shields.io/github/tag/cmorten/cypress-web-vitals" alt="Current version" /></a>
Expand All @@ -24,7 +24,11 @@ A <a href="https://github.com/GoogleChrome/web-vitals">web-vitals</a> command fo

[Web Vitals](https://web.dev/vitals/) is a Google initiative which provides a set of quality signals and thresholds that are essential to delivering a great user experience on the web.

`cypress-web-vitals` allows you to test against these signals within your Cypress workflows through a new `cy.vitals()` [custom command](https://docs.cypress.io/api/cypress-api/custom-commands).
`cypress-web-vitals` allows you to test against these signals within your Cypress workflows through a set of [custom commands](https://docs.cypress.io/api/cypress-api/custom-commands):

- `cy.vitals()` - self contained command for performing a Web Vitals audit of page load performance.
- `cy.startVitalsCapture()` - command for starting a journey based Web Vitals audit, enabling capture of [interaction to next paint (INP)](https://web.dev/inp/).
- `cy.reportVitals()` - command for reporting on a journey based Web Vitals audit started with `cy.startVitalsCapture()`.

## Getting started

Expand All @@ -38,7 +42,7 @@ $ yarn add -D cypress-web-vitals cypress-real-events
$ npm install --save-dev cypress-web-vitals cypress-real-events
```

_Note: `cypress-web-vitals` currently makes use of `cypress-real-events` to click the page to calculate [first input delay](https://web.dev/fid/). Hence it is needed as a peer-dependency._
_Note: `cypress-web-vitals` currently makes use of `cypress-real-events` to click the page as a real user would to calculate [first input delay](https://web.dev/fid/). Hence it is needed as a peer-dependency._

### Add the commands

Expand All @@ -51,13 +55,26 @@ import "cypress-web-vitals";
### Write your tests

```js
describe("web-vitals", () => {
it("should pass the audits", () => {
describe("Web Vitals", () => {
it("should pass the audits for a page load", () => {
cy.vitals({ url: "https://www.google.com/" });
});

it("should pass the audits for a customer journey", () => {
cy.startVitalsCapture({
url: "https://www.google.com/",
});

cy.findByRole("combobox", { name: "Search" }).realClick();
cy.findByRole("listbox").should("be.visible");

cy.reportVitals();
});
});
```

_Note: this example is making use of the [Cypress Testing Library](https://testing-library.com/docs/cypress-testing-library/intro) for interacting with the page. This is not required for use of this package nor is it included in the package._

## Examples

Example Cypress test setups with a variety of tests using `cypress-web-vitals` for Cypress 9.x, 10.x, and 12.x are available in the [`./examples` directory](./examples).
Expand All @@ -66,7 +83,7 @@ Example Cypress test setups with a variety of tests using `cypress-web-vitals` f

### `cy.vitals([WebVitalsConfig])`

Performs and audit against the Google web-vitals.
Performs and audit against the Google Web Vitals.

```js
cy.vitals();
Expand All @@ -85,19 +102,63 @@ cy.vitals({ thresholds: { cls: 0.2 } }); // Test the page against against a CLS
- `Optional` **firstInputSelector**: `string | string[]` - Selector(s) for the element(s) to click for capturing FID. Can be a single element selector or an array, all of which will be clicked. For each selector the first matching element is used. Default: `"body"`.
- `Optional` **onReport**: `Function` - Callback for custom handling of the report results, e.g. for sending results to application monitoring platforms.
- `Optional` **strict**: `boolean` - Enables strict mode in which tests will fail if metrics with specified thresholds cannot be calculated.
- `Optional` **thresholds**: `WebVitalsThresholds` - The thresholds to audit the web-vitals against. If not provided Google's 'Good' scores will be used (see below). If provided, any missing web-vitals signals will not be audited.
- `Optional` **thresholds**: `WebVitalsThresholds` - The thresholds to audit the Web Vitals against. If not provided Google's 'Good' scores will be used (see below). If provided, any missing Web Vitals signals will not be audited.
- `Optional` **url**: `string` - The url to audit. If not provided you will need to have called `cy.visit(url)` prior to the command.
- `Optional` **headers**: `string` - Additional headers that will be provided to `cy.visit()` if `url` is provided.
- `Optional` **auth**: `string` - Additional auth that will be provided to `cy.visit()` if `url` is provided.
- `Optional` **vitalsReportedTimeout**: `number` - Time in ms to wait for Web Vitals to be reported before failing. Default: `10000`.

### `cy.startVitalsCapture([StartWebVitalsCaptureConfig])`

Starts an audit against the Google Web Vitals.

```js
cy.startVitalsCapture();
cy.startVitalsCapture(startWebVitalsCaptureConfig);
```

Example:

```js
cy.startVitalsCapture({
url: "https://www.google.com/",
});
```

`StartWebVitalsCaptureConfig`:

- `Optional` **url**: `string` - The url to audit. If not provided you will need to have called `cy.visit(url)` prior to the command.
- `Optional` **headers**: `string` - Additional headers that will be provided to `cy.visit()` if `url` is provided.
- `Optional` **auth**: `string` - Additional auth that will be provided to `cy.visit()` if `url` is provided.
- `Optional` **vitalsReportedTimeout**: `number` - Time in ms to wait for web-vitals to be reported before failing. Default: `10000`.

`WebVitalsThresholds`:
### `cy.reportVitals([ReportWebVitalsConfig])`

Completes and reports on an audit against the Google Web Vitals.

```js
cy.reportVitals();
cy.reportVitals(reportWebVitalsConfig);
```

```js
cy.reportVitals({ thresholds: { inp: 500 } }); // Test the page against against an INP threshold of 500.
```

`ReportWebVitalsConfig`:

- `Optional` **onReport**: `Function` - Callback for custom handling of the report results, e.g. for sending results to application monitoring platforms.
- `Optional` **strict**: `boolean` - Enables strict mode in which tests will fail if metrics with specified thresholds cannot be calculated.
- `Optional` **thresholds**: `WebVitalsThresholds` - The thresholds to audit the Web Vitals against. If not provided Google's 'Good' scores will be used (see below). If provided, any missing Web Vitals signals will not be audited.
- `Optional` **vitalsReportedTimeout**: `number` - Time in ms to wait for Web Vitals to be reported before failing. Default: `10000`.

### `WebVitalsThresholds`

- `Optional` **lcp**: `number` - Threshold for [largest contentful paint (LCP)](https://web.dev/lcp/).
- `Optional` **fid**: `number` - Threshold for [first input delay (FID)](https://web.dev/fid/).
- `Optional` **cls**: `number` - Threshold for [cumulative layout shift (CLS)](https://web.dev/cls/).
- `Optional` **fcp**: `number` - Threshold for [first contentful paint (FCP)](https://web.dev/fcp/).
- `Optional` **ttfb**: `number` - Threshold for [time to first byte (TTFB)](https://web.dev/time-to-first-byte/).
- `Optional` **inp**: `number` - Threshold for [interaction to next paint (INP)](https://web.dev/inp/).

#### Google 'Good' scores

Expand All @@ -107,24 +168,25 @@ cy.vitals({ thresholds: { cls: 0.2 } }); // Test the page against against a CLS
"fid": 100,
"cls": 0.1,
"fcp": 1800,
"ttfb": 600
"ttfb": 600,
"inp": 200
}
```

#### Custom Reporting
### Custom Reporting

It can be useful to have direct access to the raw data so you can generate custom reports, send to APM, etc.

This can be achieved through the optional `onReport` callback which receives the raw report before `cypress-web-vitals` passes or fails the test.
This can be achieved through the optional `onReport` callback for `cy.vitals()` or `cy.reportVitals()` which receives the raw report before `cypress-web-vitals` passes or fails the test.

```js
describe("web-vitals", () => {
describe("Web Vitals", () => {
it("should log the report to APM", () => {
cy.vitals({
url: "https://www.google.com/",
onReport({ results, strict, thresholds }) {
console.log(results); // { lcp: ..., fid: ..., }
}
},
});
});
});
Expand All @@ -134,12 +196,16 @@ The report results contains values for _all signals_, not just the values specif

## How does it work?

1. The url is visited with the HTML response intercepted and modified by Cypress to include the [web-vitals](https://github.com/GoogleChrome/web-vitals#from-a-cdn) module script and some code to record the web-vitals values.
When using the `cy.vitals()` command:

1. The url is visited with the HTML response intercepted and modified by Cypress to include the [Web Vitals](https://github.com/GoogleChrome/web-vitals#from-a-cdn) module script and some code to record the Web Vitals values.
1. The audit then waits for the page load event to allow for the values of LCP and CLS to settle; which are subject to change as different parts of the page load into view.
1. Several elements (if exist) starting with the provided element(s) (based on `firstInputSelector`) are then clicked in quick succession to simulate a user clicking on the page to aid FID reporting. Note: if choosing a custom element(s), don't pick something that will navigate away from the page otherwise the plugin will fail to capture the web-vitals metrics.
1. Several elements (if exist) starting with the provided element(s) (based on `firstInputSelector`) are then clicked in quick succession to simulate a user clicking on the page to aid FID reporting. Note: if choosing a custom element(s), don't pick something that will navigate away from the page otherwise the plugin will fail to capture the Web Vitals metrics.
1. Next the audit simulates a page visibility state change [which is required for the CLS web-vital to be reported](https://www.npmjs.com/package/web-vitals#basic-usage).
1. The audit then attempts to wait for any outstanding web-vitals to be reported for which thresholds have been provided.
1. Finally the web-vitals values are compared against the thresholds, logging successful results and throwing an error for any unsuccessful signals. Note: if the audit was unable to record a web-vital then it is logged, _but the test will not fail_.
1. The audit then attempts to wait for any outstanding Web Vitals to be reported for which thresholds have been provided.
1. Finally the Web Vitals values are compared against the thresholds, logging successful results and throwing an error for any unsuccessful signals. Note: if the audit was unable to record a web-vital then it is logged, _but the test will not fail_.

The `cy.startVitalsCapture()` and `cy.reportVitals()` commands perform steps 1-2 and 4-6 respectively. There is no clicking of elements (step 3) with these commands as the expectation is for you to provide your own customer journey interactions.

## Contributing

Expand Down
6 changes: 3 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Security Policies and Procedures

This document outlines security procedures and general policies for the cypress-web-vitals
This document outlines security procedures and general policies for the `cypress-web-vitals`.
project.

- [Reporting a Bug](#reporting-a-bug)
Expand All @@ -9,8 +9,8 @@ project.

## Reporting a Bug

The cypress-web-vitals maintainers and community take all security bugs in cypress-web-vitals seriously.
Thank you for improving the security of cypress-web-vitals. We appreciate your efforts and
The `cypress-web-vitals` maintainers and community take all security bugs in `cypress-web-vitals` seriously.
Thank you for improving the security of `cypress-web-vitals`. We appreciate your efforts and
responsible disclosure and will make every effort to acknowledge your
contributions.

Expand Down
68 changes: 55 additions & 13 deletions commands.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ declare namespace Cypress {
* @see https://web.dev/time-to-first-byte/
*/
ttfb?: number;

/**
* Interaction to next paint.
* @see https://web.dev/inp/
*/
inp: number | null;
}

interface WebVitalsResults {
Expand Down Expand Up @@ -63,6 +69,12 @@ declare namespace Cypress {
* @see https://web.dev/time-to-first-byte/
*/
ttfb: number | null;

/**
* Interaction to next paint.
* @see https://web.dev/inp/
*/
inp: number | null;
}

interface WebVitalsReport {
Expand All @@ -71,14 +83,21 @@ declare namespace Cypress {
results: WebVitalsResults;
}

interface WebVitalsConfig {
interface StartWebVitalsCaptureConfig {
/**
* Selector(s) used for capturing FID. The first matching element is
* used for each provided selector.
* @default "body"
* Url of page for auditing Web Vitals. If not provided will
* attempt to discover the current url using `cy.url()`.
*/
firstInputSelector?: string | string[];
url?: string;

/**
* Additional parameters to pass to internal cy.visit() command when visit
* the url.
*/
[visitKey: string]: unknown;
}

interface ReportWebVitalsConfig {
/**
* Callback for custom handling of the report results, e.g. for
* sending results to application monitoring platforms.
Expand All @@ -93,30 +112,53 @@ declare namespace Cypress {
strict?: boolean;

/**
* Thresholds to compare against captured web-vitals metrics.
* Thresholds to compare against captured Web Vitals metrics.
*/
thresholds?: WebVitalsThresholds;

/**
* Url of page for auditing web-vitals. If not provided will
* attempt to discover the current url using `cy.url()`.
* Time in ms to wait for Web Vitals to be reported before failing.
* @default 10000
*/
url?: string;
vitalsReportedTimeout?: number;
}

interface WebVitalsConfig
extends StartWebVitalsCaptureConfig,
ReportWebVitalsConfig {
/**
* Time in ms to wait for web-vitals to be reported before failing.
* @default 10000
* Selector(s) used for capturing FID. The first matching element is
* used for each provided selector.
* @default "body"
*/
vitalsReportedTimeout?: number;
firstInputSelector?: string | string[];
}

interface Chainable<Subject> {
/**
* Runs a web-vitals audit
* Runs a Web Vitals audit
* @example
* cy.vitals({ thresholds, url, selector });
* @param {WebVitalsConfig} webVitalsConfig configuration
*/
vitals(webVitalsConfig?: WebVitalsConfig);

/**
* Starts a Web Vitals audit. Use with `cy.reportVitals()`.
* @example
* cy.startVitalsCapture({ url });
* @param {StartWebVitalsCaptureConfig} startWebVitalsCaptureConfig configuration
*/
startVitalsCapture(
startWebVitalsCaptureConfig: StartWebVitalsCaptureConfig
);

/**
* Reports on a Web Vitals audit. Use with `cy.startVitalsCapture()`.
* @example
* cy.reportVitals({ thresholds });
* @param {ReportWebVitalsConfig} reportWebVitalsConfig configuration
*/
reportVitals(reportWebVitalsConfig: ReportWebVitalsConfig);
}
}
2 changes: 1 addition & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ For the latest changes, please refer to the [releases page](https://github.com/c
- fix: `visibilityState` changes now simulated through stubbing the document as `window.open()` does not work with headless Cypress.
- docs: added section to readme on how the command works internally.
- feat: now wait for page load to allow LCP and CLS to change and settle.
- feat: now attempt to wait for all desired web-vitals to have been reported before calculating the results.
- feat: now attempt to wait for all desired Web Vitals to have been reported before calculating the results.

## [0.2.0] - 26-06-2021

Expand Down
2 changes: 1 addition & 1 deletion examples/10.x/cypress/e2e/custom.cy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
describe("cy.vitals() command not using the defaults", () => {
it("should meet the custom provided web-vitals thresholds", () => {
it("should meet the custom provided Web Vitals thresholds", () => {
cy.vitals({
url: "https://www.google.com/",
firstInputSelector: "body",
Expand Down
2 changes: 1 addition & 1 deletion examples/10.x/cypress/e2e/defaults.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ describe("cy.vitals() command using the defaults", () => {
cy.visit("https://www.google.com/");
});

it("should meet the web-vitals default thresholds", () => {
it("should meet the Web Vitals default thresholds", () => {
cy.vitals();
});
});
6 changes: 3 additions & 3 deletions examples/10.x/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "cypress-web-vitals-examples-10.x",
"version": "1.0.0",
"description": "web-vitals command examples for Cypress 10.x",
"description": "Web Vitals command examples for Cypress 10.x",
"author": "Craig Morten <craig.morten@hotmail.co.uk>",
"license": "MIT",
"main": "index.js",
Expand All @@ -10,7 +10,7 @@
"cy:run": "cypress run --headless --browser chrome"
},
"devDependencies": {
"cypress": "^10.3.0",
"cypress-real-events": "^1.7.6"
"cypress": "^10.11.0",
"cypress-real-events": "^1.9.1"
}
}
Loading

0 comments on commit 6f81620

Please sign in to comment.