Add the Navigation Timing panel
Provides the navigation timing toolbar. This records key moments in the page load experience. Additionally: Updates Karma config to capture tests in folders Updates the demo to have styles and use a real panel Gives buttons a click action and a parent Adds Formatter to handle formatting durations Adds a few extra interfaces to handle panels being constructed with configurations Adds PanelConfigMerger to merge user configs and default configs Adds PanelFrame to hold and show/hide panels Updates Toolbar to construct from panel classes and configurations Cleans up a bug where the wrong container was cleared in the toolbar render Removes a few tslint rules that were causing more problems than they were worth
This commit is contained in:
Коммит
ecea25b431
83
index.html
83
index.html
|
@ -2,6 +2,55 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Demo for WebPerfToolbar</title>
|
<title>Demo for WebPerfToolbar</title>
|
||||||
|
<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. */
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script defer>
|
<script defer>
|
||||||
|
@ -15,34 +64,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
var startFunc = function() {
|
var startFunc = function() {
|
||||||
/** Fake a panel with a single button */
|
|
||||||
var fakePanel = {
|
|
||||||
getButtons: function () {
|
|
||||||
return [new PerfToolbar.Button({
|
|
||||||
emoji: '#',
|
|
||||||
getValue: function () { return '1'},
|
|
||||||
getColor: function () { return 'gray'}
|
|
||||||
})];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Fake another panel with a single button */
|
|
||||||
var fakePanel2 = {
|
|
||||||
getButtons: function () {
|
|
||||||
return [new PerfToolbar.Button({
|
|
||||||
emoji: '#',
|
|
||||||
getValue: function () { return '2'},
|
|
||||||
getColor: function () { return 'lightgray'}
|
|
||||||
})];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Configure this to include the panels you need */
|
|
||||||
(new PerfToolbar.Toolbar([
|
(new PerfToolbar.Toolbar([
|
||||||
fakePanel,
|
/** Configure this to include the panels you need */
|
||||||
fakePanel2
|
{
|
||||||
])).render();
|
panel: PerfToolbar.NavigationTimingsPanel,
|
||||||
|
config: {
|
||||||
|
goalMs: 25
|
||||||
|
}
|
||||||
|
},
|
||||||
/** End configuration */
|
/** End configuration */
|
||||||
|
])).render();
|
||||||
}
|
}
|
||||||
|
|
||||||
if('requestIdleCallback' in window)
|
if('requestIdleCallback' in window)
|
||||||
|
|
|
@ -19,7 +19,7 @@ module.exports = function(config) {
|
||||||
|
|
||||||
// list of files / patterns to load in the browser
|
// list of files / patterns to load in the browser
|
||||||
files: [
|
files: [
|
||||||
'test/*.ts',
|
'test/**/*.ts'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
|
import { IPanel } from "./ipanel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configuration options for constructing a button.
|
||||||
|
*/
|
||||||
export interface IButtonConfiguration {
|
export interface IButtonConfiguration {
|
||||||
/** The icon for the button. The intention is to use a single character emoji but it's just a string, so anything goes */
|
/** The icon for the button. The intention is to use a single character emoji but it's just a string, so anything goes. */
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
|
|
||||||
|
/** The panel that owns this button. */
|
||||||
|
parent?: IPanel;
|
||||||
|
|
||||||
/** Gets the background color for the button. */
|
/** Gets the background color for the button. */
|
||||||
getColor?(): string;
|
getColor?(): string;
|
||||||
|
|
||||||
|
@ -11,7 +19,7 @@ export interface IButtonConfiguration {
|
||||||
|
|
||||||
/** Describes a button to be displayed in the collapsed toolbar. */
|
/** Describes a button to be displayed in the collapsed toolbar. */
|
||||||
export class Button {
|
export class Button {
|
||||||
/** The icon for the button. The intention is to use a single character emoji but it's just a string, so anything goes */
|
/** The icon for the button. The intention is to use a single character emoji but it's just a string, so anything goes. */
|
||||||
public readonly emoji: string;
|
public readonly emoji: string;
|
||||||
|
|
||||||
/** Gets the background color for the button. */
|
/** Gets the background color for the button. */
|
||||||
|
@ -20,11 +28,15 @@ export class Button {
|
||||||
/** Gets the displayed value for the button. */
|
/** Gets the displayed value for the button. */
|
||||||
public readonly getValue: (() => string);
|
public readonly getValue: (() => string);
|
||||||
|
|
||||||
|
/** The panel that provides this button. */
|
||||||
|
public readonly parent: IPanel | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the button.
|
* Create the button.
|
||||||
*/
|
*/
|
||||||
public constructor(config: IButtonConfiguration = {}) {
|
public constructor(config: IButtonConfiguration = {}) {
|
||||||
this.emoji = config.emoji !== undefined ? config.emoji : "";
|
this.emoji = config.emoji !== undefined ? config.emoji : "";
|
||||||
|
this.parent = config.parent;
|
||||||
/* tslint:disable no-unbound-method */
|
/* tslint:disable no-unbound-method */
|
||||||
this.getValue = config.getValue !== undefined ? config.getValue : (): string => "";
|
this.getValue = config.getValue !== undefined ? config.getValue : (): string => "";
|
||||||
this.getColor = config.getColor !== undefined ? config.getColor : (): string => "";
|
this.getColor = config.getColor !== undefined ? config.getColor : (): string => "";
|
||||||
|
@ -40,6 +52,12 @@ export class Button {
|
||||||
li.setAttribute("style", `background-color:${this.getColor()}`);
|
li.setAttribute("style", `background-color:${this.getColor()}`);
|
||||||
li.innerText = `${this.emoji} ${this.getValue()}`;
|
li.innerText = `${this.emoji} ${this.getValue()}`;
|
||||||
|
|
||||||
|
li.addEventListener("click", () => {
|
||||||
|
if (this.parent) {
|
||||||
|
this.parent.toggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
container.appendChild(li);
|
container.appendChild(li);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/** The level of precision we want to see for numbers */
|
||||||
|
export const DECIMAL_PLACES: number = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param end The end timestamp
|
||||||
|
* @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)) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return (end - start).toFixed(decimalPlaces);
|
||||||
|
};
|
|
@ -1,2 +1,3 @@
|
||||||
export { Toolbar } from "./toolbar";
|
export { Toolbar } from "./toolbar";
|
||||||
export { Button } from "./button";
|
export { Button } from "./button";
|
||||||
|
export { NavigationTimingsPanel } from "./panels/navigation-timing";
|
||||||
|
|
|
@ -1,7 +1,25 @@
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
|
import { PanelFrame } from "./panelframe";
|
||||||
|
|
||||||
|
export interface IPanelWithConfiguration<C, P> {
|
||||||
|
config: C;
|
||||||
|
panel: IPanelConstructor<C, P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPanelConstructor<C, P> {
|
||||||
|
new (frame: PanelFrame, config: C): P;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-empty-interface
|
||||||
|
export interface IPanelConfig {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/** Describes a panel within the opened toolbar. */
|
/** Describes a panel within the opened toolbar. */
|
||||||
export interface IPanel {
|
export interface IPanel {
|
||||||
|
/**
|
||||||
|
* The name of the panel.
|
||||||
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,4 +32,7 @@ export interface IPanel {
|
||||||
* @param target The HTML element to contain this panel.
|
* @param target The HTML element to contain this panel.
|
||||||
*/
|
*/
|
||||||
render(target: HTMLElement): void;
|
render(target: HTMLElement): void;
|
||||||
|
|
||||||
|
/** Toggles the visibility of this panel */
|
||||||
|
toggle(): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { IPanel } from "./ipanel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for holding and displaying panels
|
||||||
|
*/
|
||||||
|
export class PanelFrame {
|
||||||
|
/** The element that represents the frame in the DOM. */
|
||||||
|
private readonly frame: HTMLDivElement;
|
||||||
|
|
||||||
|
/** The root of the toolbar */
|
||||||
|
private readonly toolbarRoot: HTMLElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the panel frame.
|
||||||
|
* @param toolbarRoot The DOM element to contain the frame. Should be the root of the toolbar.
|
||||||
|
*/
|
||||||
|
public constructor(toolbarRoot: HTMLElement) {
|
||||||
|
this.toolbarRoot = toolbarRoot;
|
||||||
|
|
||||||
|
this.frame = document.createElement("div");
|
||||||
|
this.frame.setAttribute("id", "PTB_frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Show the provided panel, or hide the displayed panel. */
|
||||||
|
public toggle(panel: IPanel): void {
|
||||||
|
if (this.frame.parentNode === null) {
|
||||||
|
panel.render(this.frame);
|
||||||
|
this.toolbarRoot.appendChild(this.frame);
|
||||||
|
} else {
|
||||||
|
this.frame.parentNode.removeChild(this.frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { Button } from "../button";
|
||||||
|
import * as Formatter from "../formatter";
|
||||||
|
import { IPanel, IPanelConfig } from "../ipanel";
|
||||||
|
import { PanelFrame } from "../panelframe";
|
||||||
|
|
||||||
|
/** Describes the configuration options available for the network panel */
|
||||||
|
export interface INavigationTimingsPanelConfig extends IPanelConfig {
|
||||||
|
/** The goal for the load duration */
|
||||||
|
goalMs: number;
|
||||||
|
/** The performance timing object, can be included in the config to enable injection of a mock object for testing */
|
||||||
|
timings: PerformanceTiming;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A set of default configuration options for the navigation timings panel */
|
||||||
|
const navigationTimingsPanelDefaultConfig: INavigationTimingsPanelConfig = {
|
||||||
|
goalMs: 500,
|
||||||
|
timings: performance.timing,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/** The frame that displays this panel. */
|
||||||
|
private readonly frame: PanelFrame;
|
||||||
|
|
||||||
|
public constructor(frame: PanelFrame, config?: INavigationTimingsPanelConfig) {
|
||||||
|
this.frame = frame;
|
||||||
|
this.config = { ...navigationTimingsPanelDefaultConfig, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the buttons to be displayed
|
||||||
|
* @see IPanel.getButtons
|
||||||
|
*/
|
||||||
|
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",
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the contents of the panel
|
||||||
|
* @see IPanel.render
|
||||||
|
*/
|
||||||
|
public render(target: HTMLElement): void {
|
||||||
|
const t: PerformanceTiming = this.config.timings;
|
||||||
|
|
||||||
|
target.innerHTML = `
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Get Connected</th>
|
||||||
|
<td>${Formatter.duration(t.connectEnd, t.domainLookupStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DNS Lookup</td>
|
||||||
|
<td>${Formatter.duration(t.domainLookupEnd, t.domainLookupStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>SSL</td>
|
||||||
|
<td>${Formatter.duration(t.connectEnd, t.connectStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Get Content</th>
|
||||||
|
<td>${Formatter.duration(t.responseEnd, t.requestStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Waiting for Server</td>
|
||||||
|
<td>${Formatter.duration(t.responseStart, t.requestStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Time To Download</td>
|
||||||
|
<td>${Formatter.duration(t.responseEnd, t.responseStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th colspan=2>Get Ready</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Parse Content</td>
|
||||||
|
<td>${Formatter.duration(t.domInteractive, t.responseEnd)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Deferred Scripts</td>
|
||||||
|
<td>${Formatter.duration(t.domContentLoadedEventEnd, t.domInteractive)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DOM Complete</td>
|
||||||
|
<td>${Formatter.duration(t.domComplete, t.domContentLoadedEventEnd)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Load Event</td>
|
||||||
|
<td>${Formatter.duration(t.loadEventEnd, t.loadEventStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Total Load</th>
|
||||||
|
<td>${Formatter.duration(t.loadEventEnd, t.navigationStart)} ms</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the display of this panel.
|
||||||
|
*/
|
||||||
|
public toggle(): void {
|
||||||
|
this.frame.toggle(this);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +1,42 @@
|
||||||
import { IPanel } from "./ipanel";
|
import { IPanel, IPanelConfig, IPanelWithConfiguration } from "./ipanel";
|
||||||
|
import { PanelFrame } from "./panelframe";
|
||||||
|
|
||||||
/** Describes the toolbar. */
|
/** Describes the toolbar. */
|
||||||
export class Toolbar {
|
export class Toolbar {
|
||||||
/** The container that will hold the toolbar */
|
|
||||||
private container: HTMLElement;
|
|
||||||
|
|
||||||
/** The panels that will be displayed in the toolbar */
|
/** The panels that will be displayed in the toolbar */
|
||||||
private panels: IPanel[];
|
private panels: IPanel[];
|
||||||
|
|
||||||
/** The root element of the toolbar. */
|
/** The root element of the toolbar. */
|
||||||
private root: HTMLElement;
|
private toolbarRoot: HTMLElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the toolbar.
|
* Creates the toolbar.
|
||||||
* @param panels The panels to be displayed when the toolbar is opened.
|
* @param panels Classes for the panels to be displayed when the toolbar is opened.
|
||||||
* @param container Optional parameter that defaults to the body of the HTML page.
|
* @param container Optional parameter for the element that contains the toolbar. It defaults to the body of the HTML page.
|
||||||
*/
|
*/
|
||||||
public constructor(panels: IPanel[], container: HTMLElement = window.document.body) {
|
public constructor(panels: Array<IPanelWithConfiguration<IPanelConfig, IPanel>>, container: HTMLElement = window.document.body) {
|
||||||
this.panels = panels;
|
this.toolbarRoot = document.createElement("div");
|
||||||
this.container = container;
|
this.toolbarRoot.setAttribute("id", "PTB_root");
|
||||||
this.root = document.createElement("div");
|
container.appendChild(this.toolbarRoot);
|
||||||
container.appendChild(this.root);
|
|
||||||
|
// 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the toolbar.
|
* Renders the toolbar.
|
||||||
*/
|
*/
|
||||||
public render(): void {
|
public render(): void {
|
||||||
// Clear all children
|
const listOfButtons: HTMLUListElement = document.createElement("ul");
|
||||||
this.container.innerHTML = "";
|
listOfButtons.setAttribute("id", "PTB_buttons");
|
||||||
|
for (const panel of this.panels) {
|
||||||
const ul: HTMLUListElement = document.createElement("ul");
|
for (const button of panel.getButtons()) {
|
||||||
for (const p of this.panels) {
|
button.render(listOfButtons);
|
||||||
for (const b of p.getButtons()) {
|
|
||||||
b.render(ul);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.appendChild(ul);
|
this.toolbarRoot.appendChild(listOfButtons);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { assert, expect } from "chai";
|
||||||
|
import "mocha";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
|
||||||
|
import * as Formatter from "../src/formatter";
|
||||||
|
|
||||||
|
describe("Formatter class", () => {
|
||||||
|
describe("duration", () => {
|
||||||
|
it("should return a dash for invalid input in the first parameter", () => {
|
||||||
|
expect(Formatter.duration(undefined, 0)).to.equal("-");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a dash for invalid input in the second parameter", () => {
|
||||||
|
expect(Formatter.duration(0, undefined)).to.equal("-");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should default to two decimal places", () => {
|
||||||
|
expect(Formatter.duration(0, 0)).to.equal("0.00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow custom number of decimals", () => {
|
||||||
|
expect(Formatter.duration(0, 0, 1)).to.equal("0.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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,18 +1,31 @@
|
||||||
import { Button } from "../../src/button";
|
import { Button } from "../../src/button";
|
||||||
import { IPanel } from "../../src/ipanel";
|
import { IPanel } from "../../src/ipanel";
|
||||||
|
import { PanelFrame } from "../../src/panelframe";
|
||||||
|
|
||||||
|
export interface IMockPanelConfig {
|
||||||
|
getButtons(): Button[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mockPanelConfig: IMockPanelConfig = {
|
||||||
|
getButtons: (): Button[] => [new Button({})],
|
||||||
|
};
|
||||||
|
|
||||||
export class MockPanel implements IPanel {
|
export class MockPanel implements IPanel {
|
||||||
public name: string = "Mock Panel";
|
public name: string = "Mock Panel";
|
||||||
|
|
||||||
public constructor(getButtons: () => Button[]) {
|
public constructor(frame: PanelFrame, config: IMockPanelConfig) {
|
||||||
this.getButtons = getButtons;
|
this.getButtons = config.getButtons;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getButtons(): Button[] {
|
public getButtons(): Button[] {
|
||||||
return undefined;
|
throw new Error("Method must be overridden by passing a new getter in the constructor.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(target: HTMLElement): void {
|
public render(target: HTMLElement): void {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toggle(): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { assert, 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";
|
||||||
|
|
||||||
|
describe("Navigation timing panel class", () => {
|
||||||
|
|
||||||
|
it("should provide a single button that is green when below the goal", () => {
|
||||||
|
const rootElement: HTMLElement = document.createElement("div");
|
||||||
|
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||||
|
const panel: NavigationTimingsPanel = new NavigationTimingsPanel(frame, {
|
||||||
|
goalMs: 200,
|
||||||
|
timings: getMockTimings({
|
||||||
|
navigationStart: 0,
|
||||||
|
loadEventEnd: 100,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons: Button[] = panel.getButtons();
|
||||||
|
|
||||||
|
expect(buttons.length).to.equal(1, "there should only be one button for this panel");
|
||||||
|
expect(buttons[0].getColor()).to.equal("green", "the button should be green");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should provide a single button that is green when at the goal", () => {
|
||||||
|
const rootElement: HTMLElement = document.createElement("div");
|
||||||
|
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||||
|
const panel: NavigationTimingsPanel = new NavigationTimingsPanel(frame, {
|
||||||
|
goalMs: 100,
|
||||||
|
timings: getMockTimings({
|
||||||
|
navigationStart: 0,
|
||||||
|
loadEventEnd: 100,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons: Button[] = panel.getButtons();
|
||||||
|
|
||||||
|
expect(buttons.length).to.equal(1, "there should only be one button for this panel");
|
||||||
|
expect(buttons[0].getColor()).to.equal("green", "the button should be green");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should provide a button that is red when above the goal", () => {
|
||||||
|
const rootElement: HTMLElement = document.createElement("div");
|
||||||
|
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||||
|
const panel: NavigationTimingsPanel = new NavigationTimingsPanel(frame, {
|
||||||
|
goalMs: 50,
|
||||||
|
timings: getMockTimings({
|
||||||
|
navigationStart: 0,
|
||||||
|
loadEventEnd: 100,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons: Button[] = panel.getButtons();
|
||||||
|
|
||||||
|
expect(buttons.length).to.equal(1, "there should only be one button for this panel");
|
||||||
|
expect(buttons[0].getColor()).to.equal("red", "the button should be red");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render", () => {
|
||||||
|
const rootElement: HTMLElement = document.createElement("div");
|
||||||
|
const frame: PanelFrame = new PanelFrame(rootElement);
|
||||||
|
const panel: NavigationTimingsPanel = new NavigationTimingsPanel(frame, {
|
||||||
|
goalMs: 90,
|
||||||
|
timings: getMockTimings(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const target: HTMLElement = document.createElement("div");
|
||||||
|
panel.render(target);
|
||||||
|
|
||||||
|
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 => "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
|
@ -3,9 +3,9 @@ import "mocha";
|
||||||
import * as sinon from "sinon";
|
import * as sinon from "sinon";
|
||||||
|
|
||||||
import { Button } from "../src/button";
|
import { Button } from "../src/button";
|
||||||
import { IPanel } from "../src/ipanel";
|
import { IPanel, IPanelConstructor, IPanelWithConfiguration } from "../src/ipanel";
|
||||||
import { Toolbar } from "../src/toolbar";
|
import { Toolbar } from "../src/toolbar";
|
||||||
import { MockPanel } from "./mock/panel.mock";
|
import { IMockPanelConfig, MockPanel, mockPanelConfig } from "./mock/panel.mock";
|
||||||
|
|
||||||
describe("Toolbar class", () => {
|
describe("Toolbar class", () => {
|
||||||
|
|
||||||
|
@ -17,13 +17,18 @@ describe("Toolbar class", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can render buttons", () => {
|
it("can render buttons", () => {
|
||||||
const panel: IPanel = new MockPanel((): Button[] => [new Button({})]);
|
|
||||||
const container: HTMLElement = document.createElement("div");
|
const container: HTMLElement = document.createElement("div");
|
||||||
const toolbar: Toolbar = new Toolbar([panel], container);
|
|
||||||
|
const mockPanelWithConfig: IPanelWithConfiguration<IMockPanelConfig, MockPanel> = {
|
||||||
|
panel: MockPanel,
|
||||||
|
config: mockPanelConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolbar: Toolbar = new Toolbar([mockPanelWithConfig], container);
|
||||||
|
|
||||||
toolbar.render();
|
toolbar.render();
|
||||||
|
|
||||||
const expectedList: Element = container.firstElementChild;
|
const expectedList: Element = container.firstElementChild.children.item(0);
|
||||||
expect(expectedList).instanceof(HTMLUListElement, "We expect the toolbar to be a list");
|
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");
|
expect(expectedList.childElementCount).equals(1, "We expect that list to have one item");
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,8 @@
|
||||||
"prefer-function-over-method": false,
|
"prefer-function-over-method": false,
|
||||||
"no-implicit-dependencies": [true, "dev"],
|
"no-implicit-dependencies": [true, "dev"],
|
||||||
"no-import-side-effect": false,
|
"no-import-side-effect": false,
|
||||||
"strict-boolean-expressions": [true, "allow-undefined-union"]
|
"strict-boolean-expressions": [true, "allow-undefined-union"],
|
||||||
|
"interface-over-type-literal": false
|
||||||
},
|
},
|
||||||
"jsRules": {
|
"jsRules": {
|
||||||
"max-line-length": {
|
"max-line-length": {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче