impl pageActions
This commit is contained in:
Родитель
a94bb7d517
Коммит
ac5bae4877
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"type": "minor",
|
"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",
|
"packageName": "flamegrill",
|
||||||
"email": "xgao@microsoft.com",
|
"email": "xgao@microsoft.com",
|
||||||
"commit": "355b574566a201ce7c8e31be9c55472651c85310",
|
"commit": "355b574566a201ce7c8e31be9c55472651c85310",
|
||||||
|
|
|
@ -16,13 +16,22 @@ export interface Scenarios {
|
||||||
[scenarioName: string]: Scenario;
|
[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<void>;
|
||||||
|
|
||||||
|
export interface ScenarioConfig {
|
||||||
outDir?: string;
|
outDir?: string;
|
||||||
tempDir?: string;
|
tempDir?: string;
|
||||||
|
pageActions?: PageActions;
|
||||||
/** Any async operation which will be execute before taking metrics for the profiling page. */
|
}
|
||||||
executeBeforeMeasurement?(page: ProfilePage): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CookResult {
|
export interface CookResult {
|
||||||
profile: ScenarioProfile;
|
profile: ScenarioProfile;
|
||||||
|
@ -58,7 +67,7 @@ export async function cook(scenarios: Scenarios, userConfig?: ScenarioConfig): P
|
||||||
const config = {
|
const config = {
|
||||||
outDir: userConfig && userConfig.outDir ? resolveDir(userConfig.outDir) : process.cwd(),
|
outDir: userConfig && userConfig.outDir ? resolveDir(userConfig.outDir) : process.cwd(),
|
||||||
tempDir: userConfig && userConfig.tempDir ? resolveDir(userConfig.tempDir) : 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);
|
const profiles = await profile(scenarios, config);
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
import { Browser, Page } from 'puppeteer';
|
import { Browser, Page } from 'puppeteer';
|
||||||
|
|
||||||
import { __unitTestHooks, ProfilePage } from '../profile';
|
import { __unitTestHooks, ProfilePage, Profile } from '../profile';
|
||||||
|
import { PageActions, PageActionOptions } from '../../flamegrill';
|
||||||
|
|
||||||
describe('profileUrl', () => {
|
describe('profileUrl', () => {
|
||||||
const { profileUrl } = __unitTestHooks;
|
const { profileUrl } = __unitTestHooks;
|
||||||
const testUrl = 'testUrl';
|
const testUrl = 'testUrl';
|
||||||
const testMetrics = { metric1: 1, metric2: 2 };
|
const testMetrics = { metric1: 1, metric2: 2 };
|
||||||
const testPage: Page = {
|
let 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;
|
|
||||||
|
|
||||||
const testBrowser: Browser = {
|
const testBrowser: Browser = {
|
||||||
newPage: jest.fn(() => {
|
newPage: jest.fn(() => {
|
||||||
|
@ -35,12 +28,44 @@ describe('profileUrl', () => {
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
outdir.removeCallback();
|
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 () => {
|
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.length).toEqual(1);
|
||||||
expect((testPage.setDefaultTimeout as jest.Mock).mock.calls[0][0]).toEqual(0);
|
expect((testPage.setDefaultTimeout as jest.Mock).mock.calls[0][0]).toEqual(0);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import path from 'path';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
import { Browser, Metrics } from 'puppeteer';
|
import { Browser, Metrics } from 'puppeteer';
|
||||||
|
|
||||||
import { Scenarios, ScenarioConfig } from '../flamegrill';
|
import { Scenarios, ScenarioConfig, PageActions } from '../flamegrill';
|
||||||
|
|
||||||
import { arr_diff } from '../util';
|
import { arr_diff } from '../util';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ export type ProfilePage = puppeteer.Page;
|
||||||
|
|
||||||
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
|
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
|
||||||
|
|
||||||
export type ScenarioProfileConfig = Optional<Required<ScenarioConfig>, 'executeBeforeMeasurement'>;
|
export type ScenarioProfileConfig = Optional<Required<ScenarioConfig>, 'pageActions'>;
|
||||||
|
|
||||||
// const extraV8Flags = '--log-source-code --log-timer-events';
|
// const extraV8Flags = '--log-source-code --log-timer-events';
|
||||||
// const extraV8Flags = '--log-source-code';
|
// const extraV8Flags = '--log-source-code';
|
||||||
|
@ -37,7 +37,7 @@ const extraV8Flags = '';
|
||||||
* @param scenarios
|
* @param scenarios
|
||||||
*/
|
*/
|
||||||
export async function profile(scenarios: Scenarios, config: ScenarioProfileConfig): Promise<ScenarioProfiles> {
|
export async function profile(scenarios: Scenarios, config: ScenarioProfileConfig): Promise<ScenarioProfiles> {
|
||||||
const { tempDir, executeBeforeMeasurement } = config;
|
const { tempDir, pageActions } = config;
|
||||||
|
|
||||||
const logFile = path.join(tempDir, '/puppeteer.log');
|
const logFile = path.join(tempDir, '/puppeteer.log');
|
||||||
console.log(`profile logFile: ${logFile}`);
|
console.log(`profile logFile: ${logFile}`);
|
||||||
|
@ -65,10 +65,10 @@ export async function profile(scenarios: Scenarios, config: ScenarioProfileConfi
|
||||||
for (const scenarioName of Object.keys(scenarios)) {
|
for (const scenarioName of Object.keys(scenarios)) {
|
||||||
const scenario = scenarios[scenarioName];
|
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) {
|
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;
|
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} testUrl Base URL supporting 'scenario' and 'iterations' query parameters.
|
||||||
* @param {string} profileName Name of scenario that will be used with baseUrl.
|
* @param {string} profileName Name of scenario that will be used with baseUrl.
|
||||||
* @param {string} logDir Absolute path to output log profiles.
|
* @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.
|
* @returns {string} Log file path associated with test.
|
||||||
*/
|
*/
|
||||||
async function profileUrl(
|
async function profileUrl(
|
||||||
|
@ -95,17 +95,12 @@ async function profileUrl(
|
||||||
testUrl: string,
|
testUrl: string,
|
||||||
profileName: string,
|
profileName: string,
|
||||||
logDir: string,
|
logDir: string,
|
||||||
onPageLoad?: (page: ProfilePage) => Promise<void>
|
pageActions?: PageActions
|
||||||
): Promise<Profile> {
|
): Promise<Profile> {
|
||||||
const logFilesBefore = fs.readdirSync(logDir);
|
const logFilesBefore = fs.readdirSync(logDir);
|
||||||
|
|
||||||
const page = await browser.newPage();
|
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 logFilesAfter = fs.readdirSync(logDir);
|
||||||
|
|
||||||
const testLogFile = arr_diff(logFilesBefore, logFilesAfter);
|
const testLogFile = arr_diff(logFilesBefore, logFilesAfter);
|
||||||
|
@ -121,14 +116,13 @@ async function profileUrl(
|
||||||
console.log(`Starting test for ${profileName} at ${testUrl}`);
|
console.log(`Starting test for ${profileName} at ${testUrl}`);
|
||||||
|
|
||||||
console.time('Ran profile in');
|
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.");
|
console.log("Started executing user-defined page operations.");
|
||||||
await onPageLoad(page);
|
await pageActions(page, { url: testUrl });
|
||||||
console.log("Finished executing user-defined page operations.");
|
console.log("Finished executing user-defined page operations.");
|
||||||
|
} else {
|
||||||
|
await page.goto(testUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.timeEnd('Ran profile in');
|
console.timeEnd('Ran profile in');
|
||||||
|
|
Загрузка…
Ссылка в новой задаче