Merge pull request #24 from kawwong/display-vlt

Consume viewportLoadTime information in view layer
This commit is contained in:
C. Naoto Abreu Takemura 2019-01-30 11:17:59 -08:00 коммит произвёл GitHub
Родитель 9e0da19a40 77b39890c0
Коммит 946f947cf3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 652 добавлений и 104 удалений

Просмотреть файл

@ -22,33 +22,44 @@ class App extends Component {
componentDidMount () {
MezzuriteInspector.isMezzuritePresentAsync().then(() => {
MezzuriteInspector.listenForTimingEvents(event => {
const formattedTimings = formatTimingsEvent(event);
if (formattedTimings.applicationLoadTime != null) {
this.setState({ applicationLoadTime: formattedTimings.applicationLoadTime });
}
if (formattedTimings.componentTimings != null && formattedTimings.componentTimings !== []) {
this.setState((previousState) => {
const captureCycles = {
componentTimings: formattedTimings.componentTimings,
time: formattedTimings.time
};
if (previousState.captureCycles != null) {
return { captureCycles: [ captureCycles, ...previousState.captureCycles ] };
} else {
return { captureCycles: [ captureCycles ] };
}
});
}
this.setState({ framework: formattedTimings.framework });
});
MezzuriteInspector.listenForTimingEvents((event) => this.handleTimingEvent(event));
});
}
handleTimingEvent (event) {
const formattedTimings = formatTimingsEvent(event);
if (formattedTimings != null) {
if (formattedTimings.applicationLoadTime != null && this.state.applicationLoadTime == null) {
this.setState({ applicationLoadTime: formattedTimings.applicationLoadTime });
}
if (this.state.framework.name == null && this.state.framework.version == null) {
this.setState({ framework: formattedTimings.framework });
}
if (
formattedTimings.insideViewportComponents != null ||
formattedTimings.outsideViewportComponents != null
) {
const captureCycle = {
insideViewportComponents: formattedTimings.insideViewportComponents,
outsideViewportComponents: formattedTimings.outsideViewportComponents,
time: formattedTimings.time,
viewportLoadTime: formattedTimings.viewportLoadTime
};
this.setState((previousState) => {
if (previousState.captureCycles != null) {
return { captureCycles: [ captureCycle, ...previousState.captureCycles ] };
} else {
return { captureCycles: [ captureCycle ] };
}
});
}
}
}
render () {
return (
<div className='app'>

Просмотреть файл

@ -11,12 +11,8 @@
margin: 0 16px 12px;
}
.capture-cycle--components {
display: flex;
flex-wrap: wrap;
list-style-type: none;
margin: 16px;
padding: 0;
.capture-cycle--statistic {
font-weight: 500;
}
.capture-cycle--timestamp {
@ -27,6 +23,18 @@
padding: 8px 16px;
text-align: center;
}
.capture-cycle--tooltip {
background-color: var(--microsoft-blue);
border-radius: 16px;
color: var(--microsoft-white);
font-size: 10px;
font-weight: 500;
margin-left: 2px;
padding: 0 4px;
text-decoration: none;
vertical-align: text-top;
}
}
@media only screen and (min-width: 540px) {

Просмотреть файл

@ -1,31 +1,64 @@
import React from 'react';
import React, { Fragment } from 'react';
import { arrayOf, number, shape, string } from 'prop-types';
import './CaptureCycle.css';
import ComponentTiming from '../ComponentTiming/ComponentTiming.js';
import CaptureCycleSection from '../CaptureCycleSection/CaptureCycleSection';
const CaptureCycle = (props) => (
props != null && props.timings != null && props.timings.length > 0 && <section className='capture-cycle--card'>
<h2 className='capture-cycle--timestamp'>Capture Cycle{props.timestamp != null && <span> at {props.timestamp}:</span>}</h2>
<ul className='capture-cycle--components'>
{props.timings.map((timing, timingIndex) =>
<ComponentTiming
key={`capture-cycle-${props.captureCycleIndex}-component-${timingIndex}`}
name={timing.componentName}
loadTime={timing.componentLoadTime}
const CaptureCycle = (props) => {
const captureCycleHeader = <h2 className='capture-cycle--timestamp'>
{props.routeUrl == null ? 'Capture Cycle' : props.routeUrl}
{props.timestamp != null && <span> at {props.timestamp}:</span>}
</h2>;
const insideViewportHeading = 'Inside Viewport';
const insideViewportSubheading = props.viewportLoadTime != null && <Fragment>
Viewport Load Time: <span className='capture-cycle--statistic'>{props.viewportLoadTime.toFixed(1)}</span>ms
<a
aria-label='What is viewport load time?'
className='capture-cycle--tooltip'
href='https://github.com/Microsoft/Mezzurite#viewport-load-time-vlt'
target='_blank'
>?</a>
</Fragment>;
const outsideViewportHeading = 'Outside Viewport';
const shouldRenderInsideViewportComponents = props.insideViewportComponents != null && props.insideViewportComponents.length > 0;
const shouldRenderOutsideViewportComponents = props.outsideViewportComponents != null && props.outsideViewportComponents.length > 0;
return (
props != null && (shouldRenderInsideViewportComponents || shouldRenderOutsideViewportComponents) &&
<section className='capture-cycle--card'>
{captureCycleHeader}
{shouldRenderInsideViewportComponents &&
<CaptureCycleSection
captureCycleIndex={props.captureCycleIndex}
components={props.insideViewportComponents}
heading={insideViewportHeading}
subheading={insideViewportSubheading}
/>
)}
</ul>
</section>
);
}
{shouldRenderOutsideViewportComponents &&
<CaptureCycleSection
captureCycleIndex={props.captureCycleIndex}
components={props.outsideViewportComponents}
heading={outsideViewportHeading}
/>
}
</section>
);
};
CaptureCycle.propTypes = {
captureCycleIndex: number,
timestamp: string,
timings: arrayOf(shape({
insideViewportComponents: arrayOf(shape({
componentLoadTime: number,
componentName: string
}))
})),
outsideViewportComponents: arrayOf(shape({
componentLoadTime: number,
componentName: string
})),
routeUrl: string,
timestamp: string,
viewportLoadTime: number
};
export default CaptureCycle;

Просмотреть файл

@ -12,36 +12,136 @@ describe('CaptureCycle.js', () => {
expect(tree).toBeFalsy();
});
it('should not render when the timings are null', () => {
it('should not render when insideViewportComponents and outsideViewportComponents are null', () => {
const tree = renderer
.render(<CaptureCycle timings={null} />);
.render(<CaptureCycle insideViewportComponents={null} outsideViewportComponents={null} />);
expect(tree).toBeFalsy();
});
it('should not render when there are no timings', () => {
it('should not render when insideViewportComponents and outsideViewportComponents are empty', () => {
const tree = renderer
.render(<CaptureCycle timings={[]} />);
.render(<CaptureCycle insideViewportComponents={[]} outsideViewportComponents={[]} />);
expect(tree).toBeFalsy();
});
it('should not error out when there is no timestamp', () => {
it('should render when there are only insideViewportComponents', () => {
const tree = renderer
.render(<CaptureCycle captureCycleIndex={0} timings={[ { componentLoadTime: 9.2, componentName: 'componentName' } ]} />);
.render(<CaptureCycle insideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]} />);
expect(tree).toMatchSnapshot();
});
it('should not error out when there is no captureCycleIndex', () => {
it('should render when there are only outsideViewportComponents', () => {
const tree = renderer
.render(<CaptureCycle timestamp={new Date(2019, 1, 23).toLocaleTimeString()} timings={[ { componentLoadTime: 9.2, componentName: 'componentName' } ]} />);
.render(<CaptureCycle outsideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]} />);
expect(tree).toMatchSnapshot();
});
it('should render the capture cycle', () => {
it('should render when there are both insideViewportComponents and outsideViewportComponents', () => {
const tree = renderer
.render(<CaptureCycle
captureCycleIndex={0}
timestamp={new Date(2019, 1, 23).toLocaleTimeString()}
timings={[ { componentLoadTime: 9.2, componentName: 'componentName' } ]}
insideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
outsideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
/>);
expect(tree).toMatchSnapshot();
});
it('should render with a routeUrl', () => {
const tree = renderer
.render(<CaptureCycle
insideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
outsideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
routeUrl='routeUrl'
/>);
expect(tree).toMatchSnapshot();
});
it('should render with a timestamp', () => {
const tree = renderer
.render(<CaptureCycle
insideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
outsideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
timestamp='timestamp'
/>);
expect(tree).toMatchSnapshot();
});
it('should render with both a routeUrl and timestamp', () => {
const tree = renderer
.render(<CaptureCycle
insideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
outsideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
routeUrl='routeUrl'
timestamp='timestamp'
/>);
expect(tree).toMatchSnapshot();
});
it('should render with a viewportLoadTime', () => {
const tree = renderer
.render(<CaptureCycle
insideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
outsideViewportComponents={[
{
componentLoadTime: 12.2,
componentName: 'componentName'
}
]}
viewportLoadTime={15.4}
/>);
expect(tree).toMatchSnapshot();
});

Просмотреть файл

@ -1,6 +1,123 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CaptureCycle.js should not error out when there is no captureCycleIndex 1`] = `
exports[`CaptureCycle.js should render when there are both insideViewportComponents and outsideViewportComponents 1`] = `
<section
className="capture-cycle--card"
>
<h2
className="capture-cycle--timestamp"
>
Capture Cycle
</h2>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Inside Viewport"
subheading={false}
/>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Outside Viewport"
/>
</section>
`;
exports[`CaptureCycle.js should render when there are only insideViewportComponents 1`] = `
<section
className="capture-cycle--card"
>
<h2
className="capture-cycle--timestamp"
>
Capture Cycle
</h2>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Inside Viewport"
subheading={false}
/>
</section>
`;
exports[`CaptureCycle.js should render when there are only outsideViewportComponents 1`] = `
<section
className="capture-cycle--card"
>
<h2
className="capture-cycle--timestamp"
>
Capture Cycle
</h2>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Outside Viewport"
/>
</section>
`;
exports[`CaptureCycle.js should render with a routeUrl 1`] = `
<section
className="capture-cycle--card"
>
<h2
className="capture-cycle--timestamp"
>
routeUrl
</h2>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Inside Viewport"
subheading={false}
/>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Outside Viewport"
/>
</section>
`;
exports[`CaptureCycle.js should render with a timestamp 1`] = `
<section
className="capture-cycle--card"
>
@ -10,22 +127,37 @@ exports[`CaptureCycle.js should not error out when there is no captureCycleIndex
Capture Cycle
<span>
at
12:00:00 AM
timestamp
:
</span>
</h2>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={9.2}
name="componentName"
/>
</ul>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Inside Viewport"
subheading={false}
/>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Outside Viewport"
/>
</section>
`;
exports[`CaptureCycle.js should not error out when there is no timestamp 1`] = `
exports[`CaptureCycle.js should render with a viewportLoadTime 1`] = `
<section
className="capture-cycle--card"
>
@ -34,38 +166,86 @@ exports[`CaptureCycle.js should not error out when there is no timestamp 1`] = `
>
Capture Cycle
</h2>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={9.2}
name="componentName"
/>
</ul>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Inside Viewport"
subheading={
<React.Fragment>
Viewport Load Time:
<span
className="capture-cycle--statistic"
>
15.4
</span>
ms
<a
aria-label="What is viewport load time?"
className="capture-cycle--tooltip"
href="https://github.com/Microsoft/Mezzurite#viewport-load-time-vlt"
target="_blank"
>
?
</a>
</React.Fragment>
}
/>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Outside Viewport"
/>
</section>
`;
exports[`CaptureCycle.js should render the capture cycle 1`] = `
exports[`CaptureCycle.js should render with both a routeUrl and timestamp 1`] = `
<section
className="capture-cycle--card"
>
<h2
className="capture-cycle--timestamp"
>
Capture Cycle
routeUrl
<span>
at
12:00:00 AM
timestamp
:
</span>
</h2>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={9.2}
name="componentName"
/>
</ul>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Inside Viewport"
subheading={false}
/>
<CaptureCycleSection
components={
Array [
Object {
"componentLoadTime": 12.2,
"componentName": "componentName",
},
]
}
heading="Outside Viewport"
/>
</section>
`;

Просмотреть файл

@ -0,0 +1,30 @@
@media only screen {
.capture-cycle--components {
display: flex;
flex-wrap: wrap;
list-style-type: none;
margin: 16px;
padding: 0;
}
.capture-cycle--section-header {
font-size: 20px;
font-weight: 500;
margin: 12px 16px 0;
text-align: center;
}
.capture-cycle--section-sub-header {
font-size: 16px;
font-weight: 400;
margin: 8px 16px 0;
text-align: center;
}
}
@media only screen and (min-width: 540px) {
.capture-cycle--section-header,
.capture-cycle--section-sub-header {
text-align: left;
}
}

Просмотреть файл

@ -0,0 +1,33 @@
import React from 'react';
import { arrayOf, node, number, shape, string } from 'prop-types';
import ComponentTiming from '../ComponentTiming/ComponentTiming';
import './CaptureCycleSection.css';
const CaptureCycleSection = (props) => (
props != null && props.components != null && props.components.length > 0 && <section>
{props.heading != null && <h3 className='capture-cycle--section-header'>{props.heading}</h3>}
{props.subheading != null && <h4 className='capture-cycle--section-sub-header'>{props.subheading}</h4>}
<ul className='capture-cycle--components'>
{props.components.map((component, timingIndex) =>
<ComponentTiming
key={`capture-cycle-${props.captureCycleIndex}-component-${timingIndex}`}
name={component.componentName}
loadTime={component.componentLoadTime}
/>
)}
</ul>
</section>
);
CaptureCycleSection.propTypes = {
captureCycleIndex: number,
components: arrayOf(shape({
componentLoadTime: number,
componentName: string
})),
heading: string,
subheading: node
};
export default CaptureCycleSection;

Просмотреть файл

@ -0,0 +1,79 @@
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import CaptureCycleSection from './CaptureCycleSection';
describe('CaptureCycleSection.js', () => {
const renderer = new ShallowRenderer();
it('should not render when the props are null', () => {
const tree = renderer
.render(<CaptureCycleSection />);
expect(tree).toBeFalsy();
});
it('should not render when the components are null', () => {
const tree = renderer
.render(<CaptureCycleSection components={null} />);
expect(tree).toBeFalsy();
});
it('should not render when the components are empty', () => {
const tree = renderer
.render(<CaptureCycleSection components={null} />);
expect(tree).toBeFalsy();
});
it('should render the capture cycle section', () => {
const tree = renderer
.render(<CaptureCycleSection components={[
{
componentLoadTime: 14.2,
componentName: 'componentName'
}
]} />);
expect(tree).toMatchSnapshot();
});
it('should render the capture cycle section with a captureCycleIndex', () => {
const tree = renderer
.render(<CaptureCycleSection
captureCycleIndex={4}
components={[
{
componentLoadTime: 14.2,
componentName: 'componentName'
}
]}
/>);
expect(tree).toMatchSnapshot();
});
it('should render the capture cycle section with a heading', () => {
const tree = renderer
.render(<CaptureCycleSection
components={[
{
componentLoadTime: 14.2,
componentName: 'componentName'
}
]}
heading='heading'
/>);
expect(tree).toMatchSnapshot();
});
it('should render the capture cycle section with a subheading', () => {
const tree = renderer
.render(<CaptureCycleSection
components={[
{
componentLoadTime: 14.2,
componentName: 'componentName'
}
]}
subheading={<span>subheading</span>}
/>);
expect(tree).toMatchSnapshot();
});
});

Просмотреть файл

@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CaptureCycleSection.js should render the capture cycle section 1`] = `
<section>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={14.2}
name="componentName"
/>
</ul>
</section>
`;
exports[`CaptureCycleSection.js should render the capture cycle section with a captureCycleIndex 1`] = `
<section>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={14.2}
name="componentName"
/>
</ul>
</section>
`;
exports[`CaptureCycleSection.js should render the capture cycle section with a heading 1`] = `
<section>
<h3
className="capture-cycle--section-header"
>
heading
</h3>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={14.2}
name="componentName"
/>
</ul>
</section>
`;
exports[`CaptureCycleSection.js should render the capture cycle section with a subheading 1`] = `
<section>
<h4
className="capture-cycle--section-sub-header"
>
<span>
subheading
</span>
</h4>
<ul
className="capture-cycle--components"
>
<ComponentTiming
loadTime={14.2}
name="componentName"
/>
</ul>
</section>
`;

Просмотреть файл

@ -12,14 +12,14 @@
.component-timing--detail {
display: flex;
margin: 0 8px;
margin: 12px;
}
.component-timing--name {
font-size: 14px;
font-weight: 500;
line-height: 18px;
margin-left: 8px;
margin: 12px;
overflow: hidden;
text-overflow: ellipsis;
}

Просмотреть файл

@ -5,11 +5,11 @@ import './ComponentTiming.css';
const ComponentTiming = (props) => (
props != null && (props.name != null || props.loadTime != null) && <li className='component-timing--card'>
{props.name != null && <h3 className='component-timing--name'>{props.name}</h3>}
{props.name != null && <h4 className='component-timing--name'>{props.name}</h4>}
{props.loadTime != null &&
<p className='component-timing--detail'>
<span className='mobile-hidden'>Load time:</span>
<span className='component-timing--statistic'>{props.loadTime}</span>
<span className='component-timing--statistic'>{props.loadTime.toFixed(1)}</span>
ms
</p>}
</li>

Просмотреть файл

@ -4,11 +4,11 @@ exports[`ComponentTiming.js should render the componentTiming 1`] = `
<li
className="component-timing--card"
>
<h3
<h4
className="component-timing--name"
>
ComponentName
</h3>
</h4>
<p
className="component-timing--detail"
>
@ -53,10 +53,10 @@ exports[`ComponentTiming.js should render the componentTiming with only a name 1
<li
className="component-timing--card"
>
<h3
<h4
className="component-timing--name"
>
ComponentName
</h3>
</h4>
</li>
`;

Просмотреть файл

@ -19,8 +19,11 @@ const Main = (props) => {
<CaptureCycle
captureCycleIndex={captureCycleIndex}
key={`capture-cycle-${captureCycleIndex}`}
insideViewportComponents={captureCycle.insideViewportComponents}
outsideViewportComponents={captureCycle.outsideViewportComponents}
routeUrl={props.routeUrl}
timestamp={captureCycle.time}
timings={captureCycle.componentTimings}
viewportLoadTime={captureCycle.viewportLoadTime}
/>
);
}
@ -34,11 +37,17 @@ Main.propTypes = {
applicationLoadTime: number,
captureCycles: arrayOf(
shape({
componentTimings: arrayOf(shape({
insideViewportComponents: arrayOf(shape({
componentLoadTime: number,
componentName: string
})),
time: string
outsideViewportComponents: arrayOf(shape({
componentLoadTime: number,
componentName: string
})),
routeUrl: string,
time: string,
viewportLoadTime: number
})
)
};

Просмотреть файл

@ -34,7 +34,7 @@ describe('Main.js', () => {
const tree = renderer
.render(<Main captureCycles={[
{
componentTimings: [
insideViewportComponents: [
{
componentLoadTime: 5.23,
componentName: 'ComponentName'

Просмотреть файл

@ -61,8 +61,7 @@ exports[`Main.js should render without applicationLoadTime 1`] = `
>
<CaptureCycle
captureCycleIndex={0}
timestamp="time"
timings={
insideViewportComponents={
Array [
Object {
"componentLoadTime": 5.23,
@ -70,6 +69,7 @@ exports[`Main.js should render without applicationLoadTime 1`] = `
},
]
}
timestamp="time"
/>
</main>
`;