├── .gitignore
├── .travis.yml
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── appveyor.yml
├── lib
├── about.js
├── components
│ ├── about-status-bar.js
│ ├── about-view.js
│ ├── atom-logo.js
│ └── update-view.js
├── etch-component.js
├── main.js
└── update-manager.js
├── package.json
├── spec
├── about-spec.js
├── about-status-bar-spec.js
├── helpers
│ └── async-spec-helpers.js
├── mocks
│ └── updater.js
├── update-manager-spec.js
└── update-view-spec.js
└── styles
├── about.less
└── variables.less
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 |
3 | notifications:
4 | email:
5 | on_success: never
6 | on_failure: change
7 |
8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh'
9 |
10 | git:
11 | depth: 10
12 |
13 | branches:
14 | only:
15 | - master
16 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
8 |
9 | ### Prerequisites
10 |
11 | * [ ] Put an X between the brackets on this line if you have done all of the following:
12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/
14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq
15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom
16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages
17 |
18 | ### Description
19 |
20 | [Description of the issue]
21 |
22 | ### Steps to Reproduce
23 |
24 | 1. [First Step]
25 | 2. [Second Step]
26 | 3. [and so on...]
27 |
28 | **Expected behavior:** [What you expect to happen]
29 |
30 | **Actual behavior:** [What actually happens]
31 |
32 | **Reproduces how often:** [What percentage of the time does it reproduce?]
33 |
34 | ### Versions
35 |
36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running.
37 |
38 | ### Additional Information
39 |
40 | Any additional information, configuration or data that might be necessary to reproduce the issue.
41 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Machisté N. Quintana
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Requirements
2 |
3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion.
4 | * All new code requires tests to ensure against regressions
5 |
6 | ### Description of the Change
7 |
8 |
13 |
14 | ### Alternate Designs
15 |
16 |
17 |
18 | ### Benefits
19 |
20 |
21 |
22 | ### Possible Drawbacks
23 |
24 |
25 |
26 | ### Applicable Issues
27 |
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### This package is now a part of the [core Atom repository](https://github.com/atom/atom/tree/master/packages/about), please direct all issues and pull requests there in the future!
2 |
3 | ---
4 |
5 | # About package
6 | [](https://travis-ci.org/atom/about) [](https://ci.appveyor.com/project/atom/about/branch/master) [](https://david-dm.org/atom/about)
7 |
8 | View useful information about your Atom installation.
9 |
10 | 
11 |
12 | This is a package for [Atom](https://atom.io), a hackable text editor for the 21st Century.
13 |
14 | ## Usage
15 |
16 | This package provides a cross-platform "About Atom" view that displays information about your Atom installation, which currently includes the current version, the license, and the Terms of Use.
17 |
18 | ## Contributing
19 | Always feel free to help out! Whether it's filing bugs and feature requests
20 | or working on some of the open issues, Atom's [contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md)
21 | will help get you started while the [guide for contributing to packages](https://github.com/atom/atom/blob/master/docs/contributing-to-packages.md)
22 | has some extra information.
23 |
24 | ## License
25 |
26 | [MIT License](https://opensource.org/licenses/MIT) - see the [LICENSE](https://github.com/atom/about/blob/master/LICENSE.md) for more details.
27 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | platform:
2 | - x64
3 |
4 | branches:
5 | only:
6 | - master
7 |
8 | clone_depth: 10
9 |
10 | skip_tags: true
11 |
12 | environment:
13 | APM_TEST_PACKAGES:
14 |
15 | matrix:
16 | - ATOM_CHANNEL: stable
17 | - ATOM_CHANNEL: beta
18 |
19 | install:
20 | - ps: Install-Product node 6
21 |
22 | build_script:
23 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1'))
24 |
25 | test: off
26 | deploy: off
27 |
--------------------------------------------------------------------------------
/lib/about.js:
--------------------------------------------------------------------------------
1 | const {CompositeDisposable, Emitter} = require('atom')
2 | const AboutView = require('./components/about-view')
3 |
4 | // Deferred requires
5 | let shell
6 |
7 | module.exports = class About {
8 | constructor (initialState) {
9 | this.subscriptions = new CompositeDisposable()
10 | this.emitter = new Emitter()
11 |
12 | this.state = initialState
13 | this.views = {
14 | aboutView: null
15 | }
16 |
17 | this.subscriptions.add(atom.workspace.addOpener((uriToOpen) => {
18 | if (uriToOpen === this.state.uri) {
19 | return this.deserialize()
20 | }
21 | }))
22 |
23 | this.subscriptions.add(atom.commands.add('atom-workspace', 'about:view-release-notes', () => {
24 | shell = shell || require('electron').shell
25 | shell.openExternal(this.state.updateManager.getReleaseNotesURLForCurrentVersion())
26 | }))
27 | }
28 |
29 | destroy () {
30 | if (this.views.aboutView) this.views.aboutView.destroy()
31 | this.views.aboutView = null
32 |
33 | if (this.state.updateManager) this.state.updateManager.dispose()
34 | this.setState({updateManager: null})
35 |
36 | this.subscriptions.dispose()
37 | }
38 |
39 | setState (newState) {
40 | if (newState && typeof newState === 'object') {
41 | let {state} = this
42 | this.state = Object.assign({}, state, newState)
43 |
44 | this.didChange()
45 | }
46 | }
47 |
48 | didChange () {
49 | this.emitter.emit('did-change')
50 | }
51 |
52 | onDidChange (callback) {
53 | this.emitter.on('did-change', callback)
54 | }
55 |
56 | deserialize (state) {
57 | if (!this.views.aboutView) {
58 | this.setState(state)
59 |
60 | this.views.aboutView = new AboutView({
61 | uri: this.state.uri,
62 | updateManager: this.state.updateManager,
63 | currentAtomVersion: this.state.currentAtomVersion,
64 | currentElectronVersion: this.state.currentElectronVersion,
65 | currentChromeVersion: this.state.currentChromeVersion,
66 | currentNodeVersion: this.state.currentNodeVersion,
67 | availableVersion: this.state.updateManager.getAvailableVersion()
68 | })
69 | this.handleStateChanges()
70 | }
71 |
72 | return this.views.aboutView
73 | }
74 |
75 | handleStateChanges () {
76 | this.onDidChange(() => {
77 | if (this.views.aboutView) {
78 | this.views.aboutView.update({
79 | updateManager: this.state.updateManager,
80 | currentAtomVersion: this.state.currentAtomVersion,
81 | currentElectronVersion: this.state.currentElectronVersion,
82 | currentChromeVersion: this.state.currentChromeVersion,
83 | currentNodeVersion: this.state.currentNodeVersion,
84 | availableVersion: this.state.updateManager.getAvailableVersion()
85 | })
86 | }
87 | })
88 |
89 | this.state.updateManager.onDidChange(() => {
90 | this.didChange()
91 | })
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/lib/components/about-status-bar.js:
--------------------------------------------------------------------------------
1 | const {CompositeDisposable} = require('atom')
2 | const etch = require('etch')
3 | const EtchComponent = require('../etch-component')
4 |
5 | const $ = etch.dom
6 |
7 | module.exports =
8 | class AboutStatusBar extends EtchComponent {
9 | constructor () {
10 | super()
11 | this.subscriptions = new CompositeDisposable()
12 |
13 | this.subscriptions.add(atom.tooltips.add(this.element, {title: 'An update will be installed the next time Atom is relaunched.
Click the squirrel icon for more information.'}))
14 | }
15 |
16 | handleClick () {
17 | atom.workspace.open('atom://about')
18 | }
19 |
20 | render () {
21 | return $.div({className: 'about-release-notes inline-block', onclick: this.handleClick.bind(this)},
22 | $.span({type: 'button', className: 'icon icon-squirrel'})
23 | )
24 | }
25 |
26 | destroy () {
27 | super.destroy()
28 | this.subscriptions.dispose()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/components/about-view.js:
--------------------------------------------------------------------------------
1 | const {Disposable} = require('atom')
2 | const etch = require('etch')
3 | const shell = require('shell')
4 | const AtomLogo = require('./atom-logo')
5 | const EtchComponent = require('../etch-component')
6 | const UpdateView = require('./update-view')
7 |
8 | const $ = etch.dom
9 |
10 | module.exports =
11 | class AboutView extends EtchComponent {
12 | handleAtomVersionClick (e) {
13 | e.preventDefault()
14 | atom.clipboard.write(this.props.currentAtomVersion)
15 | }
16 |
17 | handleElectronVersionClick (e) {
18 | e.preventDefault()
19 | atom.clipboard.write(this.props.currentElectronVersion)
20 | }
21 |
22 | handleChromeVersionClick (e) {
23 | e.preventDefault()
24 | atom.clipboard.write(this.props.currentChromeVersion)
25 | }
26 |
27 | handleNodeVersionClick (e) {
28 | e.preventDefault()
29 | atom.clipboard.write(this.props.currentNodeVersion)
30 | }
31 |
32 | handleReleaseNotesClick (e) {
33 | e.preventDefault()
34 | shell.openExternal(this.props.updateManager.getReleaseNotesURLForAvailableVersion())
35 | }
36 |
37 | handleLicenseClick (e) {
38 | e.preventDefault()
39 | atom.commands.dispatch(atom.views.getView(atom.workspace), 'application:open-license')
40 | }
41 |
42 | handleTermsOfUseClick (e) {
43 | e.preventDefault()
44 | shell.openExternal('https://atom.io/terms')
45 | }
46 |
47 | handleHowToUpdateClick (e) {
48 | e.preventDefault()
49 | shell.openExternal('https://flight-manual.atom.io/getting-started/sections/installing-atom/')
50 | }
51 |
52 | handleShowMoreClick (e) {
53 | e.preventDefault()
54 | var showMoreDiv = document.querySelector('.show-more')
55 | var showMoreText = document.querySelector('.about-more-expand')
56 | switch (showMoreText.textContent) {
57 | case 'Show more':
58 | showMoreDiv.classList.toggle('hidden')
59 | showMoreText.textContent = 'Hide'
60 | break
61 | case 'Hide':
62 | showMoreDiv.classList.toggle('hidden')
63 | showMoreText.textContent = 'Show more'
64 | break
65 | }
66 | }
67 |
68 | render () {
69 | return $.div({className: 'pane-item native-key-bindings about'},
70 | $.div({className: 'about-container'},
71 | $.header({className: 'about-header'},
72 | $.a({className: 'about-atom-io', href: 'https://atom.io'},
73 | $(AtomLogo)
74 | ),
75 | $.div({className: 'about-header-info'},
76 | $.span({className: 'about-version-container inline-block atom', onclick: this.handleAtomVersionClick.bind(this)},
77 | $.span({className: 'about-version'}, `${this.props.currentAtomVersion} ${process.arch}`),
78 | $.span({className: 'icon icon-clippy about-copy-version'})
79 | ),
80 | $.a({className: 'about-header-release-notes', onclick: this.handleReleaseNotesClick.bind(this)}, 'Release Notes')
81 | ),
82 | $.span({className: 'about-version-container inline-block show-more-expand', onclick: this.handleShowMoreClick.bind(this)},
83 | $.span({className: 'about-more-expand'}, 'Show more')
84 | ),
85 | $.div({className: 'show-more hidden about-more-info'},
86 | $.div({className: 'about-more-info'},
87 | $.span({className: 'about-version-container inline-block electron', onclick: this.handleElectronVersionClick.bind(this)},
88 | $.span({className: 'about-more-version'}, `Electron: ${this.props.currentElectronVersion} `),
89 | $.span({className: 'icon icon-clippy about-copy-version'})
90 | )
91 | ),
92 | $.div({className: 'about-more-info'},
93 | $.span({className: 'about-version-container inline-block chrome', onclick: this.handleChromeVersionClick.bind(this)},
94 | $.span({className: 'about-more-version'}, `Chrome: ${this.props.currentChromeVersion} `),
95 | $.span({className: 'icon icon-clippy about-copy-version'})
96 | )
97 | ),
98 | $.div({className: 'about-more-info'},
99 | $.span({className: 'about-version-container inline-block node', onclick: this.handleNodeVersionClick.bind(this)},
100 | $.span({className: 'about-more-version'}, `Node: ${this.props.currentNodeVersion} `),
101 | $.span({className: 'icon icon-clippy about-copy-version'})
102 | )
103 | )
104 | )
105 | )
106 | ),
107 |
108 | $(UpdateView, {
109 | updateManager: this.props.updateManager,
110 | availableVersion: this.props.availableVersion,
111 | viewUpdateReleaseNotes: this.handleReleaseNotesClick.bind(this),
112 | viewUpdateInstructions: this.handleHowToUpdateClick.bind(this)
113 | }),
114 |
115 | $.div({className: 'about-actions group-item'},
116 | $.div({className: 'btn-group'},
117 | $.button({className: 'btn view-license', onclick: this.handleLicenseClick.bind(this)}, 'License'),
118 | $.button({className: 'btn terms-of-use', onclick: this.handleTermsOfUseClick.bind(this)}, 'Terms of Use')
119 | )
120 | ),
121 |
122 | $.div({className: 'about-love group-start'},
123 | $.span({className: 'icon icon-code'}),
124 | $.span({className: 'inline'}, ' with '),
125 | $.span({className: 'icon icon-heart'}),
126 | $.span({className: 'inline'}, ' by '),
127 | $.a({className: 'icon icon-logo-github', href: 'https://github.com'})
128 | ),
129 |
130 | $.div({className: 'about-credits group-item'},
131 | $.span({className: 'inline'}, 'And the awesome '),
132 | $.a({href: 'https://github.com/atom/atom/contributors'}, 'Atom Community')
133 | )
134 | )
135 | }
136 |
137 | serialize () {
138 | return {
139 | deserializer: this.constructor.name,
140 | uri: this.props.uri
141 | }
142 | }
143 |
144 | onDidChangeTitle () {
145 | return new Disposable()
146 | }
147 |
148 | onDidChangeModified () {
149 | return new Disposable()
150 | }
151 |
152 | getTitle () {
153 | return 'About'
154 | }
155 |
156 | getIconName () {
157 | return 'info'
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/lib/components/atom-logo.js:
--------------------------------------------------------------------------------
1 | const etch = require('etch')
2 | const EtchComponent = require('../etch-component')
3 |
4 | const $ = etch.dom
5 |
6 | module.exports =
7 | class AtomLogo extends EtchComponent {
8 | render () {
9 | return $.svg({className: 'about-logo', width: '330px', height: '68px', viewBox: '0 0 330 68'},
10 | $.g({stroke: 'none', 'stroke-width': '1', fill: 'none', 'fill-rule': 'evenodd'},
11 | $.g({transform: 'translate(2.000000, 1.000000)'},
12 | $.g({transform: 'translate(96.000000, 8.000000)', fill: 'currentColor'},
13 | $.path({d: 'M185.498,3.399 C185.498,2.417 186.34,1.573 187.324,1.573 L187.674,1.573 C188.447,1.573 189.01,1.995 189.5,2.628 L208.676,30.862 L227.852,2.628 C228.272,1.995 228.905,1.573 229.676,1.573 L230.028,1.573 C231.01,1.573 231.854,2.417 231.854,3.399 L231.854,49.403 C231.854,50.387 231.01,51.231 230.028,51.231 C229.044,51.231 228.202,50.387 228.202,49.403 L228.202,8.246 L210.151,34.515 C209.729,35.148 209.237,35.428 208.606,35.428 C207.973,35.428 207.481,35.148 207.061,34.515 L189.01,8.246 L189.01,49.475 C189.01,50.457 188.237,51.231 187.254,51.231 C186.27,51.231 185.498,50.458 185.498,49.475 L185.498,3.399 L185.498,3.399 Z'}),
14 | $.path({d: 'M113.086,26.507 L113.086,26.367 C113.086,12.952 122.99,0.941 137.881,0.941 C152.77,0.941 162.533,12.811 162.533,26.225 L162.533,26.367 C162.533,39.782 152.629,51.792 137.74,51.792 C122.85,51.792 113.086,39.923 113.086,26.507 M158.74,26.507 L158.74,26.367 C158.74,14.216 149.89,4.242 137.74,4.242 C125.588,4.242 116.879,14.075 116.879,26.225 L116.879,26.367 C116.879,38.518 125.729,48.491 137.881,48.491 C150.031,48.491 158.74,38.658 158.74,26.507'}),
15 | $.path({d: 'M76.705,5.155 L60.972,5.155 C60.06,5.155 59.287,4.384 59.287,3.469 C59.287,2.556 60.059,1.783 60.972,1.783 L96.092,1.783 C97.004,1.783 97.778,2.555 97.778,3.469 C97.778,4.383 97.005,5.155 96.092,5.155 L80.358,5.155 L80.358,49.405 C80.358,50.387 79.516,51.231 78.532,51.231 C77.55,51.231 76.706,50.387 76.706,49.405 L76.706,5.155 L76.705,5.155 Z'}),
16 | $.path({d: 'M0.291,48.562 L21.291,3.05 C21.783,1.995 22.485,1.292 23.75,1.292 L23.891,1.292 C25.155,1.292 25.858,1.995 26.348,3.05 L47.279,48.421 C47.49,48.843 47.56,49.194 47.56,49.546 C47.56,50.458 46.788,51.231 45.803,51.231 C44.961,51.231 44.329,50.599 43.978,49.826 L38.219,37.183 L9.21,37.183 L3.45,49.897 C3.099,50.739 2.538,51.231 1.694,51.231 C0.781,51.231 0.008,50.529 0.008,49.685 C0.009,49.404 0.08,48.983 0.291,48.562 L0.291,48.562 Z M36.673,33.882 L23.749,5.437 L10.755,33.882 L36.673,33.882 L36.673,33.882 Z'})
17 | ),
18 | $.g({},
19 | $.path({d: 'M40.363,32.075 C40.874,34.44 39.371,36.77 37.006,37.282 C34.641,37.793 32.311,36.29 31.799,33.925 C31.289,31.56 32.791,29.23 35.156,28.718 C37.521,28.207 39.851,29.71 40.363,32.075', fill: 'currentColor'}),
20 | $.path({d: 'M48.578,28.615 C56.851,45.587 58.558,61.581 52.288,64.778 C45.822,68.076 33.326,56.521 24.375,38.969 C15.424,21.418 13.409,4.518 19.874,1.221 C22.689,-0.216 26.648,1.166 30.959,4.629', stroke: 'currentColor', 'stroke-width': '3.08', 'stroke-linecap': 'round'}),
21 | $.path({d: 'M7.64,39.45 C2.806,36.94 -0.009,33.915 0.154,30.79 C0.531,23.542 16.787,18.497 36.462,19.52 C56.137,20.544 71.781,27.249 71.404,34.497 C71.241,37.622 68.127,40.338 63.06,42.333', stroke: 'currentColor', 'stroke-width': '3.08', 'stroke-linecap': 'round'}),
22 | $.path({d: 'M28.828,59.354 C23.545,63.168 18.843,64.561 15.902,62.653 C9.814,58.702 13.572,42.102 24.296,25.575 C35.02,9.048 48.649,-1.149 54.736,2.803 C57.566,4.639 58.269,9.208 57.133,15.232', stroke: 'currentColor', 'stroke-width': '3.08', 'stroke-linecap': 'round'})
23 | )
24 | )
25 | )
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/lib/components/update-view.js:
--------------------------------------------------------------------------------
1 | const etch = require('etch')
2 | const EtchComponent = require('../etch-component')
3 | const UpdateManager = require('../update-manager')
4 |
5 | const $ = etch.dom
6 |
7 | module.exports =
8 | class UpdateView extends EtchComponent {
9 | constructor (props) {
10 | super(props)
11 |
12 | if (this.props.updateManager.getAutoUpdatesEnabled() && this.props.updateManager.getState() === UpdateManager.State.Idle) {
13 | this.props.updateManager.checkForUpdate()
14 | }
15 | }
16 |
17 | handleAutoUpdateCheckbox (e) {
18 | atom.config.set('core.automaticallyUpdate', e.target.checked)
19 | }
20 |
21 | shouldUpdateActionButtonBeDisabled () {
22 | let {state} = this.props.updateManager
23 | return state === UpdateManager.State.CheckingForUpdate || state === UpdateManager.State.DownloadingUpdate
24 | }
25 |
26 | executeUpdateAction () {
27 | if (this.props.updateManager.state === UpdateManager.State.UpdateAvailableToInstall) {
28 | this.props.updateManager.restartAndInstallUpdate()
29 | } else {
30 | this.props.updateManager.checkForUpdate()
31 | }
32 | }
33 |
34 | renderUpdateStatus () {
35 | let updateStatus = ''
36 |
37 | switch (this.props.updateManager.state) {
38 | case UpdateManager.State.Idle:
39 | updateStatus = $.div({className: 'about-updates-item is-shown about-default-update-message'},
40 | this.props.updateManager.getAutoUpdatesEnabled() ? 'Atom will check for updates automatically' : 'Automatic updates are disabled please check manually'
41 | )
42 | break
43 | case UpdateManager.State.CheckingForUpdate:
44 | updateStatus = $.div({className: 'about-updates-item app-checking-for-updates'},
45 | $.span({className: 'about-updates-label icon icon-search'}, 'Checking for updates...')
46 | )
47 | break
48 | case UpdateManager.State.DownloadingUpdate:
49 | updateStatus = $.div({className: 'about-updates-item app-downloading-update'},
50 | $.span({className: 'loading loading-spinner-tiny inline-block'}),
51 | $.span({className: 'about-updates-label'}, 'Downloading update')
52 | )
53 | break
54 | case UpdateManager.State.UpdateAvailableToInstall:
55 | updateStatus = $.div({className: 'about-updates-item app-update-available-to-install'},
56 | $.span({className: 'about-updates-label icon icon-squirrel'}, 'New update'),
57 | $.span({className: 'about-updates-version'}, this.props.availableVersion),
58 | $.a({className: 'about-updates-release-notes', onclick: this.props.viewUpdateReleaseNotes}, 'Release Notes')
59 | )
60 | break
61 | case UpdateManager.State.UpToDate:
62 | updateStatus = $.div({className: 'about-updates-item app-up-to-date'},
63 | $.span({className: 'icon icon-check'}),
64 | $.span({className: 'about-updates-label is-strong'}, 'Atom is up to date!')
65 | )
66 | break
67 | case UpdateManager.State.Unsupported:
68 | updateStatus = $.div({className: 'about-updates-item app-unsupported'},
69 | $.span({className: 'about-updates-label is-strong'}, 'Your system does not support automatic updates'),
70 | $.a({className: 'about-updates-instructions', onclick: this.props.viewUpdateInstructions}, 'How to update')
71 | )
72 | break
73 | case UpdateManager.State.Error:
74 | updateStatus = $.div({className: 'about-updates-item app-update-error'},
75 | $.span({className: 'icon icon-x'}),
76 | $.span({className: 'about-updates-label app-error-message is-strong'}, this.props.updateManager.getErrorMessage())
77 | )
78 | break
79 | }
80 |
81 | return updateStatus
82 | }
83 |
84 | render () {
85 | return $.div({className: 'about-updates group-start'},
86 | $.div({className: 'about-updates-box'},
87 | $.div({className: 'about-updates-status'}, this.renderUpdateStatus()),
88 | $.button(
89 | {
90 | className: 'btn about-update-action-button',
91 | disabled: this.shouldUpdateActionButtonBeDisabled(),
92 | onclick: this.executeUpdateAction.bind(this),
93 | style: {
94 | display: this.props.updateManager.state === UpdateManager.State.Unsupported ? 'none' : 'block'
95 | }
96 | },
97 | this.props.updateManager.state === 'update-available' ? 'Restart and install' : 'Check now'
98 | )
99 | ),
100 | $.div(
101 | {
102 | className: 'about-auto-updates',
103 | style: {
104 | display: this.props.updateManager.state === UpdateManager.State.Unsupported ? 'none' : 'block'
105 | }
106 | },
107 | $.label({},
108 | $.input(
109 | {
110 | className: 'input-checkbox',
111 | type: 'checkbox',
112 | checked: this.props.updateManager.getAutoUpdatesEnabled(),
113 | onchange: this.handleAutoUpdateCheckbox.bind(this)
114 | }
115 | ),
116 | $.span({}, 'Automatically download updates')
117 | )
118 | )
119 | )
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/lib/etch-component.js:
--------------------------------------------------------------------------------
1 | const etch = require('etch')
2 |
3 | /*
4 | Public: Abstract class for handling the initialization
5 | boilerplate of an Etch component.
6 | */
7 | module.exports =
8 | class EtchComponent {
9 | constructor (props) {
10 | this.props = props
11 |
12 | etch.initialize(this)
13 | EtchComponent.setScheduler(atom.views)
14 | }
15 |
16 | /*
17 | Public: Gets the scheduler Etch uses for coordinating DOM updates.
18 |
19 | Returns a {Scheduler}
20 | */
21 | static getScheduler () {
22 | return etch.getScheduler()
23 | }
24 |
25 | /*
26 | Public: Sets the scheduler Etch uses for coordinating DOM updates.
27 |
28 | * `scheduler` {Scheduler}
29 | */
30 | static setScheduler (scheduler) {
31 | etch.setScheduler(scheduler)
32 | }
33 |
34 | /*
35 | Public: Updates the component's properties and re-renders it. Only the
36 | properties you specify in this object will update – any other properties
37 | the component stores will be unaffected.
38 |
39 | * `props` an {Object} representing the properties you want to update
40 | */
41 | update (props) {
42 | let oldProps = this.props
43 | this.props = Object.assign({}, oldProps, props)
44 | return etch.update(this)
45 | }
46 |
47 | /*
48 | Public: Destroys the component, removing it from the DOM.
49 | */
50 | destroy () {
51 | etch.destroy(this)
52 | }
53 |
54 | render () {
55 | throw new Error('Etch components must implement a `render` method')
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | const {CompositeDisposable} = require('atom')
2 | const semver = require('semver')
3 | const UpdateManager = require('./update-manager')
4 | const About = require('./about')
5 | const StatusBarView = require('./components/about-status-bar')
6 | let updateManager
7 |
8 | // The local storage key for the available update version.
9 | const AvailableUpdateVersion = 'about:version-available'
10 | const AboutURI = 'atom://about'
11 |
12 | module.exports = {
13 | activate () {
14 | this.subscriptions = new CompositeDisposable()
15 |
16 | this.createModel()
17 |
18 | let availableVersion = window.localStorage.getItem(AvailableUpdateVersion)
19 | if (atom.getReleaseChannel() === 'dev' || (availableVersion && semver.lte(availableVersion, atom.getVersion()))) {
20 | this.clearUpdateState()
21 | }
22 |
23 | this.subscriptions.add(updateManager.onDidChange(() => {
24 | if (updateManager.getState() === UpdateManager.State.UpdateAvailableToInstall) {
25 | window.localStorage.setItem(AvailableUpdateVersion, updateManager.getAvailableVersion())
26 | this.showStatusBarIfNeeded()
27 | }
28 | }))
29 |
30 | this.subscriptions.add(atom.commands.add('atom-workspace', 'about:clear-update-state', () => {
31 | this.clearUpdateState()
32 | }))
33 | },
34 |
35 | deactivate () {
36 | this.model.destroy()
37 | if (this.statusBarTile) this.statusBarTile.destroy()
38 |
39 | if (updateManager) {
40 | updateManager.dispose()
41 | updateManager = undefined
42 | }
43 | },
44 |
45 | clearUpdateState () {
46 | window.localStorage.removeItem(AvailableUpdateVersion)
47 | },
48 |
49 | consumeStatusBar (statusBar) {
50 | this.statusBar = statusBar
51 | this.showStatusBarIfNeeded()
52 | },
53 |
54 | deserializeAboutView (state) {
55 | if (!this.model) {
56 | this.createModel()
57 | }
58 |
59 | return this.model.deserialize(state)
60 | },
61 |
62 | createModel () {
63 | updateManager = updateManager || new UpdateManager()
64 |
65 | this.model = new About({
66 | uri: AboutURI,
67 | currentAtomVersion: atom.getVersion(),
68 | currentElectronVersion: process.versions.electron,
69 | currentChromeVersion: process.versions.chrome,
70 | currentNodeVersion: process.version,
71 | updateManager: updateManager
72 | })
73 | },
74 |
75 | isUpdateAvailable () {
76 | let availableVersion = window.localStorage.getItem(AvailableUpdateVersion)
77 | return availableVersion && semver.gt(availableVersion, atom.getVersion())
78 | },
79 |
80 | showStatusBarIfNeeded () {
81 | if (this.isUpdateAvailable() && this.statusBar) {
82 | let statusBarView = new StatusBarView()
83 |
84 | if (this.statusBarTile) {
85 | this.statusBarTile.destroy()
86 | }
87 |
88 | this.statusBarTile = this.statusBar.addRightTile({
89 | item: statusBarView,
90 | priority: -100
91 | })
92 |
93 | return this.statusBarTile
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/update-manager.js:
--------------------------------------------------------------------------------
1 | const {Emitter, CompositeDisposable} = require('atom')
2 |
3 | const Unsupported = 'unsupported'
4 | const Idle = 'idle'
5 | const CheckingForUpdate = 'checking'
6 | const DownloadingUpdate = 'downloading'
7 | const UpdateAvailableToInstall = 'update-available'
8 | const UpToDate = 'no-update-available'
9 | const ErrorState = 'error'
10 |
11 | let UpdateManager = class UpdateManager {
12 | constructor () {
13 | this.emitter = new Emitter()
14 | this.currentVersion = atom.getVersion()
15 | this.availableVersion = atom.getVersion()
16 | this.resetState()
17 | this.listenForAtomEvents()
18 | }
19 |
20 | listenForAtomEvents () {
21 | this.subscriptions = new CompositeDisposable()
22 |
23 | this.subscriptions.add(
24 | atom.autoUpdater.onDidBeginCheckingForUpdate(() => {
25 | this.setState(CheckingForUpdate)
26 | }),
27 | atom.autoUpdater.onDidBeginDownloadingUpdate(() => {
28 | this.setState(DownloadingUpdate)
29 | }),
30 | atom.autoUpdater.onDidCompleteDownloadingUpdate(({releaseVersion}) => {
31 | this.setAvailableVersion(releaseVersion)
32 | }),
33 | atom.autoUpdater.onUpdateNotAvailable(() => {
34 | this.setState(UpToDate)
35 | }),
36 | atom.autoUpdater.onUpdateError(() => {
37 | this.setState(ErrorState)
38 | }),
39 | atom.config.observe('core.automaticallyUpdate', (value) => {
40 | this.autoUpdatesEnabled = value
41 | this.emitDidChange()
42 | })
43 | )
44 |
45 | // TODO: When https://github.com/atom/electron/issues/4587 is closed we can add this support.
46 | // atom.autoUpdater.onUpdateAvailable =>
47 | // @find('.about-updates-item').removeClass('is-shown')
48 | // @updateAvailable.addClass('is-shown')
49 | }
50 |
51 | dispose () {
52 | this.subscriptions.dispose()
53 | }
54 |
55 | onDidChange (callback) {
56 | return this.emitter.on('did-change', callback)
57 | }
58 |
59 | emitDidChange () {
60 | this.emitter.emit('did-change')
61 | }
62 |
63 | getAutoUpdatesEnabled () {
64 | return this.autoUpdatesEnabled && this.state !== UpdateManager.State.Unsupported
65 | }
66 |
67 | setAutoUpdatesEnabled (enabled) {
68 | return atom.config.set('core.automaticallyUpdate', enabled)
69 | }
70 |
71 | getErrorMessage () {
72 | return atom.autoUpdater.getErrorMessage()
73 | }
74 |
75 | getState () {
76 | return this.state
77 | }
78 |
79 | setState (state) {
80 | this.state = state
81 | this.emitDidChange()
82 | }
83 |
84 | resetState () {
85 | this.state = atom.autoUpdater.platformSupportsUpdates() ? atom.autoUpdater.getState() : Unsupported
86 | this.emitDidChange()
87 | }
88 |
89 | getAvailableVersion () {
90 | return this.availableVersion
91 | }
92 |
93 | setAvailableVersion (version) {
94 | this.availableVersion = version
95 |
96 | if (this.availableVersion !== this.currentVersion) {
97 | this.state = UpdateAvailableToInstall
98 | } else {
99 | this.state = UpToDate
100 | }
101 |
102 | this.emitDidChange()
103 | }
104 |
105 | checkForUpdate () {
106 | atom.autoUpdater.checkForUpdate()
107 | }
108 |
109 | restartAndInstallUpdate () {
110 | atom.autoUpdater.restartAndInstallUpdate()
111 | }
112 |
113 | getReleaseNotesURLForCurrentVersion () {
114 | return this.getReleaseNotesURLForVersion(this.currentVersion)
115 | }
116 |
117 | getReleaseNotesURLForAvailableVersion () {
118 | return this.getReleaseNotesURLForVersion(this.availableVersion)
119 | }
120 |
121 | getReleaseNotesURLForVersion (appVersion) {
122 | // Dev versions will not have a releases page
123 | if (appVersion.indexOf('dev') > -1) {
124 | return 'https://atom.io/releases'
125 | }
126 |
127 | if (!appVersion.startsWith('v')) {
128 | appVersion = `v${appVersion}`
129 | }
130 |
131 | const releaseRepo = appVersion.indexOf('nightly') > -1 ? 'atom-nightly-releases' : 'atom'
132 | return `https://github.com/atom/${releaseRepo}/releases/tag/${appVersion}`
133 | }
134 | }
135 |
136 | UpdateManager.State = {
137 | Unsupported: Unsupported,
138 | Idle: Idle,
139 | CheckingForUpdate: CheckingForUpdate,
140 | DownloadingUpdate: DownloadingUpdate,
141 | UpdateAvailableToInstall: UpdateAvailableToInstall,
142 | UpToDate: UpToDate,
143 | Error: ErrorState
144 | }
145 |
146 | module.exports = UpdateManager
147 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "about",
3 | "author": "Machisté N. Quintana ",
4 | "main": "./lib/main",
5 | "version": "1.9.1",
6 | "description": "View useful information about your Atom installation.",
7 | "keywords": [],
8 | "repository": "https://github.com/atom/about",
9 | "license": "MIT",
10 | "scripts": {
11 | "lint": "standard"
12 | },
13 | "engines": {
14 | "atom": ">=1.7 <2.0.0"
15 | },
16 | "dependencies": {
17 | "etch": "0.9.0",
18 | "semver": "^5.5.0"
19 | },
20 | "devDependencies": {
21 | "standard": "^11.0.0"
22 | },
23 | "consumedServices": {
24 | "status-bar": {
25 | "versions": {
26 | "^1.0.0": "consumeStatusBar"
27 | }
28 | }
29 | },
30 | "deserializers": {
31 | "AboutView": "deserializeAboutView"
32 | },
33 | "standard": {
34 | "env": [
35 | "browser",
36 | "node",
37 | "atomtest",
38 | "jasmine"
39 | ],
40 | "globals": [
41 | "atom"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/spec/about-spec.js:
--------------------------------------------------------------------------------
1 | const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./helpers/async-spec-helpers') // eslint-disable-line no-unused-vars
2 |
3 | describe('About', () => {
4 | let workspaceElement
5 |
6 | beforeEach(async () => {
7 | let storage = {}
8 |
9 | spyOn(window.localStorage, 'setItem').andCallFake((key, value) => {
10 | storage[key] = value
11 | })
12 | spyOn(window.localStorage, 'getItem').andCallFake((key) => {
13 | return storage[key]
14 | })
15 |
16 | workspaceElement = atom.views.getView(atom.workspace)
17 | await atom.packages.activatePackage('about')
18 | })
19 |
20 | it('deserializes correctly', () => {
21 | let deserializedAboutView = atom.deserializers.deserialize({
22 | deserializer: 'AboutView',
23 | uri: 'atom://about'
24 | })
25 |
26 | expect(deserializedAboutView).toBeTruthy()
27 | })
28 |
29 | describe('when the about:about-atom command is triggered', () => {
30 | it('shows the About Atom view', async () => {
31 | // Attaching the workspaceElement to the DOM is required to allow the
32 | // `toBeVisible()` matchers to work. Anything testing visibility or focus
33 | // requires that the workspaceElement is on the DOM. Tests that attach the
34 | // workspaceElement to the DOM are generally slower than those off DOM.
35 | jasmine.attachToDOM(workspaceElement)
36 |
37 | expect(workspaceElement.querySelector('.about')).not.toExist()
38 | await atom.workspace.open('atom://about')
39 |
40 | let aboutElement = workspaceElement.querySelector('.about')
41 | expect(aboutElement).toBeVisible()
42 | })
43 | })
44 |
45 | describe('when the Atom version number is clicked', () => {
46 | it('copies the version number to the clipboard', async () => {
47 | await atom.workspace.open('atom://about')
48 |
49 | let aboutElement = workspaceElement.querySelector('.about')
50 | let versionContainer = aboutElement.querySelector('.atom')
51 | versionContainer.click()
52 | expect(atom.clipboard.read()).toBe(atom.getVersion())
53 | })
54 | })
55 |
56 | describe('when the show more link is clicked', () => {
57 | it('expands to show additional version numbers', async () => {
58 | await atom.workspace.open('atom://about')
59 | jasmine.attachToDOM(workspaceElement)
60 |
61 | let aboutElement = workspaceElement.querySelector('.about')
62 | let showMoreElement = aboutElement.querySelector('.show-more-expand')
63 | let moreInfoElement = workspaceElement.querySelector('.show-more')
64 | showMoreElement.click()
65 | expect(moreInfoElement).toBeVisible()
66 | })
67 | })
68 |
69 | describe('when the Electron version number is clicked', () => {
70 | it('copies the version number to the clipboard', async () => {
71 | await atom.workspace.open('atom://about')
72 |
73 | let aboutElement = workspaceElement.querySelector('.about')
74 | let versionContainer = aboutElement.querySelector('.electron')
75 | versionContainer.click()
76 | expect(atom.clipboard.read()).toBe(process.versions.electron)
77 | })
78 | })
79 |
80 | describe('when the Chrome version number is clicked', () => {
81 | it('copies the version number to the clipboard', async () => {
82 | await atom.workspace.open('atom://about')
83 |
84 | let aboutElement = workspaceElement.querySelector('.about')
85 | let versionContainer = aboutElement.querySelector('.chrome')
86 | versionContainer.click()
87 | expect(atom.clipboard.read()).toBe(process.versions.chrome)
88 | })
89 | })
90 |
91 | describe('when the Node version number is clicked', () => {
92 | it('copies the version number to the clipboard', async () => {
93 | await atom.workspace.open('atom://about')
94 |
95 | let aboutElement = workspaceElement.querySelector('.about')
96 | let versionContainer = aboutElement.querySelector('.node')
97 | versionContainer.click()
98 | expect(atom.clipboard.read()).toBe(process.version)
99 | })
100 | })
101 | })
102 |
--------------------------------------------------------------------------------
/spec/about-status-bar-spec.js:
--------------------------------------------------------------------------------
1 | const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./helpers/async-spec-helpers') // eslint-disable-line no-unused-vars
2 | const MockUpdater = require('./mocks/updater')
3 |
4 | describe('the status bar', () => {
5 | let atomVersion
6 | let workspaceElement
7 |
8 | beforeEach(async () => {
9 | let storage = {}
10 |
11 | spyOn(window.localStorage, 'setItem').andCallFake((key, value) => {
12 | storage[key] = value
13 | })
14 | spyOn(window.localStorage, 'getItem').andCallFake((key) => {
15 | return storage[key]
16 | })
17 | spyOn(atom, 'getVersion').andCallFake(() => {
18 | return atomVersion
19 | })
20 |
21 | workspaceElement = atom.views.getView(atom.workspace)
22 |
23 | await atom.packages.activatePackage('status-bar')
24 | await atom.workspace.open('sample.js')
25 | })
26 |
27 | afterEach(async () => {
28 | await atom.packages.deactivatePackage('about')
29 | await atom.packages.deactivatePackage('status-bar')
30 | })
31 |
32 | describe('on a stable version', function () {
33 | beforeEach(async () => {
34 | atomVersion = '1.2.3'
35 |
36 | await atom.packages.activatePackage('about')
37 | })
38 |
39 | describe('with no update', () => {
40 | it('does not show the view', () => {
41 | expect(workspaceElement).not.toContain('.about-release-notes')
42 | })
43 | })
44 |
45 | describe('with an update', () => {
46 | it('shows the view when the update finishes downloading', () => {
47 | MockUpdater.finishDownloadingUpdate('42.0.0')
48 | expect(workspaceElement).toContain('.about-release-notes')
49 | })
50 |
51 | describe('clicking on the status', () => {
52 | it('opens the about page', async () => {
53 | MockUpdater.finishDownloadingUpdate('42.0.0')
54 | workspaceElement.querySelector('.about-release-notes').click()
55 | await conditionPromise(() => workspaceElement.querySelector('.about'))
56 | expect(workspaceElement.querySelector('.about')).toExist()
57 | })
58 | })
59 |
60 | it('continues to show the squirrel until Atom is updated to the new version', async () => {
61 | MockUpdater.finishDownloadingUpdate('42.0.0')
62 | expect(workspaceElement).toContain('.about-release-notes')
63 |
64 | await atom.packages.deactivatePackage('about')
65 | expect(workspaceElement).not.toContain('.about-release-notes')
66 |
67 | await atom.packages.activatePackage('about')
68 | await Promise.resolve() // Service consumption hooks are deferred until the next tick
69 | expect(workspaceElement).toContain('.about-release-notes')
70 |
71 | await atom.packages.deactivatePackage('about')
72 | expect(workspaceElement).not.toContain('.about-release-notes')
73 |
74 | atomVersion = '42.0.0'
75 | await atom.packages.activatePackage('about')
76 |
77 | await Promise.resolve() // Service consumption hooks are deferred until the next tick
78 | expect(workspaceElement).not.toContain('.about-release-notes')
79 | })
80 |
81 | it('does not show the view if Atom is updated to a newer version than notified', async () => {
82 | MockUpdater.finishDownloadingUpdate('42.0.0')
83 |
84 | await atom.packages.deactivatePackage('about')
85 |
86 | atomVersion = '43.0.0'
87 | await atom.packages.activatePackage('about')
88 |
89 | await Promise.resolve() // Service consumption hooks are deferred until the next tick
90 | expect(workspaceElement).not.toContain('.about-release-notes')
91 | })
92 | })
93 | })
94 |
95 | describe('on a beta version', function () {
96 | beforeEach(async () => {
97 | atomVersion = '1.2.3-beta4'
98 |
99 | await atom.packages.activatePackage('about')
100 | })
101 |
102 | describe('with no update', () => {
103 | it('does not show the view', () => {
104 | expect(workspaceElement).not.toContain('.about-release-notes')
105 | })
106 | })
107 |
108 | describe('with an update', () => {
109 | it('shows the view when the update finishes downloading', () => {
110 | MockUpdater.finishDownloadingUpdate('42.0.0')
111 | expect(workspaceElement).toContain('.about-release-notes')
112 | })
113 |
114 | describe('clicking on the status', () => {
115 | it('opens the about page', async () => {
116 | MockUpdater.finishDownloadingUpdate('42.0.0')
117 | workspaceElement.querySelector('.about-release-notes').click()
118 | await conditionPromise(() => workspaceElement.querySelector('.about'))
119 | expect(workspaceElement.querySelector('.about')).toExist()
120 | })
121 | })
122 |
123 | it('continues to show the squirrel until Atom is updated to the new version', async () => {
124 | MockUpdater.finishDownloadingUpdate('42.0.0')
125 | expect(workspaceElement).toContain('.about-release-notes')
126 |
127 | await atom.packages.deactivatePackage('about')
128 | expect(workspaceElement).not.toContain('.about-release-notes')
129 |
130 | await atom.packages.activatePackage('about')
131 | await Promise.resolve() // Service consumption hooks are deferred until the next tick
132 | expect(workspaceElement).toContain('.about-release-notes')
133 |
134 | await atom.packages.deactivatePackage('about')
135 | expect(workspaceElement).not.toContain('.about-release-notes')
136 |
137 | atomVersion = '42.0.0'
138 | await atom.packages.activatePackage('about')
139 |
140 | await Promise.resolve() // Service consumption hooks are deferred until the next tick
141 | expect(workspaceElement).not.toContain('.about-release-notes')
142 | })
143 |
144 | it('does not show the view if Atom is updated to a newer version than notified', async () => {
145 | MockUpdater.finishDownloadingUpdate('42.0.0')
146 |
147 | await atom.packages.deactivatePackage('about')
148 |
149 | atomVersion = '43.0.0'
150 | await atom.packages.activatePackage('about')
151 |
152 | await Promise.resolve() // Service consumption hooks are deferred until the next tick
153 | expect(workspaceElement).not.toContain('.about-release-notes')
154 | })
155 | })
156 | })
157 |
158 | describe('on a development version', function () {
159 | beforeEach(async () => {
160 | atomVersion = '1.2.3-dev-0123abcd'
161 |
162 | await atom.packages.activatePackage('about')
163 | })
164 |
165 | describe('with no update', () => {
166 | it('does not show the view', () => {
167 | expect(workspaceElement).not.toContain('.about-release-notes')
168 | })
169 | })
170 |
171 | describe('with a previously downloaded update', () => {
172 | it('does not show the view', () => {
173 | window.localStorage.setItem('about:version-available', '42.0.0')
174 |
175 | expect(workspaceElement).not.toContain('.about-release-notes')
176 | })
177 | })
178 | })
179 | })
180 |
--------------------------------------------------------------------------------
/spec/helpers/async-spec-helpers.js:
--------------------------------------------------------------------------------
1 | /** @babel */
2 |
3 | const {now} = Date
4 | const {setTimeout} = global
5 |
6 | export function beforeEach (fn) {
7 | global.beforeEach(function () {
8 | const result = fn()
9 | if (result instanceof Promise) {
10 | waitsForPromise(() => result)
11 | }
12 | })
13 | }
14 |
15 | export function afterEach (fn) {
16 | global.afterEach(function () {
17 | const result = fn()
18 | if (result instanceof Promise) {
19 | waitsForPromise(() => result)
20 | }
21 | })
22 | }
23 |
24 | ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
25 | module.exports[name] = function (description, fn) {
26 | global[name](description, function () {
27 | const result = fn()
28 | if (result instanceof Promise) {
29 | waitsForPromise(() => result)
30 | }
31 | })
32 | }
33 | })
34 |
35 | export async function conditionPromise (condition) {
36 | const startTime = now()
37 |
38 | while (true) {
39 | await timeoutPromise(100)
40 |
41 | if (await condition()) {
42 | return
43 | }
44 |
45 | if (now() - startTime > 5000) {
46 | throw new Error('Timed out waiting on condition')
47 | }
48 | }
49 | }
50 |
51 | export function timeoutPromise (timeout) {
52 | return new Promise(function (resolve) {
53 | setTimeout(resolve, timeout)
54 | })
55 | }
56 |
57 | function waitsForPromise (fn) {
58 | const promise = fn()
59 | global.waitsFor('spec promise to resolve', function (done) {
60 | promise.then(done, function (error) {
61 | jasmine.getEnv().currentSpec.fail(error)
62 | done()
63 | })
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/spec/mocks/updater.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | updateError () {
3 | atom.autoUpdater.emitter.emit('update-error')
4 | },
5 |
6 | checkForUpdate () {
7 | atom.autoUpdater.emitter.emit('did-begin-checking-for-update')
8 | },
9 |
10 | updateNotAvailable () {
11 | atom.autoUpdater.emitter.emit('update-not-available')
12 | },
13 |
14 | downloadUpdate () {
15 | atom.autoUpdater.emitter.emit('did-begin-downloading-update')
16 | },
17 |
18 | finishDownloadingUpdate (releaseVersion) {
19 | atom.autoUpdater.emitter.emit('did-complete-downloading-update', {releaseVersion})
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/spec/update-manager-spec.js:
--------------------------------------------------------------------------------
1 | const UpdateManager = require('../lib/update-manager')
2 |
3 | describe('UpdateManager', () => {
4 | let updateManager
5 |
6 | beforeEach(() => {
7 | updateManager = new UpdateManager()
8 | })
9 |
10 | describe('::getReleaseNotesURLForVersion', () => {
11 | it('returns atom.io releases when dev version', () => {
12 | expect(updateManager.getReleaseNotesURLForVersion('1.7.0-dev-e44b57d')).toContain('atom.io/releases')
13 | })
14 |
15 | it('returns the page for the release when not a dev version', () => {
16 | expect(updateManager.getReleaseNotesURLForVersion('1.7.0')).toContain('atom/atom/releases/tag/v1.7.0')
17 | expect(updateManager.getReleaseNotesURLForVersion('v1.7.0')).toContain('atom/atom/releases/tag/v1.7.0')
18 | expect(updateManager.getReleaseNotesURLForVersion('1.7.0-beta10')).toContain('atom/atom/releases/tag/v1.7.0-beta10')
19 | expect(updateManager.getReleaseNotesURLForVersion('1.7.0-nightly10')).toContain('atom/atom-nightly-releases/releases/tag/v1.7.0-nightly10')
20 | })
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/spec/update-view-spec.js:
--------------------------------------------------------------------------------
1 | const {shell} = require('electron')
2 | const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./helpers/async-spec-helpers') // eslint-disable-line no-unused-vars
3 | const main = require('../lib/main')
4 | const AboutView = require('../lib/components/about-view')
5 | const UpdateView = require('../lib/components/update-view')
6 | const MockUpdater = require('./mocks/updater')
7 |
8 | describe('UpdateView', () => {
9 | let aboutElement
10 | let updateManager
11 | let workspaceElement
12 | let scheduler
13 |
14 | beforeEach(async () => {
15 | let storage = {}
16 |
17 | spyOn(window.localStorage, 'setItem').andCallFake((key, value) => {
18 | storage[key] = value
19 | })
20 | spyOn(window.localStorage, 'getItem').andCallFake((key) => {
21 | return storage[key]
22 | })
23 |
24 | workspaceElement = atom.views.getView(atom.workspace)
25 | await atom.packages.activatePackage('about')
26 | spyOn(atom.autoUpdater, 'getState').andReturn('idle')
27 | spyOn(atom.autoUpdater, 'checkForUpdate')
28 | spyOn(atom.autoUpdater, 'platformSupportsUpdates').andReturn(true)
29 | })
30 |
31 | describe('when the About page is open', () => {
32 | beforeEach(async () => {
33 | jasmine.attachToDOM(workspaceElement)
34 | await atom.workspace.open('atom://about')
35 | aboutElement = workspaceElement.querySelector('.about')
36 | updateManager = main.model.state.updateManager
37 | scheduler = AboutView.getScheduler()
38 | })
39 |
40 | describe('when the updates are not supported by the platform', () => {
41 | beforeEach(async () => {
42 | atom.autoUpdater.platformSupportsUpdates.andReturn(false)
43 | updateManager.resetState()
44 | await scheduler.getNextUpdatePromise()
45 | })
46 |
47 | it('hides the auto update UI and shows the update instructions link', async () => {
48 | expect(aboutElement.querySelector('.about-update-action-button')).not.toBeVisible()
49 | expect(aboutElement.querySelector('.about-auto-updates')).not.toBeVisible()
50 | })
51 |
52 | it('opens the update instructions page when the instructions link is clicked', async () => {
53 | spyOn(shell, 'openExternal')
54 | let link = aboutElement.querySelector('.app-unsupported .about-updates-instructions')
55 | link.click()
56 |
57 | let args = shell.openExternal.mostRecentCall.args
58 | expect(shell.openExternal).toHaveBeenCalled()
59 | expect(args[0]).toContain('installing-atom')
60 | })
61 | })
62 |
63 | describe('when updates are supported by the platform', () => {
64 | beforeEach(async () => {
65 | atom.autoUpdater.platformSupportsUpdates.andReturn(true)
66 | updateManager.resetState()
67 | await scheduler.getNextUpdatePromise()
68 | })
69 |
70 | it('shows the auto update UI', () => {
71 | expect(aboutElement.querySelector('.about-updates')).toBeVisible()
72 | })
73 |
74 | it('shows the correct panels when the app checks for updates and there is no update available', async () => {
75 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
76 |
77 | MockUpdater.checkForUpdate()
78 | await scheduler.getNextUpdatePromise()
79 | expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible()
80 | expect(aboutElement.querySelector('.app-checking-for-updates')).toBeVisible()
81 |
82 | MockUpdater.updateNotAvailable()
83 | await scheduler.getNextUpdatePromise()
84 | expect(aboutElement.querySelector('.app-up-to-date')).toBeVisible()
85 | expect(aboutElement.querySelector('.app-checking-for-updates')).not.toBeVisible()
86 | })
87 |
88 | it('shows the correct panels when the app checks for updates and encounters an error', async () => {
89 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
90 |
91 | MockUpdater.checkForUpdate()
92 | await scheduler.getNextUpdatePromise()
93 | expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible()
94 | expect(aboutElement.querySelector('.app-checking-for-updates')).toBeVisible()
95 |
96 | spyOn(atom.autoUpdater, 'getErrorMessage').andReturn('an error message')
97 | MockUpdater.updateError()
98 | await scheduler.getNextUpdatePromise()
99 | expect(aboutElement.querySelector('.app-update-error')).toBeVisible()
100 | expect(aboutElement.querySelector('.app-error-message').textContent).toBe('an error message')
101 | expect(aboutElement.querySelector('.app-checking-for-updates')).not.toBeVisible()
102 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(false)
103 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Check now')
104 | })
105 |
106 | it('shows the correct panels and button states when the app checks for updates and an update is downloaded', async () => {
107 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
108 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(false)
109 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Check now')
110 |
111 | MockUpdater.checkForUpdate()
112 | await scheduler.getNextUpdatePromise()
113 |
114 | expect(aboutElement.querySelector('.app-up-to-date')).not.toBeVisible()
115 | expect(aboutElement.querySelector('.app-checking-for-updates')).toBeVisible()
116 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(true)
117 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Check now')
118 |
119 | MockUpdater.downloadUpdate()
120 | await scheduler.getNextUpdatePromise()
121 | expect(aboutElement.querySelector('.app-checking-for-updates')).not.toBeVisible()
122 | expect(aboutElement.querySelector('.app-downloading-update')).toBeVisible()
123 | // TODO: at some point it would be nice to be able to cancel an update download, and then this would be a cancel button
124 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(true)
125 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Check now')
126 |
127 | MockUpdater.finishDownloadingUpdate('42.0.0')
128 | await scheduler.getNextUpdatePromise()
129 | expect(aboutElement.querySelector('.app-downloading-update')).not.toBeVisible()
130 | expect(aboutElement.querySelector('.app-update-available-to-install')).toBeVisible()
131 |
132 | expect(aboutElement.querySelector('.app-update-available-to-install .about-updates-version').textContent).toBe('42.0.0')
133 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(false)
134 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Restart and install')
135 | })
136 |
137 | it('opens the release notes for the downloaded release when the release notes link are clicked', async () => {
138 | MockUpdater.finishDownloadingUpdate('1.2.3')
139 | await scheduler.getNextUpdatePromise()
140 |
141 | spyOn(shell, 'openExternal')
142 | let link = aboutElement.querySelector('.app-update-available-to-install .about-updates-release-notes')
143 | link.click()
144 |
145 | let args = shell.openExternal.mostRecentCall.args
146 | expect(shell.openExternal).toHaveBeenCalled()
147 | expect(args[0]).toContain('/v1.2.3')
148 | })
149 |
150 | it('executes checkForUpdate() when the check for update button is clicked', () => {
151 | let button = aboutElement.querySelector('.about-update-action-button')
152 | button.click()
153 | expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled()
154 | })
155 |
156 | it('executes restartAndInstallUpdate() when the restart and install button is clicked', async () => {
157 | spyOn(atom.autoUpdater, 'restartAndInstallUpdate')
158 | MockUpdater.finishDownloadingUpdate('42.0.0')
159 | await scheduler.getNextUpdatePromise()
160 |
161 | let button = aboutElement.querySelector('.about-update-action-button')
162 | button.click()
163 | expect(atom.autoUpdater.restartAndInstallUpdate).toHaveBeenCalled()
164 | })
165 |
166 | it("starts in the same state as atom's AutoUpdateManager", async () => {
167 | atom.autoUpdater.getState.andReturn('downloading')
168 | updateManager.resetState()
169 |
170 | await scheduler.getNextUpdatePromise()
171 | expect(aboutElement.querySelector('.app-checking-for-updates')).not.toBeVisible()
172 | expect(aboutElement.querySelector('.app-downloading-update')).toBeVisible()
173 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(true)
174 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Check now')
175 | })
176 |
177 | describe('when core.automaticallyUpdate is toggled', () => {
178 | beforeEach(async () => {
179 | expect(atom.config.get('core.automaticallyUpdate')).toBe(true)
180 | atom.autoUpdater.checkForUpdate.reset()
181 | })
182 |
183 | it('shows the auto update UI', async () => {
184 | expect(aboutElement.querySelector('.about-auto-updates input').checked).toBe(true)
185 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
186 | expect(aboutElement.querySelector('.about-default-update-message').textContent).toBe('Atom will check for updates automatically')
187 |
188 | atom.config.set('core.automaticallyUpdate', false)
189 | await scheduler.getNextUpdatePromise()
190 |
191 | expect(aboutElement.querySelector('.about-auto-updates input').checked).toBe(false)
192 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
193 | expect(aboutElement.querySelector('.about-default-update-message').textContent).toBe('Automatic updates are disabled please check manually')
194 | })
195 |
196 | it('updates config and the UI when the checkbox is used to toggle', async () => {
197 | expect(aboutElement.querySelector('.about-auto-updates input').checked).toBe(true)
198 |
199 | aboutElement.querySelector('.about-auto-updates input').click()
200 | await scheduler.getNextUpdatePromise()
201 |
202 | expect(atom.config.get('core.automaticallyUpdate')).toBe(false)
203 | expect(aboutElement.querySelector('.about-auto-updates input').checked).toBe(false)
204 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
205 | expect(aboutElement.querySelector('.about-default-update-message').textContent).toBe('Automatic updates are disabled please check manually')
206 |
207 | aboutElement.querySelector('.about-auto-updates input').click()
208 | await scheduler.getNextUpdatePromise()
209 |
210 | expect(atom.config.get('core.automaticallyUpdate')).toBe(true)
211 | expect(aboutElement.querySelector('.about-auto-updates input').checked).toBe(true)
212 | expect(aboutElement.querySelector('.about-default-update-message')).toBeVisible()
213 | expect(aboutElement.querySelector('.about-default-update-message').textContent).toBe('Atom will check for updates automatically')
214 | })
215 |
216 | describe('checking for updates', function () {
217 | afterEach(() => {
218 | this.updateView = null
219 | })
220 |
221 | it('checks for update when the about page is shown', () => {
222 | expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled()
223 |
224 | this.updateView = new UpdateView({
225 | updateManager: updateManager,
226 | availableVersion: '9999.0.0',
227 | viewUpdateReleaseNotes: () => {}
228 | })
229 |
230 | expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled()
231 | })
232 |
233 | it('does not check for update when the about page is shown and the update manager is not in the idle state', () => {
234 | atom.autoUpdater.getState.andReturn('downloading')
235 | updateManager.resetState()
236 | expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled()
237 |
238 | this.updateView = new UpdateView({
239 | updateManager: updateManager,
240 | availableVersion: '9999.0.0',
241 | viewUpdateReleaseNotes: () => {}
242 | })
243 |
244 | expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled()
245 | })
246 |
247 | it('does not check for update when the about page is shown and auto updates are turned off', () => {
248 | atom.config.set('core.automaticallyUpdate', false)
249 | expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled()
250 |
251 | this.updateView = new UpdateView({
252 | updateManager: updateManager,
253 | availableVersion: '9999.0.0',
254 | viewUpdateReleaseNotes: () => {}
255 | })
256 |
257 | expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled()
258 | })
259 | })
260 | })
261 | })
262 | })
263 |
264 | describe('when the About page is not open and an update is downloaded', () => {
265 | it('should display the new version when it is opened', async () => {
266 | MockUpdater.finishDownloadingUpdate('42.0.0')
267 |
268 | jasmine.attachToDOM(workspaceElement)
269 | await atom.workspace.open('atom://about')
270 | aboutElement = workspaceElement.querySelector('.about')
271 | updateManager = main.model.state.updateManager
272 | scheduler = AboutView.getScheduler()
273 |
274 | expect(aboutElement.querySelector('.app-update-available-to-install')).toBeVisible()
275 | expect(aboutElement.querySelector('.app-update-available-to-install .about-updates-version').textContent).toBe('42.0.0')
276 | expect(aboutElement.querySelector('.about-update-action-button').disabled).toBe(false)
277 | expect(aboutElement.querySelector('.about-update-action-button').textContent).toBe('Restart and install')
278 | })
279 | })
280 | })
281 |
--------------------------------------------------------------------------------
/styles/about.less:
--------------------------------------------------------------------------------
1 | @import "ui-variables";
2 | @import "variables";
3 |
4 | .about {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: flex-start;
9 | -webkit-user-select: none;
10 | cursor: default;
11 | overflow: auto;
12 | text-align: center;
13 | font-size: 1.25em;
14 | line-height: 1.4;
15 | padding: 4em;
16 | color: @text-color;
17 | background-color: @base-background-color;
18 |
19 | button {
20 | cursor: default;
21 | }
22 |
23 | a:focus {
24 | // Don't use Bootstrap default here
25 | color: inherit;
26 | }
27 |
28 | img, a {
29 | -webkit-user-drag: none;
30 | }
31 |
32 | .input-checkbox {
33 | margin-top: -.2em;
34 | }
35 |
36 | // used to group different elements
37 | .group-start {
38 | margin-top: 4em;
39 | }
40 | .group-item {
41 | margin-top: 1.5em;
42 | }
43 | }
44 |
45 | .about-container {
46 | width: 100%;
47 | max-width: 500px;
48 | }
49 |
50 | // Header --------------------------------
51 |
52 | .about-atom-io:hover {
53 | .about-logo {
54 | color: @atom-green;
55 | }
56 | }
57 |
58 | .about-logo {
59 | display: block;
60 | width: 100%;
61 | max-width: 280px;
62 | margin: 0 auto 1em auto;
63 | color: @text-color-highlight;
64 | transition: color 0.2s;
65 | }
66 |
67 | .about-version-container {
68 | &:hover {
69 | color: lighten(@text-color, 15%);
70 | }
71 | &:active {
72 | color: lighten(@text-color, 30%);
73 | }
74 | }
75 |
76 | .about-version {
77 | margin-right: .5em;
78 | font-size: 1.25em;
79 | vertical-align: middle;
80 | }
81 |
82 | .about-more-version {
83 | color: @text-color-subtle;
84 | font-size: .9em;
85 | }
86 |
87 | .about-header-release-notes {
88 | vertical-align: middle;
89 | margin-left: 1em;
90 | }
91 |
92 |
93 | // Updates --------------------------------
94 |
95 | .about-updates {
96 | width: 100%;
97 | max-width: 500px;
98 | }
99 |
100 | .about-updates-box {
101 | display: flex;
102 | align-items: center;
103 | padding: @component-padding;
104 | border: 1px solid @base-border-color;
105 | border-radius: @component-border-radius * 2;
106 | background-color: @background-color-highlight;
107 | }
108 |
109 | .about-updates-status {
110 | flex: 1;
111 | margin-left: .5em;
112 | text-align: left;
113 | }
114 |
115 | .about-updates-item,
116 | .about-default-update-message .about-updates-label {
117 | display: block;
118 | }
119 |
120 | .about-updates-label {
121 | color: @text-color-subtle;
122 | &.is-strong {
123 | color: @text-color;
124 | }
125 | }
126 |
127 | .about-updates-version {
128 | margin: 0 .4em;
129 | }
130 |
131 | .about-updates-release-notes,
132 | .about-updates-instructions {
133 | margin: 0 1em 0 1.5em;
134 | }
135 |
136 | .about-auto-updates {
137 | margin-top: 1em;
138 | input {
139 | margin-right: .5em;
140 | }
141 | }
142 |
143 |
144 | // Love --------------------------------
145 |
146 | .about-love {
147 | .icon::before {
148 | // Make these octicons look good inlined with text
149 | position: relative;
150 | width: auto;
151 | height: auto;
152 | margin-right: 0;
153 | font-size: 1.5em;
154 | vertical-align: text-top;
155 | }
156 |
157 | .icon-logo-github::before {
158 | font-size: 3.6em;
159 | height: .36em;
160 | }
161 | }
162 |
163 | .about-credits {
164 | color: @text-color-subtle;
165 | }
166 |
167 |
168 | // the blue squirrel --------------------------------
169 |
170 | .about-release-notes {
171 | color: @background-color-info;
172 | &:hover {
173 | color: lighten(@background-color-info, 15%);
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/styles/variables.less:
--------------------------------------------------------------------------------
1 | @atom-green: #40a977;
2 |
--------------------------------------------------------------------------------