diff --git a/lighthouse-core/report/html/renderer/details-renderer.js b/lighthouse-core/report/html/renderer/details-renderer.js index 84a5d49f6cb8..5f5cd7910582 100644 --- a/lighthouse-core/report/html/renderer/details-renderer.js +++ b/lighthouse-core/report/html/renderer/details-renderer.js @@ -63,9 +63,9 @@ class DetailsRenderer { return null; default: { - // @ts-ignore tsc thinks this unreachable, but ts-ignore for error message just in case. - const detailsType = details.type; - throw new Error(`Unknown type: ${detailsType}`); + // @ts-ignore tsc thinks this is unreachable, but be forward compatible + // with new unexpected detail types. + return this._renderUnknown(details.type, details); } } } @@ -188,6 +188,22 @@ class DetailsRenderer { return element; } + /** + * @param {string} type + * @param {*} value + */ + _renderUnknown(type, value) { + // eslint-disable-next-line no-console + console.error(`Unknown details type: ${type}`, value); + const element = this._dom.createElement('details', 'lh-unknown'); + this._dom.createChildOf(element, 'summary').textContent = + `We don't know how to render audit details of type \`${type}\`. ` + + 'The Lighthouse version that collected this data is likely newer than the Lighthouse ' + + 'version of the report renderer. Expand for the raw JSON.'; + this._dom.createChildOf(element, 'pre').textContent = JSON.stringify(value, null, 2); + return element; + } + /** * Render a details item value for embedding in a table. Renders the value * based on the heading's valueType, unless the value itself has a `type` @@ -218,7 +234,7 @@ class DetailsRenderer { return this.renderTextURL(value.value); } default: { - throw new Error(`Unknown valueType: ${value.type}`); + return this._renderUnknown(value.type, value); } } } @@ -267,7 +283,7 @@ class DetailsRenderer { } } default: { - throw new Error(`Unknown valueType: ${heading.valueType}`); + return this._renderUnknown(heading.valueType, value); } } } diff --git a/lighthouse-core/report/html/report-styles.css b/lighthouse-core/report/html/report-styles.css index 880c39b1659d..6116e483a17f 100644 --- a/lighthouse-core/report/html/report-styles.css +++ b/lighthouse-core/report/html/report-styles.css @@ -1391,6 +1391,11 @@ display: block; } +.lh-unknown pre { + overflow: scroll; + border: solid 1px var(--color-gray-200); +} + .lh-text__url > a { color: inherit; text-decoration: none; diff --git a/lighthouse-core/test/report/html/renderer/details-renderer-test.js b/lighthouse-core/test/report/html/renderer/details-renderer-test.js index ba7cf34c7dfd..27660ea05b8b 100644 --- a/lighthouse-core/test/report/html/renderer/details-renderer-test.js +++ b/lighthouse-core/test/report/html/renderer/details-renderer-test.js @@ -194,14 +194,18 @@ describe('DetailsRenderer', () => { assert.strictEqual(diagnosticEl, null); }); - it('throws on unknown details type', () => { + it('renders an unknown details type', () => { // Disallowed by type system, but test that we get an error message out just in case. const details = { type: 'imaginary', items: 5, }; - assert.throws(() => renderer.render(details), /^Error: Unknown type: imaginary$/); + const el = renderer.render(details); + const summaryEl = el.querySelector('summary'); + expect(summaryEl.textContent) + .toContain('We don\'t know how to render audit details of type `imaginary`'); + assert.strictEqual(el.lastChild.textContent, JSON.stringify(details, null, 2)); }); }); @@ -451,7 +455,7 @@ describe('DetailsRenderer', () => { assert.strictEqual(codeItemEl.innerHTML, '
invalid-url://example.com/'); }); - it('throws on unknown heading itemType', () => { + it('renders an unknown heading itemType', () => { // Disallowed by type system, but test that we get an error message out just in case. const details = { type: 'table', @@ -459,10 +463,15 @@ describe('DetailsRenderer', () => { items: [{content: 'some string'}], }; - assert.throws(() => renderer.render(details), /^Error: Unknown valueType: notRealValueType$/); + const el = renderer.render(details); + const unknownEl = el.querySelector('td.lh-table-column--notRealValueType .lh-unknown'); + const summaryEl = unknownEl.querySelector('summary'); + expect(summaryEl.textContent) + .toContain('We don\'t know how to render audit details of type `notRealValueType`'); + assert.strictEqual(unknownEl.lastChild.textContent, '"some string"'); }); - it('throws on unknown item object type', () => { + it('renders an unknown item object type', () => { // Disallowed by type system, but test that we get an error message out just in case. const item = { type: 'imaginaryItem', @@ -475,7 +484,12 @@ describe('DetailsRenderer', () => { items: [{content: item}], }; - assert.throws(() => renderer.render(details), /^Error: Unknown valueType: imaginaryItem$/); + const el = renderer.render(details); + const unknownEl = el.querySelector('td.lh-table-column--url .lh-unknown'); + const summaryEl = unknownEl.querySelector('summary'); + expect(summaryEl.textContent) + .toContain('We don\'t know how to render audit details of type `imaginaryItem`'); + assert.strictEqual(unknownEl.lastChild.textContent, JSON.stringify(item, null, 2)); }); it('uses the item\'s type over the heading type', () => {