From ac5bae48771f3346105c0257619bb33b59ed02c4 Mon Sep 17 00:00:00 2001 From: Xu Gao Date: Fri, 24 Apr 2020 12:53:55 -0700 Subject: [PATCH] impl pageActions --- .vscode/launch.json | 36 +++++++++++++ ...megrill-2020-04-23-17-01-03-xgao-page.json | 2 +- packages/flamegrill/src/flamegrill.ts | 21 +++++--- .../src/profile/__tests__/profile.test.ts | 53 ++++++++++++++----- packages/flamegrill/src/profile/profile.ts | 28 ++++------ 5 files changed, 102 insertions(+), 38 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..de0f77a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Unit Jest Tests", + "cwd": "${workspaceFolder}", + "args": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/.bin/jest", + "--runInBand", + "--watch", + "--config", + "${workspaceRoot}/packages/flamegrill/jest.config.js", + "${fileBasenameNoExtension}" + ], + "windows": { + "args": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/jest/bin/jest.js", + "--runInBand", + "--watch", + "--config", + "${workspaceRoot}/packages/flamegrill/jest.config.js", + "${fileBasenameNoExtension}" + ] + }, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] +} diff --git a/change/flamegrill-2020-04-23-17-01-03-xgao-page.json b/change/flamegrill-2020-04-23-17-01-03-xgao-page.json index 596c605..6578a2c 100644 --- a/change/flamegrill-2020-04-23-17-01-03-xgao-page.json +++ b/change/flamegrill-2020-04-23-17-01-03-xgao-page.json @@ -1,6 +1,6 @@ { "type": "minor", - "comment": "support executing page operations before taking measures/profiling", + "comment": "Support customized page actions and reset default timeout to 30s instead of no timeout", "packageName": "flamegrill", "email": "xgao@microsoft.com", "commit": "355b574566a201ce7c8e31be9c55472651c85310", diff --git a/packages/flamegrill/src/flamegrill.ts b/packages/flamegrill/src/flamegrill.ts index 9300ead..016a692 100644 --- a/packages/flamegrill/src/flamegrill.ts +++ b/packages/flamegrill/src/flamegrill.ts @@ -16,13 +16,22 @@ export interface Scenarios { [scenarioName: string]: Scenario; }; -export type ScenarioConfig = { +export interface PageActionOptions { + /** URL the page will navigate to. */ + url: string; +} + +/** + * Async page operations which will be execute before taking metrics. + * This will override default page operations done by flamegrill before page.metrics(). + */ +export type PageActions = (page: ProfilePage, options: PageActionOptions) => Promise; + +export interface ScenarioConfig { outDir?: string; tempDir?: string; - - /** Any async operation which will be execute before taking metrics for the profiling page. */ - executeBeforeMeasurement?(page: ProfilePage): Promise; -}; + pageActions?: PageActions; +} export interface CookResult { profile: ScenarioProfile; @@ -58,7 +67,7 @@ export async function cook(scenarios: Scenarios, userConfig?: ScenarioConfig): P const config = { outDir: userConfig && userConfig.outDir ? resolveDir(userConfig.outDir) : process.cwd(), tempDir: userConfig && userConfig.tempDir ? resolveDir(userConfig.tempDir) : process.cwd(), - executeBeforeMeasurement: userConfig && userConfig.executeBeforeMeasurement, + pageActions: userConfig && userConfig.pageActions, }; const profiles = await profile(scenarios, config); diff --git a/packages/flamegrill/src/profile/__tests__/profile.test.ts b/packages/flamegrill/src/profile/__tests__/profile.test.ts index 2463be0..1b1b210 100644 --- a/packages/flamegrill/src/profile/__tests__/profile.test.ts +++ b/packages/flamegrill/src/profile/__tests__/profile.test.ts @@ -1,21 +1,14 @@ import * as tmp from 'tmp'; import { Browser, Page } from 'puppeteer'; -import { __unitTestHooks, ProfilePage } from '../profile'; +import { __unitTestHooks, ProfilePage, Profile } from '../profile'; +import { PageActions, PageActionOptions } from '../../flamegrill'; describe('profileUrl', () => { const { profileUrl } = __unitTestHooks; const testUrl = 'testUrl'; const testMetrics = { metric1: 1, metric2: 2 }; - const testPage: Page = { - close: jest.fn(() => Promise.resolve()), - goto: jest.fn(() => { - return Promise.resolve(null); - }), - metrics: jest.fn(() => Promise.resolve(testMetrics)), - setDefaultTimeout: jest.fn(() => {}), - waitForSelector: jest.fn(() => Promise.resolve()), - } as unknown as Page; + let testPage: Page; const testBrowser: Browser = { newPage: jest.fn(() => { @@ -32,15 +25,47 @@ describe('profileUrl', () => { beforeAll(() => { outdir = tmp.dirSync({ unsafeCleanup: true }); }); - + afterAll(() => { outdir.removeCallback(); - }) + }); + beforeEach(() => { + testPage = { + close: jest.fn(() => Promise.resolve()), + goto: jest.fn(() => { + return Promise.resolve(null); + }), + metrics: jest.fn(() => Promise.resolve(testMetrics)), + setDefaultTimeout: jest.fn(() => {}), + waitForSelector: jest.fn(() => Promise.resolve()), + } as unknown as Page; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('performs expected operations', async () => { - const executeBeforeMeasurement = async (page: ProfilePage) => { await page.waitForSelector(testSelector); } + const result = await profileUrl(testBrowser, testUrl, 'testScenario', outdir.name); - const result = await profileUrl(testBrowser, testUrl, 'testScenario', outdir.name, executeBeforeMeasurement); + expect((testPage.goto as jest.Mock).mock.calls.length).toEqual(1); + expect((testPage.goto as jest.Mock).mock.calls[0][0]).toEqual(testUrl); + expect((testPage.close as jest.Mock).mock.calls.length).toEqual(1); + + expect(logfile).toBeDefined(); + expect(result.logFile).toEqual(logfile.name); + expect(result.metrics).toEqual(testMetrics); + }); + + it('performs expected operations when user defined page actions', async () => { + const pageActions: PageActions = async (page: ProfilePage, options: PageActionOptions) => { + await page.setDefaultTimeout(0); + await page.goto(options.url); + await page.waitForSelector(testSelector); + }; + + const result = await profileUrl(testBrowser, testUrl, 'testScenario', outdir.name, pageActions); expect((testPage.setDefaultTimeout as jest.Mock).mock.calls.length).toEqual(1); expect((testPage.setDefaultTimeout as jest.Mock).mock.calls[0][0]).toEqual(0); diff --git a/packages/flamegrill/src/profile/profile.ts b/packages/flamegrill/src/profile/profile.ts index e159112..7060c37 100644 --- a/packages/flamegrill/src/profile/profile.ts +++ b/packages/flamegrill/src/profile/profile.ts @@ -3,7 +3,7 @@ import path from 'path'; import puppeteer from 'puppeteer'; import { Browser, Metrics } from 'puppeteer'; -import { Scenarios, ScenarioConfig } from '../flamegrill'; +import { Scenarios, ScenarioConfig, PageActions } from '../flamegrill'; import { arr_diff } from '../util'; @@ -24,7 +24,7 @@ export type ProfilePage = puppeteer.Page; type Optional = Omit & Partial; -export type ScenarioProfileConfig = Optional, 'executeBeforeMeasurement'>; +export type ScenarioProfileConfig = Optional, 'pageActions'>; // const extraV8Flags = '--log-source-code --log-timer-events'; // const extraV8Flags = '--log-source-code'; @@ -37,7 +37,7 @@ const extraV8Flags = ''; * @param scenarios */ export async function profile(scenarios: Scenarios, config: ScenarioProfileConfig): Promise { - const { tempDir, executeBeforeMeasurement } = config; + const { tempDir, pageActions } = config; const logFile = path.join(tempDir, '/puppeteer.log'); console.log(`profile logFile: ${logFile}`); @@ -65,10 +65,10 @@ export async function profile(scenarios: Scenarios, config: ScenarioProfileConfi for (const scenarioName of Object.keys(scenarios)) { const scenario = scenarios[scenarioName]; - let profileResults: ScenarioProfile = await profileUrl(browser, scenario.scenario, scenarioName, tempDir, executeBeforeMeasurement); + let profileResults: ScenarioProfile = await profileUrl(browser, scenario.scenario, scenarioName, tempDir, pageActions); if (scenario.baseline) { - profileResults.baseline = await profileUrl(browser, scenario.baseline, scenarioName, tempDir, executeBeforeMeasurement); + profileResults.baseline = await profileUrl(browser, scenario.baseline, scenarioName, tempDir, pageActions); } profiles[scenarioName] = profileResults; @@ -87,7 +87,7 @@ export async function profile(scenarios: Scenarios, config: ScenarioProfileConfi * @param {string} testUrl Base URL supporting 'scenario' and 'iterations' query parameters. * @param {string} profileName Name of scenario that will be used with baseUrl. * @param {string} logDir Absolute path to output log profiles. - * @param onPageLoad Async opertaion that is executed after page is loaded. + * @param {PageActions} pageActions Async opertaion that is executed before taking metrics. * @returns {string} Log file path associated with test. */ async function profileUrl( @@ -95,17 +95,12 @@ async function profileUrl( testUrl: string, profileName: string, logDir: string, - onPageLoad?: (page: ProfilePage) => Promise + pageActions?: PageActions ): Promise { const logFilesBefore = fs.readdirSync(logDir); const page = await browser.newPage(); - // Default timeout is 30 seconds. This is good for most tests except for problematic components like DocumentCardTitle. - // Disable timeout for now and tweak to a maximum setting once server conditions are better known. - // TODO: argument? should probably default to 30 seconds - page.setDefaultTimeout(0); - const logFilesAfter = fs.readdirSync(logDir); const testLogFile = arr_diff(logFilesBefore, logFilesAfter); @@ -121,14 +116,13 @@ async function profileUrl( console.log(`Starting test for ${profileName} at ${testUrl}`); console.time('Ran profile in'); - // TODO: consider using or exposing other load finished options: - // https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagegotourl-options - await page.goto(testUrl); - if (onPageLoad) { + if (pageActions) { console.log("Started executing user-defined page operations."); - await onPageLoad(page); + await pageActions(page, { url: testUrl }); console.log("Finished executing user-defined page operations."); + } else { + await page.goto(testUrl); } console.timeEnd('Ran profile in');