Add the resource timings pane (#5)
* First stab at resource timings * pretty numbers * Prettier output, helper methods * Add the pane to the user script * Move some new types to a common place, add a formatter for paths * Formatting fixes and added button titles * Add and update formatter tests * Bad import * Test the new Button.title property * resource-timing refactor with test stubs * Add the remaining tests, plus supporting refactors. - Added more `npm run ...` commands - No more sourcemaps - Update tslint to allow leading underscores in variable names (for unused variables in the mocks) - Remove the unused webpack test config - Rename the units type in Formatter - Fix a minor typing issue in IPanel - Move and update helpers in resource-timing - Add a tsconfig for the tests project so it can be typechecked properly - Fix tslint and tsc type errors in `button.spec.ts`, `formatter.spec.ts`, `toolbar.spec.ts`, `panel.mock.ts`, `navigation-timing-spec.ts` - Add resource timing tests * Update demo * Address CR feedback * Fix some typing errors * Remove typedef rule, fix a compile type error in tests * Improve `npm run check` to be comprehensive * Dropped a default param when cleaning up typedef * Fix keying with enums
This commit is contained in:
Родитель
25ad5d7163
Коммит
acbb97435d
|
@ -3,4 +3,5 @@ dist/
|
|||
*.js
|
||||
*.js.map
|
||||
*.d.ts
|
||||
!injectDemoToolbar.user.js
|
||||
!injectDemoToolbar.user.js
|
||||
!commontypes.d.ts
|
||||
|
|
33
index.html
33
index.html
|
@ -2,6 +2,9 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Demo for WebPerfToolbar</title>
|
||||
<style>
|
||||
body { background-color: lightgray; }
|
||||
</style>
|
||||
<style>
|
||||
/** TODO: These need to be moved into JS (for ease of usage in an app). They're inline in the demo for ease of development. */
|
||||
|
||||
|
@ -9,12 +12,12 @@
|
|||
position:fixed;
|
||||
bottom: 0;
|
||||
left: 50px;
|
||||
border:1px solid black;
|
||||
border-bottom: none;
|
||||
|
||||
list-style:none;
|
||||
padding:0;
|
||||
margin:0;
|
||||
|
||||
z-index: 2147483647; /* we're on top */
|
||||
}
|
||||
#PTB_buttons li {
|
||||
display:inline-block;
|
||||
|
@ -22,6 +25,9 @@
|
|||
margin-left:0.5em;
|
||||
padding:0.2em;
|
||||
cursor:pointer;
|
||||
|
||||
border:1px solid black;
|
||||
border-bottom: none;
|
||||
}
|
||||
#PTB_buttons li:first-child {
|
||||
margin-left:0;
|
||||
|
@ -29,15 +35,20 @@
|
|||
|
||||
#PTB_frame {
|
||||
position:fixed;
|
||||
width:30%;
|
||||
min-width:300px;
|
||||
right:0;
|
||||
left:0;
|
||||
top:0;
|
||||
|
||||
width:100%;
|
||||
height:100%;
|
||||
border-left:1px solid black;
|
||||
overflow:auto;
|
||||
|
||||
padding:0.5em;
|
||||
background:rgba(255, 255, 255, 0.95);
|
||||
z-index:2147483646; /* we're one layer below the top */
|
||||
}
|
||||
|
||||
#PTB_frame table {
|
||||
margin-top:0.5em;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
border: 1px solid black;
|
||||
|
@ -50,6 +61,10 @@
|
|||
border: 1px solid black;
|
||||
padding:0.2em;
|
||||
}
|
||||
|
||||
#PTB_frame .numeric {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -67,11 +82,15 @@
|
|||
(new PerfToolbar.Toolbar([
|
||||
/** Configure this to include the panels you need */
|
||||
{
|
||||
panel: PerfToolbar.NavigationTimingsPanel,
|
||||
panelConstructor: PerfToolbar.NavigationTimingsPanel,
|
||||
config: {
|
||||
goalMs: 25
|
||||
}
|
||||
},
|
||||
{
|
||||
panelConstructor: PerfToolbar.ResourceTimingsPanel,
|
||||
config: {}
|
||||
}
|
||||
/** End configuration */
|
||||
])).render();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
var initFunc = function() {
|
||||
var style = document.createElement("style");
|
||||
style.innerHTML = "#PTB_buttons {position:fixed;bottom: 0;left: 50px;border:1px solid black;border-bottom: none;list-style:none;padding:0;margin:0;}#PTB_buttons li {display:inline-block;line-height:1.6em;margin-left:0.5em;padding:0.2em;cursor:pointer;}#PTB_buttons li:first-child {margin-left:0;}#PTB_frame {position:fixed;width:30%;min-width:300px;right:0;top:0;height:100%;border-left:1px solid black;background:white;z-index:99999;overflow:scroll;}#PTB_frame table {border-collapse: collapse;border-spacing: 0;border: 1px solid black;}#PTB_frame th {font-weight: bold;}#PTB_frame th,#PTB_frame td {border: 1px solid black;padding:0.2em;}";
|
||||
style.innerHTML = "#PTB_buttons {position:fixed;bottom: 0;left: 50px;list-style:none;padding:0;margin:0;z-index: 2147483647; /* we're on top */}#PTB_buttons li {display:inline-block;line-height:1.6em;margin-left:0.5em;padding:0.2em;cursor:pointer;border:1px solid black;border-bottom: none;}#PTB_buttons li:first-child {margin-left:0;}#PTB_frame {position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;padding:0.5em;background:rgba(255, 255, 255, 0.95);z-index:2147483646; /* we're one layer below the top */}#PTB_frame table {margin-top:0.5em;border-collapse: collapse;border-spacing: 0;border: 1px solid black;}#PTB_frame th {font-weight: bold;}#PTB_frame th,#PTB_frame td {border: 1px solid black;padding:0.2em;}#PTB_frame .numeric {text-align: right;}";
|
||||
document.body.appendChild(style);
|
||||
|
||||
var s = document.createElement("script");
|
||||
|
@ -27,10 +27,14 @@
|
|||
(new PerfToolbar.Toolbar([
|
||||
/** Configure this to include the panels you need */
|
||||
{
|
||||
panel: PerfToolbar.NavigationTimingsPanel,
|
||||
panelConstructor: PerfToolbar.NavigationTimingsPanel,
|
||||
config: {
|
||||
goalMs: 25
|
||||
}
|
||||
},
|
||||
{
|
||||
panelConstructor: PerfToolbar.ResourceTimingsPanel,
|
||||
config: {}
|
||||
}
|
||||
/** End configuration */
|
||||
])).render();
|
||||
|
|
13
package.json
13
package.json
|
@ -4,11 +4,20 @@
|
|||
"description": "A toolbar for monitoring web performance.",
|
||||
"main": "dist/bundle.js",
|
||||
"scripts": {
|
||||
"test": "karma start karma.conf.js",
|
||||
"build": "webpack",
|
||||
"test": "karma start karma.conf.js",
|
||||
"test-once": "karma start karma.conf.js --single-run",
|
||||
"demo": "npm run build && http-server -o -c-1",
|
||||
|
||||
"tslint": "tslint -p .",
|
||||
"tsc": "tsc -p tsconfig.json",
|
||||
"tsc-verbose": "tsc -p tsconfig.json --traceResolution true",
|
||||
"tslint": "tslint -p ."
|
||||
|
||||
"test-tslint": "tslint -p test/.",
|
||||
"test-tsc": "tsc -p test/tsconfig.json",
|
||||
"test-tsc-verbose": "tsc -p test/tsconfig.json --traceResolution true",
|
||||
|
||||
"check": "npm run tslint && npm run test-tslint && npm run test-tsc && npm run tsc && npm run test-once"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
|
@ -10,6 +10,9 @@ export interface IButtonConfiguration {
|
|||
/** The panel that owns this button. */
|
||||
parent?: IPanel;
|
||||
|
||||
/** The name of the button, exposed in the title attribute for the button. */
|
||||
title?: string;
|
||||
|
||||
/** Gets the background color for the button. */
|
||||
getColor?(): string;
|
||||
|
||||
|
@ -31,15 +34,19 @@ export class Button {
|
|||
/** The panel that provides this button. */
|
||||
public readonly parent: IPanel | undefined;
|
||||
|
||||
/** The name of the button, exposed in the title attribute for the button. */
|
||||
public readonly title: string;
|
||||
|
||||
/**
|
||||
* Create the button.
|
||||
*/
|
||||
public constructor(config: IButtonConfiguration = {}) {
|
||||
this.title = config.title !== undefined ? config.title : "";
|
||||
this.emoji = config.emoji !== undefined ? config.emoji : "";
|
||||
this.parent = config.parent;
|
||||
/* tslint:disable no-unbound-method */
|
||||
this.getValue = config.getValue !== undefined ? config.getValue : (): string => "";
|
||||
this.getColor = config.getColor !== undefined ? config.getColor : (): string => "";
|
||||
this.getValue = config.getValue !== undefined ? config.getValue : () => "";
|
||||
this.getColor = config.getColor !== undefined ? config.getColor : () => "";
|
||||
/* tslint:enable no-unbound-method */
|
||||
}
|
||||
|
||||
|
@ -50,6 +57,7 @@ export class Button {
|
|||
public render(container: HTMLElement): void {
|
||||
const li: HTMLLIElement = document.createElement("li");
|
||||
li.setAttribute("style", `background-color:${this.getColor()}`);
|
||||
li.setAttribute("title", this.title);
|
||||
li.innerText = `${this.emoji} ${this.getValue()}`;
|
||||
|
||||
li.addEventListener("click", () => {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// begin types from https://github.com/Microsoft/TypeScript/issues/15012
|
||||
|
||||
type Required<T> = {
|
||||
[P in Purify<keyof T>]: NonNullable<T[P]>;
|
||||
};
|
||||
type Purify<T extends string> = {[P in T]: T; }[T];
|
||||
type NonNullable<T> = T & {};
|
||||
|
||||
// end types from https://github.com/Microsoft/TypeScript/issues/15012
|
||||
|
||||
/**
|
||||
* The types we expect from entry.initiatorType.
|
||||
* Values from https://w3c.github.io/resource-timing/#dom-performanceresourcetiming-initiatortype
|
||||
*/
|
||||
type InitiatorType = "link" | "script" | "img" | "iframe" | "css" | "navigation" | "xmlhttprequest" | "fetch" | "beacon" | "other";
|
||||
|
||||
/**
|
||||
* Finish the typings for a file gotten by `performance.getEntriesByType("resource")`
|
||||
*/
|
||||
interface IResourcePerformanceEntry extends PerformanceEntry, PerformanceResourceTiming {
|
||||
decodedBodySize: number;
|
||||
encodedBodySize: number;
|
||||
initiatorType: InitiatorType;
|
||||
transferSize: number;
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
/** The level of precision we want to see for numbers */
|
||||
export const DECIMAL_PLACES: number = 2;
|
||||
|
||||
/** The maximum length of a file name */
|
||||
export const MAX_FILE_NAME_LENGTH: number = 60;
|
||||
|
||||
/**
|
||||
* Formats a duration for output. Makes sure the numbers are valid and only returns a certain number of decimal places.
|
||||
* Invalid input returns a dash.
|
||||
|
@ -8,11 +11,69 @@ export const DECIMAL_PLACES: number = 2;
|
|||
* @param start The start timestamp
|
||||
* @param decimalPlaces The number of decimal places to show.
|
||||
*/
|
||||
export const duration: (end: number, start: number, decimalPlaces?: number) => string =
|
||||
(end: number, start: number, decimalPlaces: number = DECIMAL_PLACES): string => {
|
||||
if (isNaN(end) || isNaN(start)) {
|
||||
export const duration = (end: number, start: number, decimalPlaces: number = DECIMAL_PLACES): string => {
|
||||
if (isNaN(end) ||
|
||||
isNaN(start) ||
|
||||
(end - start < 0) ||
|
||||
(end === 0 && start === 0)) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return (end - start).toFixed(decimalPlaces);
|
||||
return (end - start).toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a path into a filename.
|
||||
* @param path The file name to be formatted.
|
||||
* @param maxLength The maximum length of the returned file name, plus three characters for periods.
|
||||
*/
|
||||
export const pathToFilename = (path: string, maxLength: number = MAX_FILE_NAME_LENGTH): string => {
|
||||
let trimmed: string = path.substring(path.lastIndexOf("/") + 1);
|
||||
|
||||
if (trimmed.length > maxLength) {
|
||||
trimmed = `${trimmed.substring(0, maxLength)}...`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
};
|
||||
|
||||
enum FileSizeUnits {
|
||||
b,
|
||||
Kb,
|
||||
Mb,
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple object for passing to toLocaleString to configure the number of decimal places to display.
|
||||
*/
|
||||
const LOCALE_STRING_DECIMAL_PLACES: { maximumFractionDigits: number; minimumFractionDigits: number } = {
|
||||
minimumFractionDigits: DECIMAL_PLACES,
|
||||
maximumFractionDigits: DECIMAL_PLACES,
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a size in bytes to another size, with a unit.
|
||||
* @param bytes The size in bytes.
|
||||
* @param unit The desired unit to conver to.
|
||||
*/
|
||||
export const sizeToString = (bytes: number, unit: keyof typeof FileSizeUnits = "Kb"): string => {
|
||||
const twoExpTen: number = 1024;
|
||||
|
||||
if (bytes === 0) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
switch (unit) {
|
||||
case FileSizeUnits[FileSizeUnits.b]:
|
||||
return `${bytes.toLocaleString(undefined, LOCALE_STRING_DECIMAL_PLACES)} b`;
|
||||
case FileSizeUnits[FileSizeUnits.Kb]:
|
||||
return `${(bytes / twoExpTen).toLocaleString(undefined, LOCALE_STRING_DECIMAL_PLACES)} Kb`;
|
||||
case FileSizeUnits[FileSizeUnits.Mb]:
|
||||
return `${(bytes / (twoExpTen * twoExpTen)).toLocaleString(undefined, LOCALE_STRING_DECIMAL_PLACES)} Mb`;
|
||||
default:
|
||||
throw new Error("unknown unit");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { Toolbar } from "./toolbar";
|
||||
export { Button } from "./button";
|
||||
export { NavigationTimingsPanel } from "./panels/navigation-timing";
|
||||
export { ResourceTimingsPanel } from "./panels/resource-timing";
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Button } from "./button";
|
||||
import { PanelFrame } from "./panelframe";
|
||||
|
||||
export interface IPanelWithConfiguration<C, P> {
|
||||
export interface IPanelWithConfiguration<C extends IPanelConfig, P extends IPanel> {
|
||||
config: C;
|
||||
panel: IPanelConstructor<C, P>;
|
||||
panelConstructor: IPanelConstructor<C, P>;
|
||||
}
|
||||
|
||||
export interface IPanelConstructor<C, P> {
|
||||
|
@ -17,11 +17,6 @@ export interface IPanelConfig {
|
|||
|
||||
/** Describes a panel within the opened toolbar. */
|
||||
export interface IPanel {
|
||||
/**
|
||||
* The name of the panel.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Gets the buttons provided by this panel to be displayed in the collapsed toolbar.
|
||||
*/
|
||||
|
|
|
@ -5,8 +5,14 @@ import { PanelFrame } from "../panelframe";
|
|||
|
||||
/** Describes the configuration options available for the network panel */
|
||||
export interface INavigationTimingsPanelConfig extends IPanelConfig {
|
||||
/** The emoji for the button */
|
||||
buttonEmoji?: string;
|
||||
/** The title for the button */
|
||||
buttonTitle?: string;
|
||||
/** The goal for the load duration */
|
||||
goalMs: number;
|
||||
/** The name of the panel */
|
||||
panelName?: string;
|
||||
/** The performance timing object, can be included in the config to enable injection of a mock object for testing */
|
||||
timings: PerformanceTiming;
|
||||
}
|
||||
|
@ -15,14 +21,15 @@ export interface INavigationTimingsPanelConfig extends IPanelConfig {
|
|||
const navigationTimingsPanelDefaultConfig: INavigationTimingsPanelConfig = {
|
||||
goalMs: 500,
|
||||
timings: performance.timing,
|
||||
panelName: "Navigation Timings",
|
||||
buttonTitle: "Duration from navigation to end of load event",
|
||||
buttonEmoji: "⏱️",
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides a panel that shows the navigation timings for a page
|
||||
*/
|
||||
export class NavigationTimingsPanel implements IPanel {
|
||||
/** The name of the panel */
|
||||
public readonly name: string = "Navigation Timings";
|
||||
|
||||
/** The settings for this panel. */
|
||||
private readonly config: INavigationTimingsPanelConfig;
|
||||
|
@ -42,9 +49,10 @@ export class NavigationTimingsPanel implements IPanel {
|
|||
public getButtons(): Button[] {
|
||||
return [new Button({
|
||||
parent: this,
|
||||
emoji: "⏱️",
|
||||
getValue: (): string => `${Formatter.duration(this.config.timings.loadEventEnd, this.config.timings.navigationStart)} ms`,
|
||||
getColor: (): string => this.config.timings.loadEventEnd - this.config.timings.navigationStart <= this.config.goalMs ? "green" : "red",
|
||||
title: this.config.buttonTitle,
|
||||
emoji: this.config.buttonEmoji,
|
||||
getValue: () => `${Formatter.duration(this.config.timings.loadEventEnd, this.config.timings.navigationStart)} ms`,
|
||||
getColor: () => this.config.timings.loadEventEnd - this.config.timings.navigationStart <= this.config.goalMs ? "green" : "red",
|
||||
})];
|
||||
}
|
||||
|
||||
|
@ -55,7 +63,7 @@ export class NavigationTimingsPanel implements IPanel {
|
|||
public render(target: HTMLElement): void {
|
||||
const t: PerformanceTiming = this.config.timings;
|
||||
|
||||
target.innerHTML = `
|
||||
target.innerHTML = `<h1>${this.config.panelName}</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Get Connected</th>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
import { IPanelConfig } from "../ipanel";
|
||||
|
||||
/** Describes the configuration for the bytes over wire button. */
|
||||
export interface IBytesOverWireButtonConfig {
|
||||
/** The emoji for the bytes over wire button. */
|
||||
bytesOverWireButtonEmoji?: string;
|
||||
/** The title of the bytes over wire button. */
|
||||
bytesOverWireButtonTitle?: string;
|
||||
}
|
||||
|
||||
/** Describes the configuration for the iamge bytes over wire button. */
|
||||
export interface IImageBytesOverWireButtonConfig {
|
||||
/** The emoji for the image bytes over wire button. */
|
||||
imageBytesOverWireButtonEmoji?: string;
|
||||
/** The title for the image bytes over wire button. */
|
||||
imageBytesOverWireButtonTitle?: string;
|
||||
}
|
||||
|
||||
/** Describes the configuration options available for the network panel */
|
||||
export interface IResourceTimingsPanelConfig extends IPanelConfig, IBytesOverWireButtonConfig, IImageBytesOverWireButtonConfig {
|
||||
/** The name of the panel */
|
||||
panelName?: string;
|
||||
/**
|
||||
* The global performance object, can be included in the config to enable injection of a mock object for testing.
|
||||
* This pane only requires the getEntriesByType method
|
||||
*/
|
||||
performance: Partial<Performance> & Required<{ getEntriesByType(entryType: string): {} }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Types that map to the string from PerformanceResourceTiming.initatorType, with the exception of "all"
|
||||
* which is added to help summarization.
|
||||
* @see https://www.w3.org/TR/resource-timing-1/#dom-performanceresourcetiming-initiatortype
|
||||
* @note These enum values are lowercase rather than uppercase since we string match against the values
|
||||
* provided by the browser API.
|
||||
*/
|
||||
export enum InitiatorTypes {
|
||||
all,
|
||||
other,
|
||||
link,
|
||||
script,
|
||||
img,
|
||||
css,
|
||||
iframe,
|
||||
xmlhttprequest,
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a row in the summary table
|
||||
*/
|
||||
export type SummaryRow = {
|
||||
decodedBytes: number;
|
||||
format: "all" | InitiatorType;
|
||||
largestBytes: number;
|
||||
numFiles: number;
|
||||
overWireBytes: number;
|
||||
};
|
|
@ -0,0 +1,219 @@
|
|||
import { Button } from "../button";
|
||||
import * as Formatter from "../formatter";
|
||||
import { IPanel } from "../ipanel";
|
||||
import { PanelFrame } from "../panelframe";
|
||||
import {
|
||||
IBytesOverWireButtonConfig,
|
||||
IImageBytesOverWireButtonConfig,
|
||||
InitiatorTypes,
|
||||
IResourceTimingsPanelConfig,
|
||||
SummaryRow,
|
||||
} from "./resource-timing-types";
|
||||
|
||||
/** A set of default configuration options for the Resource timings panel */
|
||||
const resourceTimingsPanelDefaultConfig: IResourceTimingsPanelConfig = {
|
||||
performance: window.performance,
|
||||
panelName: "Resource Timings",
|
||||
bytesOverWireButtonTitle: "Total bytes over wire",
|
||||
bytesOverWireButtonEmoji: "🔌",
|
||||
imageBytesOverWireButtonTitle: "Image bytes over wire",
|
||||
imageBytesOverWireButtonEmoji: "🖼️",
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a summary table with default zeroed values.
|
||||
*/
|
||||
export const getZeroedSummaryTable = (): SummaryRow[] => {
|
||||
const zeroValues: SummaryRow = {
|
||||
format: "other",
|
||||
decodedBytes: 0,
|
||||
overWireBytes: 0,
|
||||
numFiles: 0,
|
||||
largestBytes: 0,
|
||||
};
|
||||
|
||||
const numberOfSummaries: number = 8;
|
||||
const summaryCounts: SummaryRow[] = new Array(numberOfSummaries);
|
||||
/* tslint:disable no-any (Because keying into the enum gives a string and not the specific string literal.) */
|
||||
summaryCounts[InitiatorTypes.all] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.all] as any };
|
||||
summaryCounts[InitiatorTypes.other] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.other] as any };
|
||||
summaryCounts[InitiatorTypes.link] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.link] as any };
|
||||
summaryCounts[InitiatorTypes.script] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.script] as any };
|
||||
summaryCounts[InitiatorTypes.img] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.img] as any };
|
||||
summaryCounts[InitiatorTypes.css] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.css] as any };
|
||||
summaryCounts[InitiatorTypes.iframe] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.iframe] as any };
|
||||
summaryCounts[InitiatorTypes.xmlhttprequest] = { ...zeroValues, format: InitiatorTypes[InitiatorTypes.xmlhttprequest] as any };
|
||||
/* tslint:enable no-any */
|
||||
|
||||
return summaryCounts;
|
||||
};
|
||||
|
||||
export const getBytesOverWireButton = (
|
||||
parent: ResourceTimingsPanel | undefined,
|
||||
config: IBytesOverWireButtonConfig,
|
||||
summaryCounts: SummaryRow[],
|
||||
) => new Button({
|
||||
parent,
|
||||
title: config.bytesOverWireButtonTitle,
|
||||
emoji: config.bytesOverWireButtonEmoji,
|
||||
getValue: (): string => `${Formatter.sizeToString(summaryCounts[InitiatorTypes.all].overWireBytes, "Kb")}`,
|
||||
getColor: (): string => "white",
|
||||
});
|
||||
|
||||
export const getImageBytesOverWireButton = (
|
||||
parent: ResourceTimingsPanel | undefined,
|
||||
config: IImageBytesOverWireButtonConfig,
|
||||
summaryCounts: SummaryRow[],
|
||||
) => new Button({
|
||||
parent,
|
||||
title: config.imageBytesOverWireButtonTitle,
|
||||
emoji: config.imageBytesOverWireButtonEmoji,
|
||||
getValue: (): string => `${Formatter.sizeToString(summaryCounts[InitiatorTypes.img].overWireBytes, "Kb")}`,
|
||||
getColor: (): string => "white",
|
||||
});
|
||||
|
||||
/**
|
||||
* Provides a panel that shows the Resource timings for a page.
|
||||
*/
|
||||
export class ResourceTimingsPanel implements IPanel {
|
||||
/** The settings for this panel. */
|
||||
private readonly config: IResourceTimingsPanelConfig;
|
||||
|
||||
/** The frame that displays this panel. */
|
||||
private readonly frame: PanelFrame;
|
||||
|
||||
public constructor(frame: PanelFrame, config?: Partial<IResourceTimingsPanelConfig>) {
|
||||
this.frame = frame;
|
||||
this.config = { ...resourceTimingsPanelDefaultConfig, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the buttons to be displayed
|
||||
* @see IPanel.getButtons
|
||||
*/
|
||||
public getButtons(): Button[] {
|
||||
const summaryCounts: SummaryRow[] = getZeroedSummaryTable();
|
||||
this.populateSummaryTable(summaryCounts);
|
||||
|
||||
return [
|
||||
getBytesOverWireButton(this, this.config, summaryCounts),
|
||||
getImageBytesOverWireButton(this, this.config, summaryCounts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summarized resource data as an array of rows.
|
||||
*/
|
||||
public getSummaryCounts(): SummaryRow[] {
|
||||
const summaryCounts: SummaryRow[] = getZeroedSummaryTable();
|
||||
this.populateSummaryTable(summaryCounts);
|
||||
|
||||
return summaryCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the contents of the panel
|
||||
* @see IPanel.render
|
||||
*/
|
||||
public render(target: HTMLElement): void {
|
||||
target.innerHTML = `<h1>${this.config.panelName}</h1>
|
||||
${this.getSummaryTable(this.getSummaryCounts())}
|
||||
${this.getDetailTable()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the display of this panel.
|
||||
*/
|
||||
public toggle(): void {
|
||||
this.frame.toggle(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the detail table.
|
||||
*/
|
||||
private getDetailTable(): string {
|
||||
const entries: IResourcePerformanceEntry[] = this.config.performance.getEntriesByType("resource") as IResourcePerformanceEntry[];
|
||||
const rows: string = entries.map((entry: IResourcePerformanceEntry) => `
|
||||
<tr>
|
||||
<td>${entry.initiatorType}</td>
|
||||
<td title="${entry.name}">${Formatter.pathToFilename(entry.name)}</td>
|
||||
<td class="numeric">${Formatter.sizeToString(entry.transferSize)}</td>
|
||||
<td class="numeric">${Formatter.sizeToString(entry.decodedBodySize)}</td>
|
||||
<td class="numeric">${Formatter.duration(entry.duration, 0)} ms</td>
|
||||
<td class="numeric">${Formatter.duration(entry.responseStart, entry.fetchStart)} ms</td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
return `
|
||||
<table>
|
||||
<tr>
|
||||
<th>Initiated By</th>
|
||||
<th>Path</th>
|
||||
<th>Bytes Over Wire</th>
|
||||
<th>Actual Size</th>
|
||||
<th>Duration</th>
|
||||
<th>Time To First Byte</th>
|
||||
</tr>
|
||||
${rows}
|
||||
</table>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the summary table.
|
||||
* @param summaryCounts An array of summaries data.
|
||||
*/
|
||||
private getSummaryTable(summaryCounts: SummaryRow[]): string {
|
||||
const rows: string = summaryCounts.map((row: SummaryRow): string => `
|
||||
<tr>
|
||||
<td>${row.format}</td>
|
||||
<td class="numeric">${Formatter.sizeToString(row.decodedBytes)}</td>
|
||||
<td class="numeric">${Formatter.sizeToString(row.overWireBytes)}</td>
|
||||
<td class="numeric">${row.numFiles}</td>
|
||||
<td class="numeric">${Formatter.sizeToString(row.largestBytes)}</td>
|
||||
</tr>`).join("");
|
||||
|
||||
return `
|
||||
<table>
|
||||
<tr>
|
||||
<th>Format</th>
|
||||
<th>Decoded Size</th>
|
||||
<th>Bytes Over Wire</th>
|
||||
<th>Num Files</th>
|
||||
<th>Largest Decoded</th>
|
||||
</tr>
|
||||
${rows}
|
||||
</table>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a row in the summary table.
|
||||
* @param row The row to increment.
|
||||
* @param entry The entry with values that we increment by.
|
||||
*/
|
||||
private incrementCount(row: SummaryRow, entry: IResourcePerformanceEntry): void {
|
||||
row.decodedBytes += entry.decodedBodySize;
|
||||
row.overWireBytes += entry.transferSize;
|
||||
row.numFiles++;
|
||||
row.largestBytes = Math.max(entry.decodedBodySize, row.largestBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a table of summary rows by looping over all of the resource performance entires.
|
||||
* @param summaryCounts The table to be filled.
|
||||
*/
|
||||
private populateSummaryTable(summaryCounts: SummaryRow[]): void {
|
||||
const entries: IResourcePerformanceEntry[] = this.config.performance.getEntriesByType("resource") as IResourcePerformanceEntry[];
|
||||
for (const entry of entries) {
|
||||
// Add to the All count
|
||||
this.incrementCount(summaryCounts[InitiatorTypes.all], entry);
|
||||
|
||||
const index: number = InitiatorTypes[entry.initiatorType as keyof typeof InitiatorTypes];
|
||||
|
||||
if (index as number | undefined !== undefined) {
|
||||
this.incrementCount(summaryCounts[index], entry);
|
||||
} else {
|
||||
this.incrementCount(summaryCounts[InitiatorTypes.other], entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ export class Toolbar {
|
|||
// Construct the frame and the panels that use it
|
||||
const frame: PanelFrame = new PanelFrame(this.toolbarRoot);
|
||||
this.panels = panels.map((panelWithConfig: IPanelWithConfiguration<IPanelConfig, IPanel>): IPanel =>
|
||||
new panelWithConfig.panel(frame, panelWithConfig.config));
|
||||
new panelWithConfig.panelConstructor(frame, panelWithConfig.config));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { assert, expect } from "chai";
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { Button, IButtonConfiguration } from "../src/button";
|
||||
import { Toolbar } from "../src/toolbar";
|
||||
import { assertNotNull } from "./test-utilities";
|
||||
|
||||
describe("Button class", () => {
|
||||
|
||||
|
@ -10,6 +10,7 @@ describe("Button class", () => {
|
|||
const button: Button = new Button();
|
||||
button.render(container);
|
||||
|
||||
expect(button.title).to.equal("");
|
||||
expect(button.emoji).to.equal("");
|
||||
expect(button.getValue()).to.equal("");
|
||||
expect(button.getColor()).to.equal("");
|
||||
|
@ -33,7 +34,7 @@ describe("Button class", () => {
|
|||
|
||||
button.render(container);
|
||||
|
||||
expect(container.firstElementChild.innerHTML)
|
||||
expect(assertNotNull(container.firstElementChild, "Not in DOM").innerHTML)
|
||||
.equals("EMOJI VALUE", "We expect the button to show the label and value we set");
|
||||
});
|
||||
|
||||
|
@ -46,7 +47,19 @@ describe("Button class", () => {
|
|||
|
||||
button.render(container);
|
||||
|
||||
expect(container.firstElementChild.getAttribute("style"))
|
||||
expect(assertNotNull(container.firstElementChild, "Not in DOM").getAttribute("style"))
|
||||
.to.match(/red(;)?$/, "We expect the color to red");
|
||||
});
|
||||
|
||||
it("should render the title as a title attribute", () => {
|
||||
const container: HTMLElement = document.createElement("ul");
|
||||
const config: IButtonConfiguration = {
|
||||
title: "TITLE",
|
||||
};
|
||||
const button: Button = new Button(config);
|
||||
|
||||
button.render(container);
|
||||
|
||||
expect(assertNotNull(container.firstElementChild, "Not in DOM").getAttribute("title")).to.equal("TITLE");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,30 +1,63 @@
|
|||
import { assert, expect } from "chai";
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import * as Formatter from "../src/formatter";
|
||||
|
||||
describe("Formatter class", () => {
|
||||
describe("Formatter", () => {
|
||||
describe("duration", () => {
|
||||
it("should return a dash for invalid input in the first parameter", () => {
|
||||
expect(Formatter.duration(undefined, 0)).to.equal("-");
|
||||
expect(Formatter.duration(undefined as any, 0)).to.equal("-"); // tslint:disable-line:no-any
|
||||
});
|
||||
|
||||
it("should return a dash for invalid input in the second parameter", () => {
|
||||
expect(Formatter.duration(0, undefined)).to.equal("-");
|
||||
expect(Formatter.duration(0, undefined as any)).to.equal("-"); // tslint:disable-line:no-any
|
||||
});
|
||||
|
||||
it("should default to two decimal places", () => {
|
||||
expect(Formatter.duration(0, 0)).to.equal("0.00");
|
||||
expect(Formatter.duration(1, 0)).to.equal("1.00");
|
||||
});
|
||||
|
||||
it("should allow custom number of decimals", () => {
|
||||
expect(Formatter.duration(0, 0, 1)).to.equal("0.0");
|
||||
expect(Formatter.duration(1, 0, 1)).to.equal("1.0");
|
||||
});
|
||||
|
||||
it("should do subtract the start from the end", () => {
|
||||
expect(Formatter.duration(1, 0, 0)).to.equal("1");
|
||||
expect(Formatter.duration(0, 1, 0)).to.equal("-1");
|
||||
});
|
||||
|
||||
it("should not allow negative durations", () => {
|
||||
expect(Formatter.duration(0, 1, 0)).to.equal("-");
|
||||
});
|
||||
|
||||
it("should return - for zero duration", () => {
|
||||
expect(Formatter.duration(0, 0)).to.equal("-");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pathToFilename", () => {
|
||||
it("should return everything after the last slash", () => {
|
||||
expect(Formatter.pathToFilename("https://foo/a.b?c=d")).to.equal("a.b?c=d");
|
||||
});
|
||||
|
||||
it("should trim long file names", () => {
|
||||
expect(Formatter.pathToFilename("https://foo/abc", 1)).to.equal("a...");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sizeToString", () => {
|
||||
const twoToTheTenth: number = 1024;
|
||||
const bigNumber: number = 10000;
|
||||
|
||||
it("should only format when requesting bytes", () => {
|
||||
expect(Formatter.sizeToString(bigNumber, "b")).to.equal("10,000.00 b");
|
||||
});
|
||||
|
||||
it("should reduce 2^10 bytes to 1 Kb", () => {
|
||||
expect(Formatter.sizeToString(twoToTheTenth, "Kb")).to.equal("1.00 Kb");
|
||||
});
|
||||
|
||||
it("should reduce 2^20 bytes to 1 Mb", () => {
|
||||
expect(Formatter.sizeToString(twoToTheTenth * twoToTheTenth, "Mb")).to.equal("1.00 Mb");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,30 +1,42 @@
|
|||
import { Button } from "../../src/button";
|
||||
import { IPanel } from "../../src/ipanel";
|
||||
import { IPanel, IPanelConfig } from "../../src/ipanel";
|
||||
import { PanelFrame } from "../../src/panelframe";
|
||||
|
||||
export interface IMockPanelConfig {
|
||||
/** Describes the mocked panel configuration */
|
||||
export interface IMockPanelConfig extends IPanelConfig {
|
||||
/** The mock requires a getButtons method in the config so the getButtons provided by the class can be replaced */
|
||||
getButtons(): Button[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The default config for the mock panel which just creates a button with no options.
|
||||
*/
|
||||
export const mockPanelConfig: IMockPanelConfig = {
|
||||
getButtons: (): Button[] => [new Button({})],
|
||||
getButtons: () => [new Button({})],
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mocked panel.
|
||||
*/
|
||||
export class MockPanel implements IPanel {
|
||||
/** The name of our mock panel. */
|
||||
public name: string = "Mock Panel";
|
||||
|
||||
public constructor(frame: PanelFrame, config: IMockPanelConfig) {
|
||||
public constructor(_frame: PanelFrame, config: IMockPanelConfig) {
|
||||
this.getButtons = config.getButtons;
|
||||
}
|
||||
|
||||
/** Gets the buttons. Will be overriden by the constructor. */
|
||||
public getButtons(): Button[] {
|
||||
throw new Error("Method must be overridden by passing a new getter in the constructor.");
|
||||
}
|
||||
|
||||
public render(target: HTMLElement): void {
|
||||
/** Normally renders the panel. Not implemented here. */
|
||||
public render(_target: HTMLElement): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
/** Normally toggles the panel. Not implemented here. */
|
||||
public toggle(): void {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
|
|
@ -1,12 +1,41 @@
|
|||
import { assert, expect } from "chai";
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { Button } from "../../src/button";
|
||||
import { PanelConfigMerger } from "../../src/panelconfigmerger";
|
||||
import { PanelFrame } from "../../src/panelframe";
|
||||
import { NavigationTimingsPanel } from "../../src/panels/navigation-timing";
|
||||
|
||||
// Provides mocked performance timings for tests
|
||||
const getMockTimings = (overrides?: Partial<PerformanceTiming>): PerformanceTiming => {
|
||||
const zero: PerformanceTiming = {
|
||||
connectEnd: 0,
|
||||
connectStart: 0,
|
||||
domainLookupEnd: 0,
|
||||
domainLookupStart: 0,
|
||||
domComplete: 0,
|
||||
domContentLoadedEventEnd: 0,
|
||||
domContentLoadedEventStart: 0,
|
||||
domInteractive: 0,
|
||||
domLoading: 0,
|
||||
fetchStart: 0,
|
||||
loadEventEnd: 0,
|
||||
loadEventStart: 0,
|
||||
msFirstPaint: 0,
|
||||
navigationStart: 0,
|
||||
redirectEnd: 0,
|
||||
redirectStart: 0,
|
||||
requestStart: 0,
|
||||
responseEnd: 0,
|
||||
responseStart: 0,
|
||||
secureConnectionStart: 0,
|
||||
unloadEventEnd: 0,
|
||||
unloadEventStart: 0,
|
||||
toJSON: () => "",
|
||||
};
|
||||
|
||||
return { ...zero, ...overrides };
|
||||
};
|
||||
|
||||
describe("Navigation timing panel class", () => {
|
||||
|
||||
it("should provide a single button that is green when below the goal", () => {
|
||||
|
@ -74,35 +103,4 @@ describe("Navigation timing panel class", () => {
|
|||
expect(target.childElementCount).to.be.above(0, "it should render to the DOM");
|
||||
|
||||
});
|
||||
|
||||
// Provides mocked performance timings for tests
|
||||
const getMockTimings: (overrides?: Partial<PerformanceTiming>) => PerformanceTiming =
|
||||
(overrides: Partial<PerformanceTiming> = {}): PerformanceTiming => { // tslint:disable-line:arrow-return-shorthand cyclomatic-complexity
|
||||
return {
|
||||
connectEnd: overrides.connectEnd || 0,
|
||||
connectStart: overrides.connectStart || 0,
|
||||
domainLookupEnd: overrides.domainLookupEnd || 0,
|
||||
domainLookupStart: overrides.domainLookupStart || 0,
|
||||
domComplete: overrides.domComplete || 0,
|
||||
domContentLoadedEventEnd: overrides.domContentLoadedEventEnd || 0,
|
||||
domContentLoadedEventStart: overrides.domContentLoadedEventStart || 0,
|
||||
domInteractive: overrides.domInteractive || 0,
|
||||
domLoading: overrides.domLoading || 0,
|
||||
fetchStart: overrides.fetchStart || 0,
|
||||
loadEventEnd: overrides.loadEventEnd || 0,
|
||||
loadEventStart: overrides.loadEventStart || 0,
|
||||
msFirstPaint: overrides.msFirstPaint || 0,
|
||||
navigationStart: overrides.navigationStart || 0,
|
||||
redirectEnd: overrides.redirectEnd || 0,
|
||||
redirectStart: overrides.redirectStart || 0,
|
||||
requestStart: overrides.requestStart || 0,
|
||||
responseEnd: overrides.responseEnd || 0,
|
||||
responseStart: overrides.responseStart || 0,
|
||||
secureConnectionStart: overrides.secureConnectionStart || 0,
|
||||
unloadEventEnd: overrides.unloadEventEnd || 0,
|
||||
unloadEventStart: overrides.unloadEventStart || 0,
|
||||
toJSON: (): string => "",
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
|
||||
import { Button } from "../../src/button";
|
||||
import { PanelFrame } from "../../src/panelframe";
|
||||
import * as rt from "../../src/panels/resource-timing";
|
||||
import {
|
||||
InitiatorTypes,
|
||||
IResourceTimingsPanelConfig,
|
||||
SummaryRow,
|
||||
} from "../../src/panels/resource-timing-types";
|
||||
|
||||
/** A resource performance entry that is basically zeroes. */
|
||||
const zeroEntry: IResourcePerformanceEntry = {
|
||||
connectEnd: 0,
|
||||
connectStart: 0,
|
||||
decodedBodySize: 0,
|
||||
domainLookupEnd: 0,
|
||||
domainLookupStart: 0,
|
||||
duration: 0,
|
||||
encodedBodySize: 0,
|
||||
entryType: "",
|
||||
fetchStart: 0,
|
||||
initiatorType: "other",
|
||||
name: "",
|
||||
redirectEnd: 0,
|
||||
redirectStart: 0,
|
||||
requestStart: 0,
|
||||
responseEnd: 0,
|
||||
responseStart: 0,
|
||||
startTime: 0,
|
||||
transferSize: 0,
|
||||
};
|
||||
|
||||
/** Provides a short mock of the summary data that would be computed from the performance object. */
|
||||
const getMockSummaryRows = (): SummaryRow[] => {
|
||||
const summary: SummaryRow[] = rt.getZeroedSummaryTable();
|
||||
const allOverWireBytes: number = 1024;
|
||||
const imageOverWireBytes: number = 2048;
|
||||
|
||||
summary[InitiatorTypes.all].overWireBytes = allOverWireBytes;
|
||||
summary[InitiatorTypes.img].overWireBytes = imageOverWireBytes;
|
||||
|
||||
return summary;
|
||||
};
|
||||
|
||||
/* tslint:disable:no-magic-numbers */
|
||||
/**
|
||||
* Get mock entries.
|
||||
* We use prime numbers (greater than two) to make sure they can be added uniquely to verify test cases better.
|
||||
* 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101
|
||||
*/
|
||||
const getMockEntries = (): IResourcePerformanceEntry[] => [
|
||||
{
|
||||
...zeroEntry,
|
||||
initiatorType: "img",
|
||||
decodedBodySize: 3,
|
||||
transferSize: 5,
|
||||
},
|
||||
{
|
||||
...zeroEntry,
|
||||
initiatorType: "img",
|
||||
decodedBodySize: 7,
|
||||
transferSize: 0, // Zero is the transferred size for a cached file
|
||||
},
|
||||
{
|
||||
...zeroEntry,
|
||||
initiatorType: "link",
|
||||
decodedBodySize: 13,
|
||||
transferSize: 17,
|
||||
},
|
||||
];
|
||||
/* tslint:enable:no-magic-numbers */
|
||||
|
||||
describe("Resource timing panel class", () => {
|
||||
|
||||
it("should provide two buttons", () => {
|
||||
const rootElement: HTMLElement = document.createElement("div");
|
||||
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||
const panel: rt.ResourceTimingsPanel = new rt.ResourceTimingsPanel(frame);
|
||||
const expectedButtons: number = 2;
|
||||
|
||||
const buttons: Button[] = panel.getButtons();
|
||||
|
||||
expect(buttons.length).to.equal(expectedButtons, "there should be two buttons");
|
||||
});
|
||||
|
||||
it("should summarize the performance data", () => {
|
||||
const rootElement: HTMLElement = document.createElement("div");
|
||||
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||
const config: IResourceTimingsPanelConfig = {
|
||||
performance: {
|
||||
getEntriesByType: (_entryType: string): IResourcePerformanceEntry[] => getMockEntries(),
|
||||
},
|
||||
};
|
||||
|
||||
const panel: rt.ResourceTimingsPanel = new rt.ResourceTimingsPanel(frame, config);
|
||||
const counts: SummaryRow[] = panel.getSummaryCounts();
|
||||
|
||||
/* tslint:disable:no-magic-numbers */
|
||||
const image: SummaryRow = counts[InitiatorTypes.img];
|
||||
expect(image.decodedBytes).to.equal(3 + 7);
|
||||
expect(image.overWireBytes).to.equal(5);
|
||||
expect(image.numFiles).to.equal(2);
|
||||
expect(image.largestBytes).to.equal(7);
|
||||
|
||||
const link: SummaryRow = counts[InitiatorTypes.link];
|
||||
expect(link.decodedBytes).to.equal(13);
|
||||
expect(link.overWireBytes).to.equal(17);
|
||||
expect(link.numFiles).to.equal(1);
|
||||
expect(link.largestBytes).to.equal(13);
|
||||
|
||||
const other: SummaryRow = counts[InitiatorTypes.other];
|
||||
expect(other.decodedBytes).to.equal(0);
|
||||
expect(other.overWireBytes).to.equal(0);
|
||||
expect(other.numFiles).to.equal(0);
|
||||
expect(other.largestBytes).to.equal(0);
|
||||
/* tslint:enable:no-magic-numbers */
|
||||
});
|
||||
|
||||
it("should render two tables", () => {
|
||||
const rootElement: HTMLElement = document.createElement("div");
|
||||
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||
const panel: rt.ResourceTimingsPanel = new rt.ResourceTimingsPanel(frame);
|
||||
const div: HTMLDivElement = document.createElement("div");
|
||||
const expectedTableCount: number = 2;
|
||||
|
||||
panel.render(div);
|
||||
|
||||
expect(div.querySelectorAll("table").length).to.equal(expectedTableCount);
|
||||
});
|
||||
|
||||
it("should have a total bytes over wire button", () => {
|
||||
const button: Button = rt.getBytesOverWireButton(undefined, {}, getMockSummaryRows());
|
||||
|
||||
expect(button.getValue()).to.equal("1.00 Kb");
|
||||
});
|
||||
|
||||
it("should have an image bytes over wire button", () => {
|
||||
const button: Button = rt.getImageBytesOverWireButton(undefined, {}, getMockSummaryRows());
|
||||
|
||||
expect(button.getValue()).to.equal("2.00 Kb");
|
||||
});
|
||||
|
||||
it("should have zeroes for numeric data in the zero summary table", () => {
|
||||
const zeroed: SummaryRow[] = rt.getZeroedSummaryTable();
|
||||
|
||||
for (const row of zeroed) {
|
||||
expect(row.decodedBytes).to.equal(0);
|
||||
expect(row.overWireBytes).to.equal(0);
|
||||
expect(row.numFiles).to.equal(0);
|
||||
expect(row.largestBytes).to.equal(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should have every format in the zero summary table with the right labels", () => {
|
||||
const zeroed: SummaryRow[] = rt.getZeroedSummaryTable();
|
||||
const numObjectKeysPerEnumValue: number = 2;
|
||||
const numEnumValues: number = Object.keys(InitiatorTypes).length / numObjectKeysPerEnumValue;
|
||||
|
||||
expect(zeroed.length).to.equal(numEnumValues);
|
||||
expect(zeroed[InitiatorTypes.all].format).to.equal("All");
|
||||
expect(zeroed[InitiatorTypes.other].format).to.equal("other");
|
||||
expect(zeroed[InitiatorTypes.link].format).to.equal("link");
|
||||
expect(zeroed[InitiatorTypes.script].format).to.equal("script");
|
||||
expect(zeroed[InitiatorTypes.img].format).to.equal("img");
|
||||
expect(zeroed[InitiatorTypes.css].format).to.equal("css");
|
||||
expect(zeroed[InitiatorTypes.iframe].format).to.equal("iframe");
|
||||
expect(zeroed[InitiatorTypes.xmlhttprequest].format).to.equal("xmlhttprequest");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Throw if an argument is null.
|
||||
* @param arg The value to verify isn't null
|
||||
* @param message The message to throw if the argument is null
|
||||
*/
|
||||
export const assertNotNull = <T>(arg: T | null, message: string) => {
|
||||
if (arg !== null) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
throw new Error(message);
|
||||
};
|
|
@ -1,17 +1,16 @@
|
|||
import { assert, expect } from "chai";
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import { Button } from "../src/button";
|
||||
import { IPanel, IPanelConstructor, IPanelWithConfiguration } from "../src/ipanel";
|
||||
import { IPanel, IPanelConfig, IPanelConstructor, IPanelWithConfiguration } from "../src/ipanel";
|
||||
import { Toolbar } from "../src/toolbar";
|
||||
import { IMockPanelConfig, MockPanel, mockPanelConfig } from "./mock/panel.mock";
|
||||
import { MockPanel, mockPanelConfig } from "./mock/panel.mock";
|
||||
|
||||
describe("Toolbar class", () => {
|
||||
|
||||
it("should construct with no panels", () => {
|
||||
const container: HTMLElement = document.createElement("div");
|
||||
const toolbar: Toolbar = new Toolbar([], container);
|
||||
toolbar.render();
|
||||
|
||||
expect(container.childElementCount).to.equal(1);
|
||||
});
|
||||
|
@ -19,15 +18,19 @@ describe("Toolbar class", () => {
|
|||
it("can render buttons", () => {
|
||||
const container: HTMLElement = document.createElement("div");
|
||||
|
||||
const mockPanelWithConfig: IPanelWithConfiguration<IMockPanelConfig, MockPanel> = {
|
||||
panel: MockPanel,
|
||||
config: mockPanelConfig,
|
||||
const mockPanelWithConfig: IPanelWithConfiguration<IPanelConfig, IPanel> = {
|
||||
config: mockPanelConfig as IPanelConfig,
|
||||
panelConstructor: MockPanel as any as IPanelConstructor<IPanelConfig, IPanel>, // tslint:disable-line:no-any
|
||||
};
|
||||
|
||||
const toolbar: Toolbar = new Toolbar([mockPanelWithConfig], container);
|
||||
|
||||
toolbar.render();
|
||||
|
||||
if (container.firstElementChild === null) {
|
||||
throw new Error("DOM was not set up properly");
|
||||
}
|
||||
|
||||
const expectedList: Element = container.firstElementChild.children.item(0);
|
||||
expect(expectedList).instanceof(HTMLUListElement, "We expect the toolbar to be a list");
|
||||
expect(expectedList.childElementCount).equals(1, "We expect that list to have one item");
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../tsconfig",
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../src/**/*"
|
||||
]
|
||||
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
"noImplicitAny": true,
|
||||
"module": "es2015",
|
||||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"sourceMap": false,
|
||||
"listFiles": true,
|
||||
"baseUrl": "src",
|
||||
"types": [
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
"no-implicit-dependencies": [true, "dev"],
|
||||
"no-import-side-effect": false,
|
||||
"strict-boolean-expressions": [true, "allow-undefined-union"],
|
||||
"interface-over-type-literal": false
|
||||
"interface-over-type-literal": false,
|
||||
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"],
|
||||
"typedef": false
|
||||
},
|
||||
"jsRules": {
|
||||
"max-line-length": {
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
target: 'node',
|
||||
entry: "mocha\!./test/toolbar.spec.ts",
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: '/node_modules/'
|
||||
}
|
||||
],
|
||||
exprContextCritical: false
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
output: {
|
||||
filename: 'testBundle.js',
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче