Update project error UI (#6432)

* update default title to "an unexpected error occurred"

* show stack trace in error, if avail

* add "copy to clipboard"

* wrap browser errors

* add tests

* improve md formatting

* update desktopgui tests

* update
This commit is contained in:
Zach Bloomquist
2020-02-14 16:29:51 -05:00
committed by GitHub
parent 15c3e95429
commit dd3b63aa0b
7 changed files with 106 additions and 13 deletions
@@ -128,17 +128,41 @@ describe('Error Message', function () {
this.start()
cy.get('.error').contains('ReferenceError: alsdkjf is not defined')
cy.get('details').should('not.have.attr', 'open')
cy.get('details').click().should('have.attr', 'open')
cy.get('summary').should('contain', 'ReferenceError')
cy.get('details.details-body').should('not.have.attr', 'open')
cy.get('details.details-body').click().should('have.attr', 'open')
cy.get('details.details-body > summary').should('contain', 'ReferenceError')
})
it('doesn\'t show error details if not provided', function () {
cy.stub(this.ipc, 'onProjectError').yields(null, this.err)
this.start()
cy.get('details.details-body > summary').should('not.exist')
})
it('shows error stack trace if provided', function () {
const err = new Error('foo')
err.stack = 'bar'
cy.stub(this.ipc, 'onProjectError').yields(null, err)
this.start()
cy.get('.error').contains('foo')
cy.get('details.stacktrace').should('not.have.attr', 'open')
cy.get('details.stacktrace').click().should('have.attr', 'open')
cy.get('details.stacktrace').should('contain', 'bar')
})
it('doesn\'t show error stack trace if not provided', function () {
const err = new Error()
delete err.stack
cy.stub(this.ipc, 'onProjectError').yields(null, err)
this.start()
cy.get('.error')
cy.get('summary').should('not.exist')
cy.get('details.stacktrace > summary').should('not.be.visible')
})
it('shows abbreviated error details if only one line', function () {
@@ -155,7 +179,7 @@ describe('Error Message', function () {
this.start()
cy.get('.error').contains('ReferenceError: alsdkjf is not defined')
cy.get('summary').should('not.exist')
cy.get('details.details-body > summary').should('not.exist')
})
it('opens links outside of electron', function () {
@@ -197,7 +221,7 @@ describe('Error Message', function () {
this.ipc.openProject.rejects(this.detailsErr)
this.start()
cy.get('details').click()
cy.get('details.details-body').click()
cy.get('nav').should('be.visible')
cy.get('footer').should('be.visible')
})
@@ -207,7 +231,7 @@ describe('Error Message', function () {
this.ipc.openProject.rejects(this.detailsErr)
this.start()
cy.get('details').click()
cy.get('details.details-body').click()
cy.contains('Try Again').should('be.visible')
cy.get('.full-alert pre').should('have.css', 'overflow', 'auto')
})
@@ -490,6 +490,7 @@ describe('Settings', () => {
describe('errors', () => {
beforeEach(function () {
this.err = {
title: 'Foo Title',
message: 'Port \'2020\' is already in use.',
name: 'Error',
port: 2020,
@@ -514,11 +515,11 @@ describe('Settings', () => {
})
it('displays errors', () => {
cy.contains('Can\'t start server')
cy.contains('Foo Title')
})
it('displays config after error is fixed', function () {
cy.contains('Can\'t start server').then(() => {
cy.contains('Foo Title').then(() => {
this.ipc.openProject.onCall(1).resolves(this.config)
this.ipc.onConfigChanged.yield()
+1
View File
@@ -64,5 +64,6 @@ register('updater:run', false)
register('window:open')
register('window:close')
register('onboarding:closed')
register('set:clipboard:text')
export default ipc
@@ -7,6 +7,26 @@ import { configFileFormatted } from '../lib/config-file-formatted'
import Markdown from 'markdown-it'
const _copyErrorDetails = (err) => {
let details = [
`**Message:** ${err.message}`,
]
if (err.details) {
details.push(`**Details:** ${err.details}`)
}
if (err.title) {
details.unshift(`**Title:** ${err.title}`)
}
if (err.stack2) {
details.push(`**Stack trace:**\n\`\`\`\n${err.stack2}\n\`\`\``)
}
ipc.setClipboardText(details.join('\n\n'))
}
const md = new Markdown({
html: true,
linkify: true,
@@ -20,7 +40,7 @@ const ErrorDetails = observer(({ err }) => {
if (detailsBody) {
return (
<pre>
<details>
<details className='details-body'>
<summary>{detailsTitle}</summary>
{detailsBody}
</details>
@@ -61,7 +81,7 @@ class ErrorMessage extends Component {
<div className='full-alert alert alert-danger error'>
<p className='header'>
<i className='fas fa-exclamation-triangle'></i>{' '}
<strong>{err.title || 'Can\'t start server'}</strong>
<strong>{err.title || 'An unexpected error occurred'}</strong>
</p>
<span className='alert-content'>
<div ref={(node) => this.errorMessageNode = node} dangerouslySetInnerHTML={{
@@ -76,7 +96,22 @@ class ErrorMessage extends Component {
<p>To fix, stop the other running process or change the port in {configFileFormatted(this.props.project.configFile)}</p>
</div>
)}
{err.stack2 && (
<details className='stacktrace'>
<summary>Stack trace</summary>
<pre>{err.stack2}</pre>
</details>
)}
</span>
<button
className='btn btn-default btn-sm'
onClick={() => {
_copyErrorDetails(err)
}}
>
<i className='fas fa-copy'></i>{' '}
Copy to Clipboard
</button>
<button
className='btn btn-default btn-sm'
onClick={this.props.onTryAgain}
@@ -209,6 +209,9 @@ export default class Project {
}
@action setError (err = {}) {
// for some reason, the original `stack` is unavailable on `err` once it is set on the model
// `stack2` remains usable though, for some reason
err.stack2 = err.stack
this.error = err
}
+9 -2
View File
@@ -1,6 +1,6 @@
_ = require("lodash")
ipc = require("electron").ipcMain
shell = require("electron").shell
{ shell, clipboard } = require('electron')
debug = require('debug')('cypress:server:events')
pluralize = require("pluralize")
stripAnsi = require("strip-ansi")
@@ -109,7 +109,10 @@ handleEvent = (options, bus, event, id, type, arg) ->
onBrowserClose: ->
send({browserClosed: true})
})
.catch(sendErr)
.catch (err) =>
err.title ?= 'Error launching browser'
sendErr(err)
when "begin:auth"
onMessage = (msg) ->
@@ -299,6 +302,10 @@ handleEvent = (options, bus, event, id, type, arg) ->
err.apiUrl = apiUrl
sendErr(err)
when "set:clipboard:text"
clipboard.writeText(arg)
sendNull()
else
throw new Error("No ipc event registered for: '#{type}'")
@@ -699,3 +699,25 @@ describe "lib/gui/events", ->
expect(err.name).to.equal("ECONNREFUSED 127.0.0.1:1234")
expect(err.message).to.equal("ECONNREFUSED 127.0.0.1:1234")
expect(err.apiUrl).to.equal(konfig("api_url"))
describe "launch:browser", ->
it "launches browser via openProject", ->
sinon.stub(openProject, 'launch').callsFake (browser, spec, opts) ->
expect(browser).to.eq('foo')
expect(spec).to.eq('bar')
opts.onBrowserOpen()
opts.onBrowserClose()
Promise.resolve()
@handleEvent("launch:browser", { browser: 'foo', spec: 'bar' }).then =>
expect(@send.getCall(0).args[1].data).to.include({ browserOpened: true })
expect(@send.getCall(1).args[1].data).to.include({ browserClosed: true })
it "wraps error titles if not set", ->
err = new Error('foo')
sinon.stub(openProject, 'launch').rejects(err)
@handleEvent("launch:browser", {}).then =>
expect(@send.getCall(0).args[1].__error).to.include({ message: 'foo', title: 'Error launching browser' })