Skip to content

Commit

Permalink
Merge pull request #38 from Automattic/add/status-codes
Browse files Browse the repository at this point in the history
Add status code checks while generating critical CSS using playwright
  • Loading branch information
thingalon authored May 17, 2023
2 parents 3e0632a + 075a6d0 commit c9cf024
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 34 deletions.
24 changes: 4 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"node-fetch": "^3.2.10",
"os-browserify": "^0.3.0",
"path-browserify": "^1.0.1",
"playwright": "^1.25.1",
"playwright-core": "^1.33.0",
"prettier": "npm:[email protected]",
"puppeteer": "^16.2.0",
"rollup": "^2.78.1",
Expand Down
89 changes: 79 additions & 10 deletions src/browser-interface-playwright.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,102 @@
import { Viewport } from './types';
import { BrowserInterface, BrowserRunnable, FetchOptions } from './browser-interface';
import { BrowserContext, Page } from 'playwright-core';
import { objectPromiseAll } from './object-promise-all';

interface Page {
setViewportSize( viewport: Viewport ): Promise< void >;
// eslint-disable-next-line @typescript-eslint/ban-types
evaluate( method: string | Function, arg: Record< string, unknown > );
}
export type Tab = { page: Page; statusCode: number | null };
export type TabsByUrl = { [ url: string ]: Tab };

const PAGE_GOTO_TIMEOUT_MS = 5 * 60 * 1000;

export class BrowserInterfacePlaywright extends BrowserInterface {
constructor( private pages: { [ url: string ]: Page } ) {
private tabs: TabsByUrl;

/**
*
* @param context The playwright browser context to work with.
* @param urls Array of urls to evaluate. The reason we are taking this as an argument is because we want to load all of them in parallel.
*/
constructor( private context: BrowserContext, private urls: string[] ) {
super();
}

private async getTabs() {
if ( typeof this.tabs === 'undefined' ) {
await this.openUrls( this.context, this.urls );
}

return this.tabs;
}

/**
* Open an array of urls in a new browser context.
*
* Take a browser instance and an array of urls to open in new tabs.
*
* @param {BrowserContext} context - Browser context to use.
* @param {string[]} urls - Array of urls to open.
* @return {Promise< TabsByUrl >} Promise resolving to the browser context.
*/
private async openUrls( context: BrowserContext, urls: string[] ): Promise< void > {
this.tabs = await objectPromiseAll< Tab >(
urls.reduce( ( set, url ) => {
set[ url ] = this.newTab( context, url );
return set;
}, {} )
);
}

/**
* Open url in a new tab in a given browserContext.
*
* @param {BrowserContext} browserContext - Browser context to use.
* @param {string} url - Url to open.
* @return {Promise<Page>} Promise resolving to the page instance.
*/
private async newTab( browserContext: BrowserContext, url: string ): Promise< Tab > {
const tab = {
page: await browserContext.newPage(),
statusCode: null,
};
tab.page.on( 'response', async response => {
if ( response.url() === url ) {
tab.statusCode = response.status();
}
} );

await tab.page.goto( url, { timeout: PAGE_GOTO_TIMEOUT_MS } );

return tab;
}

async runInPage< ReturnType >(
pageUrl: string,
viewport: Viewport | null,
method: BrowserRunnable< ReturnType >,
...args: unknown[]
): Promise< ReturnType > {
const page = this.pages[ pageUrl ];
const tabs = await this.getTabs();
const tab = tabs[ pageUrl ];

if ( ! page ) {
if ( ! tab || ! tab.page ) {
throw new Error( `Playwright interface does not include URL ${ pageUrl }` );
}

// Bail early if the page returned a non-200 status code.
if ( ! tab.statusCode || ! this.isOkStatus( tab.statusCode ) ) {
const error = new Error( `Page returned status code ${ tab.statusCode }` );
this.trackUrlError( pageUrl, error );
throw error;
}

if ( viewport ) {
await page.setViewportSize( viewport );
await tab.page.setViewportSize( viewport );
}

// The inner window in Playwright is the directly accessible main window object.
// The evaluating method does not need a separate window object.
// Call inner method within the Playwright context.
return page.evaluate( method, { innerWindow: null, args } );
return tab.page.evaluate( method, { innerWindow: null, args } );
}

/**
Expand All @@ -47,4 +112,8 @@ export class BrowserInterfacePlaywright extends BrowserInterface {

return nodeFetch.default( url, options );
}

private isOkStatus( statusCode: number ) {
return statusCode >= 200 && statusCode < 300;
}
}
6 changes: 3 additions & 3 deletions src/browser-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export class BrowserInterface {
* Context-specific wrapper for fetch; uses window.fetch in browsers, or a
* node library when using Puppeteer.
*
* @param _url
* @param _options
* @param _role
* @param _url
* @param _options
* @param _role
*/
async fetch(
_url: string,
Expand Down
18 changes: 18 additions & 0 deletions src/object-promise-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Given an object full of promises, resolves all of them and returns an object containing resultant values.
* Roughly equivalent of Promise.all, but applies to an object.
*
* @param { Object } object - containing promises to resolve
* @return { Object } - Promise which resolves to an object containing resultant values
*/
export async function objectPromiseAll< ValueType >( object: {
[ key: string ]: Promise< ValueType >;
} ): Promise< { [ key: string ]: ValueType } > {
const keys = Object.keys( object );
const values = await Promise.all( keys.map( key => object[ key ] ) );

return keys.reduce( ( acc, key, index ) => {
acc[ key ] = values[ index ];
return acc;
}, {} as { [ key: string ]: ValueType } );
}

0 comments on commit c9cf024

Please sign in to comment.