#!/usr/bin/env node const path = require('path') const { promises: fs, constants: fsConstants } = require('fs') const ttest = require('ttest') const { By, Key } = require('selenium-webdriver') const { withFixtures } = require('./helpers') const { PAGES } = require('./webdriver/driver') const DEFAULT_NUM_SAMPLES = 20 const ALL_PAGES = Object.values(PAGES) async function measurePage (pageName) { let metrics await withFixtures({ fixtures: 'imported-account' }, async ({ driver }) => { const passwordField = await driver.findElement(By.css('#password')) await passwordField.sendKeys('correct horse battery staple') await passwordField.sendKeys(Key.ENTER) await driver.findElement(By.css('.selected-account__name')) await driver.navigate(pageName) await driver.delay(1000) metrics = await driver.collectMetrics() }) return metrics } function calculateResult (calc) { return (result) => { const calculatedResult = {} for (const key of Object.keys(result)) { calculatedResult[key] = calc(result[key]) } return calculatedResult } } const calculateSum = (array) => array.reduce((sum, val) => sum + val) const calculateAverage = (array) => calculateSum(array) / array.length const minResult = calculateResult((array) => Math.min(...array)) const maxResult = calculateResult((array) => Math.max(...array)) const averageResult = calculateResult((array) => calculateAverage(array)) const standardDeviationResult = calculateResult((array) => { const average = calculateAverage(array) const squareDiffs = array.map((value) => Math.pow(value - average, 2)) return Math.sqrt(calculateAverage(squareDiffs)) }) // 95% margin of error calculated using Student's t-distrbution const calculateMarginOfError = (array) => ttest(array).confidence()[1] - calculateAverage(array) const marginOfErrorResult = calculateResult((array) => calculateMarginOfError(array)) async function profilePageLoad (pages, numSamples) { const results = {} for (const pageName of pages) { const runResults = [] for (let i = 0; i < numSamples; i += 1) { runResults.push(await measurePage(pageName)) } if (runResults.some((result) => result.navigation.lenth > 1)) { throw new Error(`Multiple navigations not supported`) } else if (runResults.some((result) => result.navigation[0].type !== 'navigate')) { throw new Error(`Navigation type ${runResults.find((result) => result.navigation[0].type !== 'navigate').navigation[0].type} not supported`) } const result = { firstPaint: runResults.map((result) => result.paint['first-paint']), domContentLoaded: runResults.map((result) => result.navigation[0] && result.navigation[0].domContentLoaded), load: runResults.map((result) => result.navigation[0] && result.navigation[0].load), domInteractive: runResults.map((result) => result.navigation[0] && result.navigation[0].domInteractive), } results[pageName] = { min: minResult(result), max: maxResult(result), average: averageResult(result), standardDeviation: standardDeviationResult(result), marginOfError: marginOfErrorResult(result), } } return results } async function isWritable (directory) { try { await fs.access(directory, fsConstants.W_OK) return true } catch (error) { if (error.code !== 'EACCES') { throw error } return false } } async function getFirstParentDirectoryThatExists (directory) { while (true) { try { await fs.access(directory, fsConstants.F_OK) return directory } catch (error) { if (error.code !== 'ENOENT') { throw error } else if (directory === path.dirname(directory)) { throw new Error('Failed to find parent directory that exists') } directory = path.dirname(directory) } } } async function main () { const args = process.argv.slice(2) let pages = ['home'] let numSamples = DEFAULT_NUM_SAMPLES let outputPath let outputDirectory let existingParentDirectory while (args.length) { if (/^(--pages|-p)$/i.test(args[0])) { if (args[1] === undefined) { throw new Error('Missing pages argument') } pages = args[1].split(',') for (const page of pages) { if (!ALL_PAGES.includes(page)) { throw new Error(`Invalid page: '${page}`) } } args.splice(0, 2) } else if (/^(--samples|-s)$/i.test(args[0])) { if (args[1] === undefined) { throw new Error('Missing number of samples') } numSamples = parseInt(args[1], 10) if (isNaN(numSamples)) { throw new Error(`Invalid 'samples' argument given: '${args[1]}'`) } args.splice(0, 2) } else if (/^(--out|-o)$/i.test(args[0])) { if (args[1] === undefined) { throw new Error('Missing output filename') } outputPath = path.resolve(args[1]) outputDirectory = path.dirname(outputPath) existingParentDirectory = await getFirstParentDirectoryThatExists(outputDirectory) if (!await isWritable(existingParentDirectory)) { throw new Error(`Specified directory is not writable: '${args[1]}'`) } args.splice(0, 2) } else { throw new Error(`Unrecognized argument: '${args[0]}'`) } } const results = await profilePageLoad(pages, numSamples) if (outputPath) { if (outputDirectory !== existingParentDirectory) { await fs.mkdir(outputDirectory, { recursive: true }) } await fs.writeFile(outputPath, JSON.stringify(results, null, 2)) } else { console.log(JSON.stringify(results, null, 2)) } } main() .catch((e) => { console.error(e) process.exit(1) })