notifications-spec.coffee 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  1. fs = require 'fs-plus'
  2. path = require 'path'
  3. temp = require('temp').track()
  4. {Notification} = require 'atom'
  5. NotificationElement = require '../lib/notification-element'
  6. NotificationIssue = require '../lib/notification-issue'
  7. UserUtils = require '../lib/user-utilities'
  8. {generateFakeFetchResponses, generateException} = require './helper'
  9. describe "Notifications", ->
  10. [workspaceElement, activationPromise] = []
  11. beforeEach ->
  12. workspaceElement = atom.views.getView(atom.workspace)
  13. atom.notifications.clear()
  14. activationPromise = atom.packages.activatePackage('notifications')
  15. waitsForPromise ->
  16. activationPromise
  17. describe "when the package is activated", ->
  18. it "attaches an atom-notifications element to the dom", ->
  19. expect(workspaceElement.querySelector('atom-notifications')).toBeDefined()
  20. describe "when there are notifications before activation", ->
  21. beforeEach ->
  22. waitsForPromise ->
  23. # Wrapped in Promise.resolve so this test continues to work on earlier versions of Pulsar
  24. Promise.resolve(atom.packages.deactivatePackage('notifications'))
  25. it "displays all non displayed notifications", ->
  26. warning = new Notification('warning', 'Un-displayed warning')
  27. error = new Notification('error', 'Displayed error')
  28. error.setDisplayed(true)
  29. atom.notifications.addNotification(error)
  30. atom.notifications.addNotification(warning)
  31. activationPromise = atom.packages.activatePackage('notifications')
  32. waitsForPromise ->
  33. activationPromise
  34. runs ->
  35. notificationContainer = workspaceElement.querySelector('atom-notifications')
  36. notification = notificationContainer.querySelector('atom-notification.warning')
  37. expect(notification).toExist()
  38. notification = notificationContainer.querySelector('atom-notification.error')
  39. expect(notification).not.toExist()
  40. describe "when notifications are added to atom.notifications", ->
  41. notificationContainer = null
  42. beforeEach ->
  43. enableInitNotification = atom.notifications.addSuccess('A message to trigger initialization', dismissable: true)
  44. enableInitNotification.dismiss()
  45. advanceClock(NotificationElement::visibilityDuration)
  46. advanceClock(NotificationElement::animationDuration)
  47. notificationContainer = workspaceElement.querySelector('atom-notifications')
  48. jasmine.attachToDOM(workspaceElement)
  49. generateFakeFetchResponses()
  50. it "adds an atom-notification element to the container with a class corresponding to the type", ->
  51. expect(notificationContainer.childNodes.length).toBe 0
  52. atom.notifications.addSuccess('A message')
  53. notification = notificationContainer.querySelector('atom-notification.success')
  54. expect(notificationContainer.childNodes.length).toBe 1
  55. expect(notification).toHaveClass 'success'
  56. expect(notification.querySelector('.message').textContent.trim()).toBe 'A message'
  57. expect(notification.querySelector('.meta')).not.toBeVisible()
  58. atom.notifications.addInfo('A message')
  59. expect(notificationContainer.childNodes.length).toBe 2
  60. expect(notificationContainer.querySelector('atom-notification.info')).toBeDefined()
  61. atom.notifications.addWarning('A message')
  62. expect(notificationContainer.childNodes.length).toBe 3
  63. expect(notificationContainer.querySelector('atom-notification.warning')).toBeDefined()
  64. atom.notifications.addError('A message')
  65. expect(notificationContainer.childNodes.length).toBe 4
  66. expect(notificationContainer.querySelector('atom-notification.error')).toBeDefined()
  67. atom.notifications.addFatalError('A message')
  68. expect(notificationContainer.childNodes.length).toBe 5
  69. expect(notificationContainer.querySelector('atom-notification.fatal')).toBeDefined()
  70. it "displays notification with a detail when a detail is specified", ->
  71. atom.notifications.addInfo('A message', detail: 'Some detail')
  72. notification = notificationContainer.childNodes[0]
  73. expect(notification.querySelector('.detail').textContent).toContain 'Some detail'
  74. atom.notifications.addInfo('A message', detail: null)
  75. notification = notificationContainer.childNodes[1]
  76. expect(notification.querySelector('.detail')).not.toBeVisible()
  77. atom.notifications.addInfo('A message', detail: 1)
  78. notification = notificationContainer.childNodes[2]
  79. expect(notification.querySelector('.detail').textContent).toContain '1'
  80. atom.notifications.addInfo('A message', detail: {something: 'ok'})
  81. notification = notificationContainer.childNodes[3]
  82. expect(notification.querySelector('.detail').textContent).toContain 'Object'
  83. atom.notifications.addInfo('A message', detail: ['cats', 'ok'])
  84. notification = notificationContainer.childNodes[4]
  85. expect(notification.querySelector('.detail').textContent).toContain 'cats,ok'
  86. it "does not add the has-stack class if a stack is provided without any detail", ->
  87. atom.notifications.addInfo('A message', stack: 'Some stack')
  88. notification = notificationContainer.childNodes[0]
  89. notificationElement = notificationContainer.querySelector('atom-notification.info')
  90. expect(notificationElement).not.toHaveClass 'has-stack'
  91. it "renders the message as sanitized markdown", ->
  92. atom.notifications.addInfo('test <b>html</b> <iframe>but sanitized</iframe>')
  93. notification = notificationContainer.childNodes[0]
  94. expect(notification.querySelector('.message').innerHTML).toContain(
  95. 'test <b>html</b> '
  96. )
  97. describe "when a dismissable notification is added", ->
  98. it "is removed when Notification::dismiss() is called", ->
  99. notification = atom.notifications.addSuccess('A message', dismissable: true)
  100. notificationElement = notificationContainer.querySelector('atom-notification.success')
  101. expect(notificationContainer.childNodes.length).toBe 1
  102. notification.dismiss()
  103. advanceClock(NotificationElement::visibilityDuration)
  104. expect(notificationElement).toHaveClass 'remove'
  105. advanceClock(NotificationElement::animationDuration)
  106. expect(notificationContainer.childNodes.length).toBe 0
  107. it "is removed when the close icon is clicked", ->
  108. jasmine.attachToDOM(workspaceElement)
  109. waitsForPromise ->
  110. atom.workspace.open()
  111. runs ->
  112. notification = atom.notifications.addSuccess('A message', dismissable: true)
  113. notificationElement = notificationContainer.querySelector('atom-notification.success')
  114. expect(notificationContainer.childNodes.length).toBe 1
  115. notificationElement.focus()
  116. notificationElement.querySelector('.close.icon').click()
  117. advanceClock(NotificationElement::visibilityDuration)
  118. expect(notificationElement).toHaveClass 'remove'
  119. advanceClock(NotificationElement::animationDuration)
  120. expect(notificationContainer.childNodes.length).toBe 0
  121. it "is removed when core:cancel is triggered", ->
  122. notification = atom.notifications.addSuccess('A message', dismissable: true)
  123. notificationElement = notificationContainer.querySelector('atom-notification.success')
  124. expect(notificationContainer.childNodes.length).toBe 1
  125. atom.commands.dispatch(workspaceElement, 'core:cancel')
  126. advanceClock(NotificationElement::visibilityDuration * 3)
  127. expect(notificationElement).toHaveClass 'remove'
  128. advanceClock(NotificationElement::animationDuration * 3)
  129. expect(notificationContainer.childNodes.length).toBe 0
  130. it "focuses the active pane only if the dismissed notification has focus", ->
  131. jasmine.attachToDOM(workspaceElement)
  132. waitsForPromise ->
  133. atom.workspace.open()
  134. runs ->
  135. notification1 = atom.notifications.addSuccess('First message', dismissable: true)
  136. notification2 = atom.notifications.addError('Second message', dismissable: true)
  137. notificationElement1 = notificationContainer.querySelector('atom-notification.success')
  138. notificationElement2 = notificationContainer.querySelector('atom-notification.error')
  139. expect(notificationContainer.childNodes.length).toBe 2
  140. notificationElement2.focus()
  141. notification1.dismiss()
  142. advanceClock(NotificationElement::visibilityDuration)
  143. advanceClock(NotificationElement::animationDuration)
  144. expect(notificationContainer.childNodes.length).toBe 1
  145. expect(notificationElement2).toHaveFocus()
  146. notificationElement2.querySelector('.close.icon').click()
  147. advanceClock(NotificationElement::visibilityDuration)
  148. advanceClock(NotificationElement::animationDuration)
  149. expect(notificationContainer.childNodes.length).toBe 0
  150. expect(atom.views.getView(atom.workspace.getActiveTextEditor())).toHaveFocus()
  151. describe "when an autoclose notification is added", ->
  152. [notification, model] = []
  153. beforeEach ->
  154. model = atom.notifications.addSuccess('A message')
  155. notification = notificationContainer.querySelector('atom-notification.success')
  156. it "closes and removes the message after a given amount of time", ->
  157. expect(notification).not.toHaveClass 'remove'
  158. advanceClock(NotificationElement::visibilityDuration)
  159. expect(notification).toHaveClass 'remove'
  160. expect(notificationContainer.childNodes.length).toBe 1
  161. advanceClock(NotificationElement::animationDuration)
  162. expect(notificationContainer.childNodes.length).toBe 0
  163. describe "when the notification is clicked", ->
  164. beforeEach ->
  165. notification.click()
  166. it "makes the notification dismissable", ->
  167. expect(notification).toHaveClass 'has-close'
  168. advanceClock(NotificationElement::visibilityDuration)
  169. expect(notification).not.toHaveClass 'remove'
  170. it "removes the notification when dismissed", ->
  171. model.dismiss()
  172. expect(notification).toHaveClass 'remove'
  173. describe "when the default timeout setting is changed", ->
  174. [notification] = []
  175. beforeEach ->
  176. atom.config.set("notifications.defaultTimeout", 1000)
  177. atom.notifications.addSuccess('A message')
  178. notification = notificationContainer.querySelector('atom-notification.success')
  179. it "uses the setting value for the autoclose timeout", ->
  180. expect(notification).not.toHaveClass 'remove'
  181. advanceClock(1000)
  182. expect(notification).toHaveClass 'remove'
  183. describe "when the `description` option is used", ->
  184. it "displays the description text in the .description element", ->
  185. atom.notifications.addSuccess('A message', description: 'This is [a link](http://atom.io)')
  186. notification = notificationContainer.querySelector('atom-notification.success')
  187. expect(notification).toHaveClass('has-description')
  188. expect(notification.querySelector('.meta')).toBeVisible()
  189. expect(notification.querySelector('.description').textContent.trim()).toBe 'This is a link'
  190. expect(notification.querySelector('.description a').href).toBe 'http://atom.io/'
  191. describe "when the `buttons` options is used", ->
  192. it "displays the buttons in the .description element", ->
  193. clicked = []
  194. atom.notifications.addSuccess 'A message',
  195. buttons: [{
  196. text: 'Button One'
  197. className: 'btn-one'
  198. onDidClick: -> clicked.push 'one'
  199. }, {
  200. text: 'Button Two'
  201. className: 'btn-two'
  202. onDidClick: -> clicked.push 'two'
  203. }]
  204. notification = notificationContainer.querySelector('atom-notification.success')
  205. expect(notification).toHaveClass('has-buttons')
  206. expect(notification.querySelector('.meta')).toBeVisible()
  207. btnOne = notification.querySelector('.btn-one')
  208. btnTwo = notification.querySelector('.btn-two')
  209. expect(btnOne).toHaveClass 'btn-success'
  210. expect(btnOne.textContent).toBe 'Button One'
  211. expect(btnTwo).toHaveClass 'btn-success'
  212. expect(btnTwo.textContent).toBe 'Button Two'
  213. btnTwo.click()
  214. btnOne.click()
  215. expect(clicked).toEqual ['two', 'one']
  216. describe "when an exception is thrown", ->
  217. [notificationContainer, fatalError, issueTitle, issueBody] = []
  218. describe "when the editor is in dev mode", ->
  219. beforeEach ->
  220. spyOn(atom, 'inDevMode').andReturn true
  221. generateException()
  222. notificationContainer = workspaceElement.querySelector('atom-notifications')
  223. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  224. it "does not display a notification", ->
  225. expect(notificationContainer.childNodes.length).toBe 0
  226. expect(fatalError).toBe null
  227. describe "when the exception has no core or package paths in the stack trace", ->
  228. it "does not display a notification", ->
  229. atom.notifications.clear()
  230. spyOn(atom, 'inDevMode').andReturn false
  231. handler = jasmine.createSpy('onWillThrowErrorHandler')
  232. atom.onWillThrowError(handler)
  233. # Fake an unhandled error with a call stack located outside of the source
  234. # of Pulsar or an Pulsar package
  235. fs.readFile(__dirname, ->
  236. err = new Error()
  237. err.stack = 'FakeError: foo is not bar\n at blah.fakeFunc (directory/fakefile.js:1:25)'
  238. throw err
  239. )
  240. waitsFor ->
  241. handler.callCount is 1
  242. runs ->
  243. expect(atom.notifications.getNotifications().length).toBe 0
  244. describe "when the message contains a newline", ->
  245. it "removes the newline when generating the issue title", ->
  246. message = "Uncaught Error: Cannot read property 'object' of undefined\nTypeError: Cannot read property 'object' of undefined"
  247. atom.notifications.addFatalError(message)
  248. notificationContainer = workspaceElement.querySelector('atom-notifications')
  249. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  250. waitsForPromise ->
  251. fatalError.getRenderPromise().then ->
  252. issueTitle = fatalError.issue.getIssueTitle()
  253. runs ->
  254. expect(issueTitle).not.toContain "\n"
  255. expect(issueTitle).toBe "Uncaught Error: Cannot read property 'object' of undefinedTypeError: Cannot read property 'objec..."
  256. describe "when the message contains continguous newlines", ->
  257. it "removes the newlines when generating the issue title", ->
  258. message = "Uncaught Error: Cannot do the thing\n\nSuper sorry about this"
  259. atom.notifications.addFatalError(message)
  260. notificationContainer = workspaceElement.querySelector('atom-notifications')
  261. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  262. waitsForPromise ->
  263. fatalError.getRenderPromise().then ->
  264. issueTitle = fatalError.issue.getIssueTitle()
  265. runs ->
  266. expect(issueTitle).toBe "Uncaught Error: Cannot do the thingSuper sorry about this"
  267. describe "when there are multiple packages in the stack trace", ->
  268. beforeEach ->
  269. stack = """
  270. TypeError: undefined is not a function
  271. at Object.module.exports.Pane.promptToSaveItem [as defaultSavePrompt] (/Applications/Pulsar.app/Contents/Resources/app/src/pane.js:490:23)
  272. at Pane.promptToSaveItem (/Users/someguy/.atom/packages/save-session/lib/save-prompt.coffee:21:15)
  273. at Pane.module.exports.Pane.destroyItem (/Applications/Pulsar.app/Contents/Resources/app/src/pane.js:442:18)
  274. at HTMLDivElement.<anonymous> (/Applications/Pulsar.app/Contents/Resources/app/node_modules/tabs/lib/tab-bar-view.js:174:22)
  275. at space-pen-ul.jQuery.event.dispatch (/Applications/Pulsar.app/Contents/Resources/app/node_modules/archive-view/node_modules/atom-space-pen-views/node_modules/space-pen/vendor/jquery.js:4676:9)
  276. at space-pen-ul.elemData.handle (/Applications/Pulsar.app/Contents/Resources/app/node_modules/archive-view/node_modules/atom-space-pen-views/node_modules/space-pen/vendor/jquery.js:4360:46)
  277. """
  278. detail = 'ok'
  279. atom.notifications.addFatalError('TypeError: undefined', {detail, stack})
  280. notificationContainer = workspaceElement.querySelector('atom-notifications')
  281. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  282. spyOn(fs, 'realpathSync').andCallFake (p) -> p
  283. spyOn(fatalError.issue, 'getPackagePathsByPackageName').andCallFake ->
  284. 'save-session': '/Users/someguy/.atom/packages/save-session'
  285. 'tabs': '/Applications/Pulsar.app/Contents/Resources/app/node_modules/tabs'
  286. it "chooses the first package in the trace", ->
  287. expect(fatalError.issue.getPackageName()).toBe 'save-session'
  288. describe "when an exception is thrown from a package", ->
  289. beforeEach ->
  290. issueTitle = null
  291. issueBody = null
  292. spyOn(atom, 'inDevMode').andReturn false
  293. generateFakeFetchResponses()
  294. spyOn(UserUtils, 'getPackageVersionShippedWithPulsar').andCallFake -> '0.0.0'
  295. generateException()
  296. notificationContainer = workspaceElement.querySelector('atom-notifications')
  297. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  298. it "displays a fatal error with the package name in the error", ->
  299. waitsForPromise ->
  300. fatalError.getRenderPromise().then ->
  301. issueTitle = fatalError.issue.getIssueTitle()
  302. fatalError.issue.getIssueBody().then (result) ->
  303. issueBody = result
  304. runs ->
  305. expect(notificationContainer.childNodes.length).toBe 1
  306. expect(fatalError).toHaveClass 'has-close'
  307. expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined'
  308. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/pulsar\">notifications package</a>"
  309. expect(fatalError.issue.getPackageName()).toBe 'notifications'
  310. button = fatalError.querySelector('.btn')
  311. expect(button.textContent).toContain 'Create issue on the notifications package'
  312. expect(issueTitle).toContain '$ATOM_HOME'
  313. expect(issueTitle).not.toContain process.env.ATOM_HOME
  314. expect(issueBody).toMatch /Pulsar\*\*: [0-9].[0-9]+.[0-9]+/ig
  315. expect(issueBody).not.toMatch /Unknown/ig
  316. expect(issueBody).toContain 'ReferenceError: a is not defined'
  317. expect(issueBody).toContain 'Thrown From**: [notifications](https://github.com/pulsar-edit/pulsar) package '
  318. expect(issueBody).toContain '### Non-Core Packages'
  319. # FIXME: this doesnt work on the test server. `apm ls` is not working for some reason.
  320. # expect(issueBody).toContain 'notifications '
  321. it "standardizes platform separators on #win32", ->
  322. waitsForPromise ->
  323. fatalError.getRenderPromise().then ->
  324. issueTitle = fatalError.issue.getIssueTitle()
  325. runs ->
  326. expect(issueTitle).toContain path.posix.sep
  327. expect(issueTitle).not.toContain path.win32.sep
  328. describe "when an exception contains the user's home directory", ->
  329. beforeEach ->
  330. issueTitle = null
  331. spyOn(atom, 'inDevMode').andReturn false
  332. generateFakeFetchResponses()
  333. # Create a custom error message that contains the user profile but not ATOM_HOME
  334. try
  335. a + 1
  336. catch e
  337. home = if process.platform is 'win32' then process.env.USERPROFILE else process.env.HOME
  338. errMsg = "#{e.toString()} in #{home}#{path.sep}somewhere"
  339. window.onerror.call(window, errMsg, '/dev/null', 2, 3, e)
  340. notificationContainer = workspaceElement.querySelector('atom-notifications')
  341. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  342. it "replaces the directory with a ~", ->
  343. waitsForPromise ->
  344. fatalError.getRenderPromise().then ->
  345. issueTitle = fatalError.issue.getIssueTitle()
  346. runs ->
  347. expect(issueTitle).toContain '~'
  348. if process.platform is 'win32'
  349. expect(issueTitle).not.toContain process.env.USERPROFILE
  350. else
  351. expect(issueTitle).not.toContain process.env.HOME
  352. describe "when an exception is thrown from a linked package", ->
  353. beforeEach ->
  354. spyOn(atom, 'inDevMode').andReturn false
  355. generateFakeFetchResponses()
  356. packagesDir = path.join(temp.mkdirSync('atom-packages-'), '.atom', 'packages')
  357. atom.packages.packageDirPaths.push(packagesDir)
  358. packageDir = path.join(packagesDir, '..', '..', 'github', 'linked-package')
  359. fs.makeTreeSync path.dirname(path.join(packagesDir, 'linked-package'))
  360. fs.symlinkSync(packageDir, path.join(packagesDir, 'linked-package'), 'junction')
  361. fs.writeFileSync path.join(packageDir, 'package.json'), """
  362. {
  363. "name": "linked-package",
  364. "version": "1.0.0",
  365. "repository": "https://github.com/pulsar-edit/notifications"
  366. }
  367. """
  368. atom.packages.enablePackage('linked-package')
  369. stack = """
  370. ReferenceError: path is not defined
  371. at Object.module.exports.LinkedPackage.wow (#{path.join(fs.realpathSync(packageDir), 'linked-package.coffee')}:29:15)
  372. at atom-workspace.subscriptions.add.atom.commands.add.linked-package:wow (#{path.join(packageDir, 'linked-package.coffee')}:18:102)
  373. at CommandRegistry.module.exports.CommandRegistry.handleCommandEvent (/Applications/Pulsar.app/Contents/Resources/app/src/command-registry.js:238:29)
  374. at /Applications/Pulsar.app/Contents/Resources/app/src/command-registry.js:3:61
  375. at CommandPaletteView.module.exports.CommandPaletteView.confirmed (/Applications/Pulsar.app/Contents/Resources/app/node_modules/command-palette/lib/command-palette-view.js:159:32)
  376. """
  377. detail = "At #{path.join(packageDir, 'linked-package.coffee')}:41"
  378. message = "Uncaught ReferenceError: path is not defined"
  379. atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
  380. notificationContainer = workspaceElement.querySelector('atom-notifications')
  381. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  382. it "displays a fatal error with the package name in the error", ->
  383. waitsForPromise ->
  384. fatalError.getRenderPromise()
  385. runs ->
  386. expect(notificationContainer.childNodes.length).toBe 1
  387. expect(fatalError).toHaveClass 'has-close'
  388. expect(fatalError.innerHTML).toContain "Uncaught ReferenceError: path is not defined"
  389. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">linked-package package</a>"
  390. expect(fatalError.issue.getPackageName()).toBe 'linked-package'
  391. describe "when an exception is thrown from an unloaded package", ->
  392. beforeEach ->
  393. spyOn(atom, 'inDevMode').andReturn false
  394. generateFakeFetchResponses()
  395. packagesDir = temp.mkdirSync('atom-packages-')
  396. atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
  397. packageDir = path.join(packagesDir, '.atom', 'packages', 'unloaded')
  398. fs.writeFileSync path.join(packageDir, 'package.json'), """
  399. {
  400. "name": "unloaded",
  401. "version": "1.0.0",
  402. "repository": "https://github.com/pulsar-edit/notifications"
  403. }
  404. """
  405. stack = "Error\n at #{path.join(packageDir, 'index.js')}:1:1"
  406. detail = 'ReferenceError: unloaded error'
  407. message = "Error"
  408. atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
  409. notificationContainer = workspaceElement.querySelector('atom-notifications')
  410. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  411. it "displays a fatal error with the package name in the error", ->
  412. waitsForPromise ->
  413. fatalError.getRenderPromise()
  414. runs ->
  415. expect(notificationContainer.childNodes.length).toBe 1
  416. expect(fatalError).toHaveClass 'has-close'
  417. expect(fatalError.innerHTML).toContain 'ReferenceError: unloaded error'
  418. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">unloaded package</a>"
  419. expect(fatalError.issue.getPackageName()).toBe 'unloaded'
  420. describe "when an exception is thrown from a package trying to load", ->
  421. beforeEach ->
  422. spyOn(atom, 'inDevMode').andReturn false
  423. generateFakeFetchResponses()
  424. packagesDir = temp.mkdirSync('atom-packages-')
  425. atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
  426. packageDir = path.join(packagesDir, '.atom', 'packages', 'broken-load')
  427. fs.writeFileSync path.join(packageDir, 'package.json'), """
  428. {
  429. "name": "broken-load",
  430. "version": "1.0.0",
  431. "repository": "https://github.com/pulsar-edit/notifications"
  432. }
  433. """
  434. stack = "TypeError: Cannot read property 'prototype' of undefined\n at __extends (<anonymous>:1:1)\n at Object.defineProperty.value [as .coffee] (/Applications/Pulsar.app/Contents/Resources/app.asar/src/compile-cache.js:169:21)"
  435. detail = "TypeError: Cannot read property 'prototype' of undefined"
  436. message = "Failed to load the broken-load package"
  437. atom.notifications.addFatalError(message, {stack, detail, packageName: 'broken-load', dismissable: true})
  438. notificationContainer = workspaceElement.querySelector('atom-notifications')
  439. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  440. it "displays a fatal error with the package name in the error", ->
  441. waitsForPromise ->
  442. fatalError.getRenderPromise()
  443. runs ->
  444. expect(notificationContainer.childNodes.length).toBe 1
  445. expect(fatalError).toHaveClass 'has-close'
  446. expect(fatalError.innerHTML).toContain "TypeError: Cannot read property 'prototype' of undefined"
  447. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">broken-load package</a>"
  448. expect(fatalError.issue.getPackageName()).toBe 'broken-load'
  449. describe "when an exception is thrown from a package trying to load a grammar", ->
  450. beforeEach ->
  451. spyOn(atom, 'inDevMode').andReturn false
  452. generateFakeFetchResponses()
  453. packagesDir = temp.mkdirSync('atom-packages-')
  454. atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
  455. packageDir = path.join(packagesDir, '.atom', 'packages', 'language-broken-grammar')
  456. fs.writeFileSync path.join(packageDir, 'package.json'), """
  457. {
  458. "name": "language-broken-grammar",
  459. "version": "1.0.0",
  460. "repository": "https://github.com/pulsar-edit/notifications"
  461. }
  462. """
  463. stack = """
  464. Unexpected string
  465. at nodeTransforms.Literal (/usr/share/atom/resources/app/node_modules/season/node_modules/cson-parser/lib/parse.js:100:15)
  466. at #{path.join('packageDir', 'grammars', 'broken-grammar.cson')}:1:1
  467. """
  468. detail = """
  469. At Syntax error on line 241, column 18: evalmachine.<anonymous>:1
  470. "#\\{" "end": "\\}"
  471. ^^^^^
  472. Unexpected string in #{path.join('packageDir', 'grammars', 'broken-grammar.cson')}
  473. SyntaxError: Syntax error on line 241, column 18: evalmachine.<anonymous>:1
  474. "#\\{" "end": "\\}"
  475. ^^^^^
  476. """
  477. message = "Failed to load a language-broken-grammar package grammar"
  478. atom.notifications.addFatalError(message, {stack, detail, packageName: 'language-broken-grammar', dismissable: true})
  479. notificationContainer = workspaceElement.querySelector('atom-notifications')
  480. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  481. it "displays a fatal error with the package name in the error", ->
  482. waitsForPromise ->
  483. fatalError.getRenderPromise()
  484. runs ->
  485. expect(notificationContainer.childNodes.length).toBe 1
  486. expect(fatalError).toHaveClass 'has-close'
  487. expect(fatalError.innerHTML).toContain "Failed to load a language-broken-grammar package grammar"
  488. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">language-broken-grammar package</a>"
  489. expect(fatalError.issue.getPackageName()).toBe 'language-broken-grammar'
  490. describe "when an exception is thrown from a package trying to activate", ->
  491. beforeEach ->
  492. spyOn(atom, 'inDevMode').andReturn false
  493. generateFakeFetchResponses()
  494. packagesDir = temp.mkdirSync('atom-packages-')
  495. atom.packages.packageDirPaths.push(path.join(packagesDir, '.atom', 'packages'))
  496. packageDir = path.join(packagesDir, '.atom', 'packages', 'broken-activation')
  497. fs.writeFileSync path.join(packageDir, 'package.json'), """
  498. {
  499. "name": "broken-activation",
  500. "version": "1.0.0",
  501. "repository": "https://github.com/pulsar-edit/notifications"
  502. }
  503. """
  504. stack = "TypeError: Cannot read property 'command' of undefined\n at Object.module.exports.activate (<anonymous>:7:23)\n at Package.module.exports.Package.activateNow (/Applications/Pulsar.app/Contents/Resources/app.asar/src/package.js:232:19)"
  505. detail = "TypeError: Cannot read property 'command' of undefined"
  506. message = "Failed to activate the broken-activation package"
  507. atom.notifications.addFatalError(message, {stack, detail, packageName: 'broken-activation', dismissable: true})
  508. notificationContainer = workspaceElement.querySelector('atom-notifications')
  509. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  510. it "displays a fatal error with the package name in the error", ->
  511. waitsForPromise ->
  512. fatalError.getRenderPromise()
  513. runs ->
  514. expect(notificationContainer.childNodes.length).toBe 1
  515. expect(fatalError).toHaveClass 'has-close'
  516. expect(fatalError.innerHTML).toContain "TypeError: Cannot read property 'command' of undefined"
  517. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">broken-activation package</a>"
  518. expect(fatalError.issue.getPackageName()).toBe 'broken-activation'
  519. describe "when an exception is thrown from a package without a trace, but with a URL", ->
  520. beforeEach ->
  521. issueBody = null
  522. spyOn(atom, 'inDevMode').andReturn false
  523. generateFakeFetchResponses()
  524. try
  525. a + 1
  526. catch e
  527. # Pull the file path from the stack
  528. filePath = e.stack.split('\n')[1].match(/\((.+?):\d+/)[1]
  529. window.onerror.call(window, e.toString(), filePath, 2, 3, message: e.toString(), stack: undefined)
  530. notificationContainer = workspaceElement.querySelector('atom-notifications')
  531. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  532. # TODO: Have to be honest, NO IDEA where this detection happens...
  533. xit "detects the package name from the URL", ->
  534. waitsForPromise -> fatalError.getRenderPromise()
  535. runs ->
  536. expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined'
  537. expect(fatalError.innerHTML).toContain "<a href=\"https://github.com/pulsar-edit/notifications\">notifications package</a>"
  538. expect(fatalError.issue.getPackageName()).toBe 'notifications'
  539. describe "when an exception is thrown from core", ->
  540. beforeEach ->
  541. atom.commands.dispatch(workspaceElement, 'some-package:a-command')
  542. atom.commands.dispatch(workspaceElement, 'some-package:a-command')
  543. atom.commands.dispatch(workspaceElement, 'some-package:a-command')
  544. spyOn(atom, 'inDevMode').andReturn false
  545. generateFakeFetchResponses()
  546. try
  547. a + 1
  548. catch e
  549. # Mung the stack so it looks like its from core
  550. e.stack = e.stack.replace(new RegExp(__filename, 'g'), '<embedded>').replace(/notifications/g, 'core')
  551. window.onerror.call(window, e.toString(), '/dev/null', 2, 3, e)
  552. notificationContainer = workspaceElement.querySelector('atom-notifications')
  553. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  554. waitsForPromise ->
  555. fatalError.getRenderPromise().then ->
  556. fatalError.issue.getIssueBody().then (result) ->
  557. issueBody = result
  558. it "displays a fatal error with the package name in the error", ->
  559. expect(notificationContainer.childNodes.length).toBe 1
  560. expect(fatalError).toBeDefined()
  561. expect(fatalError).toHaveClass 'has-close'
  562. expect(fatalError.innerHTML).toContain 'ReferenceError: a is not defined'
  563. expect(fatalError.innerHTML).toContain 'bug in Pulsar'
  564. expect(fatalError.issue.getPackageName()).toBeUndefined()
  565. button = fatalError.querySelector('.btn')
  566. expect(button.textContent).toContain 'Create issue on pulsar-edit/pulsar'
  567. expect(issueBody).toContain 'ReferenceError: a is not defined'
  568. expect(issueBody).toContain '**Thrown From**: Pulsar Core'
  569. it "contains the commands that the user run in the issue body", ->
  570. expect(issueBody).toContain 'some-package:a-command'
  571. it "allows the user to toggle the stack trace", ->
  572. stackToggle = fatalError.querySelector('.stack-toggle')
  573. stackContainer = fatalError.querySelector('.stack-container')
  574. expect(stackToggle).toExist()
  575. expect(stackContainer.style.display).toBe 'none'
  576. stackToggle.click()
  577. expect(stackContainer.style.display).toBe 'block'
  578. stackToggle.click()
  579. expect(stackContainer.style.display).toBe 'none'
  580. describe "when the there is an error searching for the issue", ->
  581. beforeEach ->
  582. spyOn(atom, 'inDevMode').andReturn false
  583. generateFakeFetchResponses(issuesErrorResponse: '403')
  584. generateException()
  585. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  586. waitsForPromise ->
  587. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  588. it "asks the user to create an issue", ->
  589. button = fatalError.querySelector('.btn')
  590. fatalNotification = fatalError.querySelector('.fatal-notification')
  591. expect(button.textContent).toContain 'Create issue'
  592. expect(fatalNotification.textContent).toContain 'The error was thrown from the notifications package.'
  593. describe "when the error has not been reported", ->
  594. beforeEach ->
  595. spyOn(atom, 'inDevMode').andReturn false
  596. describe "when the message is longer than 100 characters", ->
  597. message = "Uncaught Error: Cannot find module 'dialog'Error: Cannot find module 'dialog' at Function.Module._resolveFilename (module.js:351:15) at Function.Module._load (module.js:293:25) at Module.require (module.js:380:17) at EventEmitter.<anonymous> (/Applications/Pulsar.app/Contents/Resources/atom/browser/lib/rpc-server.js:128:79) at EventEmitter.emit (events.js:119:17) at EventEmitter.<anonymous> (/Applications/Pulsar.app/Contents/Resources/atom/browser/api/lib/web-contents.js:99:23) at EventEmitter.emit (events.js:119:17)"
  598. expectedIssueTitle = "Uncaught Error: Cannot find module 'dialog'Error: Cannot find module 'dialog' at Function.Module...."
  599. beforeEach ->
  600. generateFakeFetchResponses()
  601. try
  602. a + 1
  603. catch e
  604. e.code = 'Error'
  605. e.message = message
  606. window.onerror.call(window, e.message, 'abc', 2, 3, e)
  607. it "truncates the issue title to 100 characters", ->
  608. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  609. waitsForPromise ->
  610. fatalError.getRenderPromise()
  611. runs ->
  612. button = fatalError.querySelector('.btn')
  613. expect(button.textContent).toContain 'Create issue'
  614. expect(fatalError.issue.getIssueTitle()).toBe(expectedIssueTitle)
  615. describe "when the package is out of date", ->
  616. beforeEach ->
  617. installedVersion = '0.9.0'
  618. UserUtilities = require '../lib/user-utilities'
  619. spyOn(UserUtilities, 'getPackageVersion').andCallFake -> installedVersion
  620. spyOn(atom, 'inDevMode').andReturn false
  621. describe "when the package is a non-core package", ->
  622. beforeEach ->
  623. generateFakeFetchResponses
  624. packageResponse:
  625. repository: url: 'https://github.com/someguy/somepackage'
  626. releases: latest: '0.10.0'
  627. spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
  628. spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
  629. generateException()
  630. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  631. waitsForPromise ->
  632. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  633. it "asks the user to update their packages", ->
  634. fatalNotification = fatalError.querySelector('.fatal-notification')
  635. button = fatalError.querySelector('.btn')
  636. expect(button.textContent).toContain 'Check for package updates'
  637. expect(fatalNotification.textContent).toContain 'Upgrading to the latest'
  638. expect(button.getAttribute('href')).toBe '#'
  639. describe "when the package is an atom-owned non-core package", ->
  640. beforeEach ->
  641. generateFakeFetchResponses
  642. packageResponse:
  643. repository: url: 'https://github.com/pulsar-edit/sort-lines'
  644. releases: latest: '0.10.0'
  645. spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "sort-lines"
  646. spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/pulsar-edit/sort-lines"
  647. generateException()
  648. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  649. waitsForPromise ->
  650. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  651. it "asks the user to update their packages", ->
  652. fatalNotification = fatalError.querySelector('.fatal-notification')
  653. button = fatalError.querySelector('.btn')
  654. expect(button.textContent).toContain 'Check for package updates'
  655. expect(fatalNotification.textContent).toContain 'Upgrading to the latest'
  656. expect(button.getAttribute('href')).toBe '#'
  657. describe "when the package is a core package", ->
  658. beforeEach ->
  659. generateFakeFetchResponses
  660. packageResponse:
  661. repository: url: 'https://github.com/pulsar-edit/notifications'
  662. releases: latest: '0.11.0'
  663. describe "when the locally installed version is lower than Pulsar's version", ->
  664. beforeEach ->
  665. versionShippedWithPulsar = '0.10.0'
  666. UserUtilities = require '../lib/user-utilities'
  667. spyOn(UserUtilities, 'getPackageVersionShippedWithPulsar').andCallFake -> versionShippedWithPulsar
  668. generateException()
  669. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  670. waitsForPromise ->
  671. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  672. it "doesn't show the Create Issue button", ->
  673. button = fatalError.querySelector('.btn-issue')
  674. expect(button).not.toExist()
  675. it "tells the user that the package is a locally installed core package and out of date", ->
  676. fatalNotification = fatalError.querySelector('.fatal-notification')
  677. expect(fatalNotification.textContent).toContain 'Locally installed core Pulsar package'
  678. expect(fatalNotification.textContent).toContain 'is out of date'
  679. describe "when the locally installed version matches Pulsar's version", ->
  680. beforeEach ->
  681. versionShippedWithPulsar = '0.9.0'
  682. UserUtilities = require '../lib/user-utilities'
  683. spyOn(UserUtilities, 'getPackageVersionShippedWithPulsar').andCallFake -> versionShippedWithPulsar
  684. generateException()
  685. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  686. waitsForPromise ->
  687. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  688. it "ignores the out of date package because they cant upgrade it without upgrading atom", ->
  689. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  690. button = fatalError.querySelector('.btn')
  691. expect(button.textContent).toContain 'Create issue'
  692. # TODO: Re-enable when Pulsar have a way to check this
  693. # describe "when Pulsar is out of date", ->
  694. # beforeEach ->
  695. # installedVersion = '0.179.0'
  696. # spyOn(atom, 'getVersion').andCallFake -> installedVersion
  697. # spyOn(atom, 'inDevMode').andReturn false
  698. #
  699. # generateFakeFetchResponses
  700. # atomResponse:
  701. # name: '0.180.0'
  702. #
  703. # generateException()
  704. #
  705. # fatalError = notificationContainer.querySelector('atom-notification.fatal')
  706. # waitsForPromise ->
  707. # fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  708. #
  709. # it "doesn't show the Create Issue button", ->
  710. # button = fatalError.querySelector('.btn-issue')
  711. # expect(button).not.toExist()
  712. #
  713. # it "tells the user that Pulsar is out of date", ->
  714. # fatalNotification = fatalError.querySelector('.fatal-notification')
  715. # expect(fatalNotification.textContent).toContain 'Pulsar is out of date'
  716. #
  717. # it "provides a link to the latest released version", ->
  718. # fatalNotification = fatalError.querySelector('.fatal-notification')
  719. # expect(fatalNotification.innerHTML).toContain '<a href="https://github.com/pulsar-edit/pulsar/releases/tag/v0.180.0">latest version</a>'
  720. describe "when the error has been reported", ->
  721. beforeEach ->
  722. spyOn(atom, 'inDevMode').andReturn false
  723. describe "when the issue is open", ->
  724. beforeEach ->
  725. generateFakeFetchResponses
  726. issuesResponse:
  727. items: [
  728. {
  729. title: 'ReferenceError: a is not defined in $ATOM_HOME/somewhere'
  730. html_url: 'http://url.com/ok'
  731. state: 'open'
  732. }
  733. ]
  734. spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
  735. spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
  736. generateException()
  737. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  738. waitsForPromise ->
  739. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  740. it "shows the user a view issue button", ->
  741. fatalNotification = fatalError.querySelector('.fatal-notification')
  742. button = fatalError.querySelector('.btn')
  743. expect(button.textContent).toContain 'View Issue'
  744. expect(button.getAttribute('href')).toBe 'http://url.com/ok'
  745. expect(fatalNotification.textContent).toContain 'already been reported'
  746. expect(fetch.calls[0].args[0]).toContain encodeURIComponent('someguy/somepackage')
  747. describe "when the issue is closed", ->
  748. beforeEach ->
  749. generateFakeFetchResponses
  750. issuesResponse:
  751. items: [
  752. {
  753. title: 'ReferenceError: a is not defined in $ATOM_HOME/somewhere'
  754. html_url: 'http://url.com/closed'
  755. state: 'closed'
  756. }
  757. ]
  758. spyOn(NotificationIssue.prototype, 'getPackageName').andCallFake -> "somepackage"
  759. spyOn(NotificationIssue.prototype, 'getRepoUrl').andCallFake -> "https://github.com/someguy/somepackage"
  760. generateException()
  761. fatalError = notificationContainer.querySelector('atom-notification.fatal')
  762. waitsForPromise ->
  763. fatalError.getRenderPromise().then -> issueBody = fatalError.issue.issueBody
  764. it "shows the user a view issue button", ->
  765. button = fatalError.querySelector('.btn')
  766. expect(button.textContent).toContain 'View Issue'
  767. expect(button.getAttribute('href')).toBe 'http://url.com/closed'
  768. describe "when a BufferedProcessError is thrown", ->
  769. it "adds an error to the notifications", ->
  770. expect(notificationContainer.querySelector('atom-notification.error')).not.toExist()
  771. window.onerror('Uncaught BufferedProcessError: Failed to spawn command `bad-command`', 'abc', 2, 3, {name: 'BufferedProcessError'})
  772. error = notificationContainer.querySelector('atom-notification.error')
  773. expect(error).toExist()
  774. expect(error.innerHTML).toContain 'Failed to spawn command'
  775. expect(error.innerHTML).not.toContain 'BufferedProcessError'
  776. describe "when a spawn ENOENT error is thrown", ->
  777. beforeEach ->
  778. spyOn(atom, 'inDevMode').andReturn false
  779. describe "when the binary has no path", ->
  780. beforeEach ->
  781. error = new Error('Error: spawn some_binary ENOENT')
  782. error.code = 'ENOENT'
  783. window.onerror.call(window, error.message, 'abc', 2, 3, error)
  784. it "displays a dismissable error without the stack trace", ->
  785. notificationContainer = workspaceElement.querySelector('atom-notifications')
  786. error = notificationContainer.querySelector('atom-notification.error')
  787. expect(error.textContent).toContain "'some_binary' could not be spawned"
  788. describe "when the binary has /atom in the path", ->
  789. beforeEach ->
  790. try
  791. a + 1
  792. catch e
  793. e.code = 'ENOENT'
  794. message = 'Error: spawn /opt/atom/Pulsar Helper (deleted) ENOENT'
  795. window.onerror.call(window, message, 'abc', 2, 3, e)
  796. it "displays a fatal error", ->
  797. notificationContainer = workspaceElement.querySelector('atom-notifications')
  798. error = notificationContainer.querySelector('atom-notification.fatal')
  799. expect(error).toExist()