UNPKG

@api-client/har

Version:

Everything related to HAR processing and visualizing in API Client.

917 lines (862 loc) 30.5 kB
/* eslint-disable no-plusplus */ /* eslint-disable class-methods-use-this */ import { LitElement, html } from 'lit-element'; import { classMap } from 'lit-html/directives/class-map.js'; import { styleMap } from 'lit-html/directives/style-map.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { HeadersParser } from '@advanced-rest-client/arc-headers'; import '@anypoint-web-components/anypoint-collapse/anypoint-collapse.js'; import '@anypoint-web-components/anypoint-listbox/anypoint-listbox.js'; import '@anypoint-web-components/anypoint-item/anypoint-icon-item.js'; import '@anypoint-web-components/anypoint-item/anypoint-item-body.js'; import '@anypoint-web-components/anypoint-tabs/anypoint-tabs.js'; import '@anypoint-web-components/anypoint-tabs/anypoint-tab.js'; import * as DataSize from './lib/DataSize.js'; import elementStyles from './styles/HarViewer.js'; /** @typedef {import('har-format').Har} Har */ /** @typedef {import('har-format').Page} Page */ /** @typedef {import('har-format').Entry} Entry */ /** @typedef {import('har-format').Cache} Cache */ /** @typedef {import('har-format').Request} Request */ /** @typedef {import('har-format').Response} Response */ /** @typedef {import('har-format').Header} Header */ /** @typedef {import('har-format').PostData} PostData */ /** @typedef {import('har-format').Content} Content */ /** @typedef {import('har-format').QueryString} QueryString */ /** @typedef {import('har-format').Cookie} Cookie */ /** @typedef {import('har-format').Timings} Timings */ /** @typedef {import('lit-element').TemplateResult} TemplateResult */ /** @typedef {import('@anypoint-web-components/anypoint-listbox').AnypointListbox} AnypointListbox */ /** @typedef {import('@anypoint-web-components/anypoint-tabs').AnypointTabs} AnypointTabs */ /** @typedef {import('./types').RenderedPage} RenderedPage */ /** @typedef {import('./types').RenderedEntry} RenderedEntry */ /** @typedef {import('./types').RenderedEntryTimings} RenderedEntryTimings */ /** @typedef {import('./types').SortableEntry} SortableEntry */ /** @typedef {import('./types').EntrySizing} EntrySizing */ export const harValue = Symbol('harValue'); export const ignorePagesValue = Symbol('ignorePagesValue'); export const processHar = Symbol('processHar'); export const computeEntriesOnly = Symbol('computeEntriesOnly'); export const computePages = Symbol('computePages'); export const pagesValue = Symbol('pagesValue'); export const entriesValue = Symbol('entriesValue'); export const renderPages = Symbol('renderPages'); export const renderEntries = Symbol('renderEntries'); export const pageTemplate = Symbol('pageTemplate'); export const pageHeaderTemplate = Symbol('pageHeaderTemplate'); export const entriesTemplate = Symbol('entriesTemplate'); export const entryTemplate = Symbol('entryTemplate'); export const openedPagesValue = Symbol('openedPagesValue'); export const openedEntriesValue = Symbol('openedEntriesValue'); export const computeRenderedEntries = Symbol('computeRenderedEntries'); export const computeStatusClasses = Symbol('computeStatusClasses'); export const statusLabel = Symbol('statusLabel'); export const loadingTimeTemplate = Symbol('loadingTimeTemplate'); export const responseSizeTemplate = Symbol('responseSizeTemplate'); export const togglePage = Symbol('togglePage'); export const pageClickHandler = Symbol('pageClickHandler'); export const pageKeydownHandler = Symbol('pageKeydownHandler'); export const computeTotalTime = Symbol('computeTotalTime'); export const computeVisualTimes = Symbol('computeVisualTimes'); export const sumTimings = Symbol('sumTimings'); export const timingsTemplate = Symbol('timingsTemplate'); export const timingTemplate = Symbol('timingTemplate'); export const sortEntires = Symbol('sortEntires'); export const entrySelectionHandler = Symbol('entrySelectionHandler'); export const entryDetails = Symbol('entryDetails'); export const entryDetailsTabsTemplate = Symbol('entryDetailsTabsTemplate'); export const selectedTabsValue = Symbol('selectedTabsValue'); export const detailsTabSelectionHandler = Symbol('detailsTabSelectionHandler'); export const entryDetailsContentTemplate = Symbol('entryDetailsContentTemplate'); export const entryDetailsRequestTemplate = Symbol('entryDetailsRequestTemplate'); export const entryDetailsResponseTemplate = Symbol('entryDetailsResponseTemplate'); export const definitionTemplate = Symbol('definitionTemplate'); export const headersTemplate = Symbol('headersTemplate'); export const queryParamsTemplate = Symbol('queryParamsTemplate'); export const computeEntrySizeInfo = Symbol('computeEntrySizeInfo'); export const sizesTemplate = Symbol('sizesTemplate'); export const entryDetailsRequestBodyTemplate = Symbol('entryDetailsRequestBodyTemplate'); export const entryDetailsResponseBodyTemplate = Symbol('entryDetailsResponseBodyTemplate'); export const entryDetailsCookiesTemplate = Symbol('entryDetailsCookiesTemplate'); /** @type {number} used when generating keys for entires */ let nextId = 0; export class HarViewerElement extends LitElement { static get styles() { return elementStyles; } static get properties() { return { /** * The HAR object to render. */ har: { type: Object }, /** * When set it ignores pages matching and renders all requests in a single table. */ ignorePages: { type: Boolean }, }; } /** * @type {Har} */ get har() { return this[harValue]; } /** * @param {Har} value */ set har(value) { const old = this[harValue]; if (old === value) { return; } this[harValue] = value; this[processHar](); } /** * @type {boolean} */ get ignorePages() { return this[ignorePagesValue]; } /** * @param {boolean} value */ set ignorePages(value) { const old = this[ignorePagesValue]; if (old === value) { return; } this[ignorePagesValue] = value; this[processHar](); } constructor() { super(); /** * @type {RenderedEntry[]} */ this[entriesValue] = undefined; /** * @type {RenderedPage[]} */ this[pagesValue] = undefined; /** * @type {string[]} */ this[openedPagesValue] = []; /** * @type {string[]} */ this[openedEntriesValue] = []; this[selectedTabsValue] = {}; } /** * Called when the `har` or `ignorePages` changed. */ [processHar]() { this[entriesValue] = undefined; this[pagesValue] = undefined; const { har, ignorePages } = this; if (!har || !har.log) { this.requestUpdate(); return; } const { log } = har; const { pages, entries, } = log; if (!entries || !entries.length) { this.requestUpdate(); return; } const items = this[sortEntires](entries); if (ignorePages || !pages || !pages.length) { this[computeEntriesOnly](items); } else { this[computePages](pages, items); } } /** * @param {Entry[]} entries * @returns {SortableEntry[]} Copy of the entires array with a shallow copy of each entry. */ [sortEntires](entries) { const cp = entries.map((entry) => { const d = new Date(entry.startedDateTime); return /** @type SortableEntry */ ({ ...entry, timestamp: d.getTime(), }); }); cp.sort((a, b) => a.timestamp - b.timestamp); return cp; } /** * Performs computations to render entries only. * @param {SortableEntry[]} entries The list of entries to process. */ [computeEntriesOnly](entries) { const totalTime = this[computeTotalTime](entries[0], entries[entries.length - 1]); this[entriesValue] = this[computeRenderedEntries](entries, totalTime); this[openedEntriesValue] = /** @type string[] */ ([]); this.requestUpdate(); } /** * Performs computations to render entries by page. * @param {Page[]} pages The list of pages to process. * @param {SortableEntry[]} entries The list of entries to process. */ [computePages](pages, entries) { const result = /** @type RenderedPage[] */ ([]); const opened = /** @type string[] */ ([]); pages.forEach((page) => { opened.push(page.id); const items = entries.filter((entry) => entry.pageref === page.id); const totalTime = this[computeTotalTime](items[0], items[items.length - 1]) const item = /** @type RenderedPage */ ({ page, entries: this[computeRenderedEntries](items, totalTime), totalTime, }); result.push(item); }); this[pagesValue] = result; this[openedPagesValue] = opened; this[openedEntriesValue] = /** @type string[] */ ([]); this.requestUpdate(); } /** * @param {SortableEntry[]} entries The entries to perform computations on. * @param {number} totalTime The total time of all entries rendered in the group * @returns {RenderedEntry[]} */ [computeRenderedEntries](entries, totalTime) { const result = /** @type RenderedEntry[] */ ([]); if (!Array.isArray(entries) || !entries.length) { return result; } // This expects entires to be sorted by time (as required by the spec). const [startEntry] = entries; const startTime = startEntry.timestamp; entries.forEach((entry) => { const d = new Date(entry.timestamp); const visualTimings = this[computeVisualTimes](entry.timings, entry.timestamp - startTime, totalTime); const numType = /** @type {"numeric" | "2-digit"} */ ('numeric'); const options = /** @type Intl.DateTimeFormatOptions */ ({ hour: numType, minute: numType, second: numType, fractionalSecondDigits: 3, }); const format = new Intl.DateTimeFormat(undefined, options); const requestTime = format.format(d); const format2 = new Intl.DateTimeFormat(undefined , { timeStyle: 'medium', dateStyle: 'medium', }); const requestFormattedDate = format2.format(d); const requestSizes = this[computeEntrySizeInfo](entry.request); const responseSizes = this[computeEntrySizeInfo](entry.response); const item = /** @type RenderedEntry */ ({ id: nextId++, requestTime, visualTimings, requestFormattedDate, requestSizes, responseSizes, ...entry, }); result.push(item); }); return result; } /** * @param {Request|Response} info * @returns {EntrySizing} */ [computeEntrySizeInfo](info) { const result = /** @type EntrySizing */ ({ headersComputed: false, bodyComputed: false, }); let { headersSize=0, bodySize=0 } = info; const { headers } = info; if (headersSize < 1) { const hdrStr = HeadersParser.toString(headers); headersSize = DataSize.calculateBytes(hdrStr); result.headersComputed = true; } if (bodySize < 1) { const typedRequest = /** @type Request */ (info); const typedResponse = /** @type Response */ (info); if (typedResponse.content) { if (typedResponse.content.size) { bodySize = typedResponse.content.size; } else if (typedResponse.content.text) { bodySize = DataSize.calculateBytes(typedResponse.content.text); result.bodyComputed = true; } } else if (typedRequest.postData && typedRequest.postData.text) { bodySize = DataSize.calculateBytes(typedRequest.postData.text); result.bodyComputed = true; } } if (bodySize < 0) { bodySize = 0; } result.body = DataSize.bytesToSize(bodySize); result.headers = DataSize.bytesToSize(headersSize); result.sum = DataSize.bytesToSize(headersSize + bodySize); result.sumComputed = result.bodyComputed || result.headersComputed; return result; } /** * @param {number} code The status code to test for classes. * @returns {object} List of classes to be set on the status code */ [computeStatusClasses](code) { const classes = { 'status-code': true, error: code >= 500 || code === 0, warning: code >= 400 && code < 500, info: code >= 300 && code < 400, }; return classes; } /** * Computes the total time of page requests. * @param {Entry} first The earliest entry in the range * @param {Entry} last The latest entry in the range * @returns {number} The total time of the page. Used to build the timeline. */ [computeTotalTime](first, last) { if (first === last) { return this[sumTimings](last.timings); } const startTime = new Date(first.startedDateTime).getTime(); const endTime = new Date(last.startedDateTime).getTime(); const lastDuration = this[sumTimings](last.timings); return endTime - startTime + lastDuration; } /** * @param {Timings} timings The entry's timings object. * @param {number} delay The timestamp when the first request started. * @param {number} total The number of milliseconds all entries took. * @returns {RenderedEntryTimings|undefined} */ [computeVisualTimes](timings, delay, total) { if (!timings) { return undefined; } const timingsSum = this[sumTimings](timings); const totalPercent = timingsSum / total * 100; const result = /** @type RenderedEntryTimings */ ({ total: totalPercent, totalValue: timingsSum, }); if (delay) { result.delay = delay / total * 100; } if (typeof timings.blocked === 'number' && timings.blocked > 0) { result.blocked = timings.blocked / timingsSum * 100; } if (typeof timings.connect === 'number' && timings.connect > 0) { result.connect = timings.connect / timingsSum * 100; } if (typeof timings.dns === 'number' && timings.dns > 0) { result.dns = timings.dns / timingsSum * 100; } if (typeof timings.receive === 'number' && timings.receive > 0) { result.receive = timings.receive / timingsSum * 100; } if (typeof timings.send === 'number' && timings.send > 0) { result.send = timings.send / timingsSum * 100; } if (typeof timings.ssl === 'number' && timings.ssl > 0) { result.ssl = timings.ssl / timingsSum * 100; } if (typeof timings.wait === 'number' && timings.wait > 0) { result.wait = timings.wait / timingsSum * 100; } return result; } /** * Sums all timing values. * @param {Timings} timings The timings object to compute * @returns {number} The total time, excluding -1s */ [sumTimings](timings) { let result = 0; if (!timings) { return result; } if (typeof timings.blocked === 'number' && timings.blocked > 0) { result += timings.blocked; } if (typeof timings.connect === 'number' && timings.connect > 0) { result += timings.connect; } if (typeof timings.dns === 'number' && timings.dns > 0) { result += timings.dns; } if (typeof timings.receive === 'number' && timings.receive > 0) { result += timings.receive; } if (typeof timings.send === 'number' && timings.send > 0) { result += timings.send; } if (typeof timings.ssl === 'number' && timings.ssl > 0) { result += timings.ssl; } if (typeof timings.wait === 'number' && timings.wait > 0) { result += timings.wait; } return result; } /** * A handler for the page label click to toggle the page entries. * @param {Event} e */ [pageClickHandler](e) { const node = /** @type HTMLElement */ (e.currentTarget); const id = node.dataset.page; this[togglePage](id); } /** * A handler for the page label keydown to toggle the page entries on space key. * @param {KeyboardEvent} e */ [pageKeydownHandler](e) { if (e.code !== 'Space') { return; } const node = /** @type HTMLElement */ (e.target); const id = node.dataset.page; this[togglePage](id); } /** * Toggles the visibility of the page entries. * @param {string} id The id of the page. */ [togglePage](id) { const allOpened = this[openedPagesValue]; const index = allOpened.indexOf(id); if (index === -1) { allOpened.push(id); } else { allOpened.splice(index, 1); } this.requestUpdate(); } /** * Handler for the list item selection event. * @param {Event} e */ [entrySelectionHandler](e) { const list = /** @type AnypointListbox */ (e.target); const items = /** @type HTMLElement[] */ (list.selectedItems); const ids = items.map((item) => item.dataset.entry); this[openedEntriesValue] = ids; this.requestUpdate(); } /** * Handler for the list item selection event. * @param {Event} e */ [detailsTabSelectionHandler](e) { const tabs = /** @type AnypointTabs */ (e.target); const { selected } = tabs; const id = Number(tabs.dataset.entry); this[selectedTabsValue][id] = selected; this.requestUpdate(); } render() { const pages = this[pagesValue]; if (Array.isArray(pages) && pages.length) { return this[renderPages](pages); } const entries = this[entriesValue]; if (Array.isArray(entries) && entries.length) { return this[renderEntries](entries); } return html``; } /** * @param {RenderedPage[]} pages * @returns {TemplateResult} Template for the pages table */ [renderPages](pages) { return html` <div class="pages"> ${pages.map((info) => this[pageTemplate](info))} </div> `; } /** * @param {RenderedEntry[]} entries * @returns {TemplateResult} Template for the entries table */ [renderEntries](entries) { return html` <section class="entries-list"> ${this[entriesTemplate](entries)} </section> `; } /** * @param {RenderedPage} info * @returns {TemplateResult} Template for a single page */ [pageTemplate](info) { const allOpened = this[openedPagesValue]; const opened = allOpened.includes(info.page.id); return html` <section class="page"> ${this[pageHeaderTemplate](info.page, info.totalTime)} <anypoint-collapse .opened="${opened}"> <div class="page-entries"> ${this[entriesTemplate](info.entries)} </div> </anypoint-collapse> </section> `; } /** * @param {Page} page * @param {number} totalTime * @returns {TemplateResult} Template for the pages table */ [pageHeaderTemplate](page, totalTime) { return html` <div class="page-header" @click="${this[pageClickHandler]}" @keydown="${this[pageKeydownHandler]}" tabindex="0" data-page="${page.id}"> <span class="label">${page.title || 'Unknown page'}</span> ${this[loadingTimeTemplate](totalTime)} </div> `; } /** * @param {RenderedEntry[]} entries * @returns {TemplateResult} The template for the entries list */ [entriesTemplate](entries) { return html` <anypoint-listbox @selected="${this[entrySelectionHandler]}" multi selectable="anypoint-icon-item" aria-label="Select a list item to see details" > ${entries.map((item) => this[entryTemplate](item))} </anypoint-listbox> `; } /** * @param {RenderedEntry} entry * @returns {TemplateResult} The template for a single entry */ [entryTemplate](entry) { const { request, response, timings, visualTimings, id, responseSizes } = entry; const allSelected = this[openedEntriesValue]; const selected = allSelected.includes(`${id}`); return html` <anypoint-icon-item class="entry-item" data-entry="${id}"> <div class="time" slot="item-icon"> ${entry.requestTime} </div> <anypoint-item-body twoline class="entry-body"> <div class="entry-detail-line"> ${this[statusLabel](response.status, response.statusText)} ${this[loadingTimeTemplate](entry.time)} ${this[responseSizeTemplate](responseSizes)} </div> <div class="entry-location" title="${request.url}">${request.method} ${request.url}</div> </anypoint-item-body> <div class="entry-timings"> ${this[timingsTemplate](timings, visualTimings, selected)}</div> </div> </anypoint-icon-item> ${selected ? this[entryDetails](entry) : ''} `; } /** * @param {number} status The response status code * @param {string} statusText The response reason part of the status. * @returns {TemplateResult} The template for the status message */ [statusLabel](status, statusText='') { const codeClasses = this[computeStatusClasses](status); return html` <span class="${classMap(codeClasses)}">${status}</span> <span class="message">${statusText}</span> `; } /** * @param {number} value The response loading time * @returns {TemplateResult|string} Template for the loading time message */ [loadingTimeTemplate](value) { if (Number.isNaN(value)) { return ''; } const roundedValue = Math.round(value || 0); return html`<span class="loading-time-label">Time: ${roundedValue} ms</span>`; } /** * @param {EntrySizing} sizing * @returns {TemplateResult|string} Template for the response size */ [responseSizeTemplate](sizing) { return html`<span class="response-size-label">Size: ${sizing.sum}</span>`; } /** * @param {Timings} timings The entry's timings * @param {RenderedEntryTimings} visualTimings The computed visual timings for the template * @param {boolean} [fullWidth=false] When set then it renders the timeline in the whole available space. * @returns {TemplateResult|string} The template for the timings timeline */ [timingsTemplate](timings, visualTimings, fullWidth=false) { if (!visualTimings) { return ''; } const { total, delay, blocked, connect, dns, receive, send, ssl, wait, } = visualTimings; const styles = { width: fullWidth ? '100%' : `${total}%`, }; return html` ${fullWidth ? '' : this[timingTemplate](delay, 'delay')} <div class="entry-timings-value" style="${styleMap(styles)}"> ${this[timingTemplate](blocked, 'blocked', 'Blocked', timings)} ${this[timingTemplate](dns, 'dns', 'DNS', timings)} ${this[timingTemplate](connect, 'connect', 'Connecting', timings)} ${this[timingTemplate](ssl, 'ssl', 'SSL negotiation', timings)} ${this[timingTemplate](send, 'send', 'Sending', timings)} ${this[timingTemplate](wait, 'wait', 'Waiting', timings)} ${this[timingTemplate](receive, 'receive', 'Receiving', timings)} </div> `; } /** * @param {number} width * @param {string} type Added to the class name. * @param {string=} label The label to use in the title attribute * @param {Timings=} timings The entry's timings object * @returns {TemplateResult|string} The template for a timing timeline item */ [timingTemplate](width, type, label, timings) { if (!width) { return ''; } const styles = { width: `${width}%`, }; const classes = { 'timing-entry': true, [type]: true, }; const time = timings && timings[type]; const title = typeof time === 'number' ? `${label}: ${Math.round(time)}ms` : undefined; return html` <div class="${classMap(classes)}" style="${styleMap(styles)}" title="${ifDefined(title)}"></div> `; } /** * @param {RenderedEntry} entry The entry to render * @returns {TemplateResult} The template for an entry details. */ [entryDetails](entry) { const { id } = entry; const selectedTab = this[selectedTabsValue][id] || 0; return html` <section class="entry-details"> ${this[entryDetailsTabsTemplate](entry, selectedTab)} <div class="details-content" tabindex="0"> ${this[entryDetailsContentTemplate](entry, selectedTab)} </div> </section> `; } /** * @param {RenderedEntry} entry The entry to render * @param {number} selected The index of the selected tab * @returns {TemplateResult} The template for entry details content tabs. */ [entryDetailsTabsTemplate](entry, selected) { const { id, request, response } = entry; const { postData, cookies: requestCookies } = request; const { content, cookies: responseCookies } = response; const hashRequestContent = !!postData && !!postData.text; const hashResponseContent = !!content && !!content.text; const hasRequestCookies = Array.isArray(requestCookies) && !!requestCookies.length; const hasResponseCookies = Array.isArray(responseCookies) && !!responseCookies.length; const hasCookies = hasRequestCookies || hasResponseCookies; return html` <anypoint-tabs .selected="${selected}" @selected="${this[detailsTabSelectionHandler]}" data-entry="${id}"> <anypoint-tab>Request</anypoint-tab> <anypoint-tab>Response</anypoint-tab> <anypoint-tab ?hidden="${!hashRequestContent}">Request content</anypoint-tab> <anypoint-tab ?hidden="${!hashResponseContent}">Response content</anypoint-tab> <anypoint-tab ?hidden="${!hasCookies}">Cookies</anypoint-tab> </anypoint-tabs> `; } /** * @param {RenderedEntry} entry The entry to render * @param {number} selected The index of the selected tab * @returns {TemplateResult|string} The template for entry details content. */ [entryDetailsContentTemplate](entry, selected) { switch (selected) { case 0: return this[entryDetailsRequestTemplate](entry); case 1: return this[entryDetailsResponseTemplate](entry); case 2: return this[entryDetailsRequestBodyTemplate](entry); case 3: return this[entryDetailsResponseBodyTemplate](entry); case 4: return this[entryDetailsCookiesTemplate](entry); default: return ''; } } /** * @param {RenderedEntry} entry The entry to render * @returns {TemplateResult} The template for entry's request content. */ [entryDetailsRequestTemplate](entry) { const { request, requestFormattedDate, serverIPAddress, requestSizes } = entry; const { headers, url, method, httpVersion, queryString } = request; return html` <div class="entry-details-title">Request on ${requestFormattedDate}</div> <dl class="details-list"> <dt>General</dt> <dd> ${this[definitionTemplate]('URL', url)} ${this[definitionTemplate]('HTTP version', httpVersion)} ${this[definitionTemplate]('Operation', method)} ${this[definitionTemplate]('Remote Address', serverIPAddress)} </dd> ${this[headersTemplate](headers)} ${this[queryParamsTemplate](queryString)} ${this[sizesTemplate](requestSizes)} </dl> `; } /** * @param {RenderedEntry} entry The entry to render * @returns {TemplateResult} The template for entry's response content. */ [entryDetailsResponseTemplate](entry) { const { response, responseSizes } = entry; const { headers } = response; return html` <dl class="details-list"> ${this[headersTemplate](headers)} ${this[sizesTemplate](responseSizes)} </dl> `; } /** * @param {RenderedEntry} entry The entry to render * @returns {TemplateResult} The template for entry's request body preview. */ [entryDetailsRequestBodyTemplate](entry) { const { request } = entry; const { postData } = request; if (!postData || !postData.text) { return html`<p>No request body data.</p>`; } return html` <pre><code class="body-preview">${postData.text}</code></pre> `; } /** * @param {RenderedEntry} entry The entry to render * @returns {TemplateResult} The template for entry's response body preview. */ [entryDetailsResponseBodyTemplate](entry) { const { response } = entry; const { content } = response; if (!content || !content.text) { return html`<p>No request body data.</p>`; } return html` <pre><code class="body-preview">${content.text}</code></pre> `; } /** * @param {RenderedEntry} entry The entry to render * @returns {TemplateResult} The template for entry's cookies. */ [entryDetailsCookiesTemplate](entry) { const { request, response } = entry; const { cookies: requestCookies } = request; const { cookies: responseCookies } = response; const hasRequestCookies = Array.isArray(requestCookies) && !!requestCookies.length; const hasResponseCookies = Array.isArray(responseCookies) && !!responseCookies.length; return html` <dl class="details-list"> <dt>Request cookies</dt> <dd> ${hasRequestCookies ? requestCookies.map((item) => this[definitionTemplate](item.name, item.value)) : html`No cookies recorded.`} </dd> <dt>Response cookies</dt> <dd> ${hasResponseCookies ? responseCookies.map((item) => this[definitionTemplate](item.name, item.value)) : html`No cookies recorded.`} </dd> </dl> `; } /** * @param {string} term Definition label * @param {string} value Definition value * @returns {TemplateResult|string} The template for the definition. */ [definitionTemplate](term, value) { if (!value) { return ''; } return html` <p class="definition"> <dfn>${term}:</dfn> ${value} </p> `; } /** * @param {Header[]} headers * @returns {TemplateResult|string} The template for the list of headers. */ [headersTemplate](headers) { return html` <dt>Headers</dt> <dd> ${Array.isArray(headers) && headers.length ? headers.map((item) => this[definitionTemplate](item.name, item.value)) : html`No headers recorded.`} </dd> `; } /** * @param {QueryString[]} params * @returns {TemplateResult|string} The template for the query parameters. */ [queryParamsTemplate](params) { if (!Array.isArray(params) || !params.length) { return ''; } return html` <dt>Query parameters</dt> <dd> ${params.map((item) => this[definitionTemplate](item.name, item.value))} </dd> `; } /** * @param {EntrySizing} sizes * @returns {TemplateResult} The template for sizes information */ [sizesTemplate](sizes) { return html` <dt>Size</dt> <dd> ${this[definitionTemplate]('Headers', sizes.headers)} ${this[definitionTemplate]('Body', sizes.body)} ${this[definitionTemplate]('Total', sizes.sum)} </dd>`; } }