├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── addon ├── .gitkeep ├── components │ └── async-await.js └── templates │ └── components │ └── async-await.hbs ├── app ├── .gitkeep └── components │ └── async-await.js ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ └── application.hbs │ ├── config │ │ ├── ember-cli-update.json │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ └── robots.txt ├── helpers │ └── .gitkeep ├── index.html ├── integration │ ├── .gitkeep │ └── components │ │ └── async-await-test.js ├── test-helper.js └── unit │ └── .gitkeep ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: {}, 23 | overrides: [ 24 | // node files 25 | { 26 | files: [ 27 | '.eslintrc.js', 28 | '.prettierrc.js', 29 | '.template-lintrc.js', 30 | 'ember-cli-build.js', 31 | 'index.js', 32 | 'testem.js', 33 | 'blueprints/*/index.js', 34 | 'config/**/*.js', 35 | 'tests/dummy/config/**/*.js', 36 | ], 37 | excludedFiles: [ 38 | 'addon/**', 39 | 'addon-test-support/**', 40 | 'app/**', 41 | 'tests/dummy/app/**', 42 | ], 43 | parserOptions: { 44 | sourceType: 'script', 45 | }, 46 | env: { 47 | browser: false, 48 | node: true, 49 | }, 50 | plugins: ['node'], 51 | extends: ['plugin:node/recommended'], 52 | }, 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn.lock 30 | .gitkeep 31 | 32 | # ember-try 33 | /.node_modules.ember-try/ 34 | /bower.json.ember-try 35 | /package.json.ember-try 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane', 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "12" 7 | 8 | dist: xenial 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | yarn: true 15 | 16 | env: 17 | global: 18 | # See https://git.io/vdao3 for details. 19 | - JOBS=1 20 | 21 | branches: 22 | only: 23 | - master 24 | # npm version tags 25 | - /^v\d+\.\d+\.\d+/ 26 | 27 | jobs: 28 | fast_finish: true 29 | allow_failures: 30 | - env: EMBER_TRY_SCENARIO=ember-canary 31 | 32 | include: 33 | # runs linting and tests with current locked deps 34 | - stage: "Tests" 35 | name: "Tests" 36 | script: 37 | - yarn lint 38 | - yarn test:ember 39 | 40 | - stage: "Additional Tests" 41 | name: "Floating Dependencies" 42 | install: 43 | - yarn install --no-lockfile --non-interactive 44 | script: 45 | - yarn test:ember 46 | 47 | # we recommend new addons test the current and previous LTS 48 | # as well as latest stable release (bonus points to beta/canary) 49 | - env: EMBER_TRY_SCENARIO=ember-lts-3.16 50 | - env: EMBER_TRY_SCENARIO=ember-lts-3.20 51 | - env: EMBER_TRY_SCENARIO=ember-lts-3.24 52 | - env: EMBER_TRY_SCENARIO=ember-release 53 | - env: EMBER_TRY_SCENARIO=ember-beta 54 | - env: EMBER_TRY_SCENARIO=ember-canary 55 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 56 | - env: EMBER_TRY_SCENARIO=ember-classic 57 | - env: EMBER_TRY_SCENARIO=embroider-safe 58 | - env: EMBER_TRY_SCENARIO=embroider-optimized 59 | 60 | before_install: 61 | - curl -o- -L https://yarnpkg.com/install.sh | bash 62 | - export PATH=$HOME/.yarn/bin:$PATH 63 | 64 | script: 65 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 66 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-async-await-helper` 7 | * `yarn install` 8 | 9 | ## Linting 10 | 11 | * `yarn lint` 12 | * `yarn lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | * `ember serve` 23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Tilde Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-async-await-helper 2 | 3 | [![Build Status](https://travis-ci.com/tildeio/ember-async-await-helper.svg?branch=master)](https://travis-ci.com/tildeio/ember-async-await-helper) 4 | 5 | Awaits a promise, then yields its result to a block. 👌 6 | 7 | ## Compatibility 8 | 9 | * Ember.js v3.16 or above 10 | * Ember CLI v2.13 or above 11 | * Node.js v12 or above 12 | 13 | 14 | ## Installation 15 | 16 | ``` 17 | ember install ember-async-await-helper 18 | ``` 19 | 20 | 21 | ## Usage 22 | 23 | The `{{#async-await}}` template helper takes a promise as a positional parameter, a block to render once the promise is resolved. Once the promise is resolved, the helper yields the promise's result to the block. 24 | 25 | ```hbs 26 | {{#async-await this.users as |users|}} 27 | 28 | {{/async-await}} 29 | ``` 30 | 31 | If the passed in value is not a promise, it will be converted to one using `Promise.resolve()`. 32 | 33 | ### Loading States 34 | 35 | Optionally, you can pass an inverse block to be displayed while the promise is pending. 36 | 37 | ```hbs 38 | {{#async-await this.users as |users|}} 39 | 40 | {{else}} 41 | 42 | {{/async-await}} 43 | ``` 44 | 45 | ### Error Handling 46 | 47 | In general, it's a bad idea to pass a fallible promise into the template. By default, if your promise rejects, `{{#async-await}}` calls `Ember.onerror`, which should trigger your typical error handling paths, such as showing a "something went wrong..." screen and/or reporting to Bugsnag. 48 | 49 | The default error object comes with a `reason` property set to the promise's rejection reason: 50 | 51 | ```js 52 | Ember.onerror = function(error) { 53 | console.error(error.message); // => Unhandled promise rejection in {{#async-await}}: **rejection reason** 54 | 55 | console.error(error.reason); // => **rejection reason** 56 | }; 57 | ``` 58 | 59 | Note that after the promise rejects, the `{{#async-await}}` helper will remain in the "pending" state (i.e. the `{{else}}` block). 60 | 61 | #### Recommended Method 62 | 63 | In order to avoid dealing with rejections in the template, it is recommended that you wrap your promises in an async function that handles any expected error scenarios, so that the promise is (mostly) infallible: 64 | 65 | ```js 66 | export default Component.extend({ 67 | users: computed(async function() { 68 | let retries = 0; 69 | 70 | while (retries < 5) { 71 | try { 72 | return await fetch('/users.json'); 73 | } catch (e) { 74 | if (isNetworkError(e)) { 75 | retries += 1; 76 | } else { 77 | // Unexpected Error! We can let this trigger the default 78 | // `onReject` callback. In our `Ember.onerror` handler, 79 | // we will transition the app into a generic error route. 80 | throw e; 81 | } 82 | } 83 | } 84 | }) 85 | }); 86 | ``` 87 | 88 | For any non-trivial functionality, you may also want to consider using an [ember-concurrency](https://ember-concurrency.com/) task instead. [Read on](#using-with-ember-concurrency) for how to use the `{{#async-await}}` helper together with ember-concurrency. 89 | 90 | #### Inline `onReject` callbacks 91 | 92 | While the above method is recommended, it is also possible to pass an `onReject` callback to run when the promise rejects: 93 | 94 | ```hbs 95 | {{#async-await this.users onReject=handleError as |users|}} 96 | 97 | {{/async-await}} 98 | ``` 99 | 100 | As mentioned above, after the promise rejects, the `{{#async-await}}` helper will remain in the "pending" state (i.e. the `{{else}}` block). Your rejection handler can retry the original operation by replacing the promise passed to the `{{#async-await}}` helper: 101 | 102 | ```js 103 | export default Component.extend({ 104 | // ... 105 | 106 | handleError(reason) { 107 | if (isNetworkError(reason)) { 108 | // retry the fetch 109 | this.set('users', fetch('/users.json')); 110 | } else { 111 | // show a "something went wrong" modal 112 | handleUnexpectedError(reason); 113 | } 114 | } 115 | }); 116 | ``` 117 | 118 | Finally, if you really want to, you can also pass `null` to silence the rejections completely: 119 | 120 | ```hbs 121 | {{#async-await this.users onReject=null as |users|}} 122 | 123 | {{/async-await}} 124 | ``` 125 | 126 | ### Using with `ember-concurrency` 127 | 128 | Did you know that `ember-concurrency` tasks (`TaskInstance`s to be exact) are also promise-like objects (they have a `.then` method on them). That means, you can await them with the `{{#async-await}}` just like any other promises! 129 | 130 | ```js 131 | export default Component.extend({ 132 | init() { 133 | this._super(...arguments); 134 | this.fetchUsers.perform(); 135 | }, 136 | 137 | users: alias('fetchUsers.last'), 138 | 139 | fetchUsers: task(function * () { 140 | let retries = 0; 141 | 142 | while (retries < 5) { 143 | try { 144 | return yield fetch('/users.json'); 145 | } catch (e) { 146 | if (isNetworkError(e)) { 147 | retries += 1; 148 | } else { 149 | // this will trigger the default `onReject` 150 | throw e; 151 | } 152 | } 153 | } 154 | }).restartable() 155 | }); 156 | ``` 157 | 158 | With this setup, you can continue to pass `this.users` to the `{{#async-await}}` helper as you normally would: 159 | 160 | ```hbs 161 | {{#async-await this.users as |users|}} 162 | 163 | {{else}} 164 | 165 | {{/async-await}} 166 | ``` 167 | 168 | ## Contributing 169 | 170 | See the [Contributing](CONTRIBUTING.md) guide for details. 171 | 172 | 173 | ## License 174 | 175 | This project is licensed under the [MIT License](LICENSE.md). 176 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/addon/.gitkeep -------------------------------------------------------------------------------- /addon/components/async-await.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable ember/no-component-lifecycle-hooks */ 2 | /* eslint-disable ember/no-classic-classes */ 3 | /* eslint-disable ember/no-classic-components */ 4 | 5 | import { VERSION } from '@ember/version'; 6 | import Component from '@ember/component'; 7 | import { bind } from '@ember/runloop'; 8 | import Ember from 'ember'; 9 | import RSVP from 'rsvp'; 10 | import layout from '../templates/components/async-await'; 11 | 12 | /** 13 | Used for uninitialized values so that we can distinguish them from values that 14 | were intentionally set to `null`/`undefined` in the console. 15 | 16 | @private 17 | @method UNINITIALIZED 18 | @returns undefined 19 | */ 20 | function UNINITIALIZED() {} 21 | 22 | function DEFAULT_REJECTION_HANDLER(reason) { 23 | try { 24 | let error = new Error( 25 | `Unhandled promise rejection in {{#async-await}}: ${reason}` 26 | ); 27 | error.reason = reason; 28 | throw error; 29 | } catch (error) { 30 | if (typeof Ember.onerror === 'function') { 31 | Ember.onerror(error); 32 | } else { 33 | console.assert(false, error); // eslint-disable-line no-console 34 | } 35 | } 36 | } 37 | 38 | let hashProto; 39 | 40 | if (VERSION.startsWith('2.')) { 41 | // Glimmer in older version of Ember does some weird things in creating an empty "hash", 42 | // so we have to jump through some hoops to get the correct prototype. 43 | hashProto = Object.getPrototypeOf( 44 | Ember.__loader.require('@glimmer/util').dict() 45 | ); 46 | } else { 47 | // The `hash` helper creates an object with `Object.create(null)` which will have no 48 | // prototype. 49 | hashProto = null; 50 | } 51 | 52 | function isHash(value) { 53 | return ( 54 | typeof value === 'object' && Object.getPrototypeOf(value) === hashProto 55 | ); 56 | } 57 | 58 | /** 59 | This component awaits a promise (passed as a positional param), then yields 60 | the resolved value to the given block. Thus, the code within the block can be 61 | synchronous. 62 | 63 | Optionally, pass in an inverse block to show while the promise is resolving. 64 | 65 | ``` 66 | {{#async-await this.promise as |value|}} 67 | 68 | {{else}} 69 | 70 | {{/async-await}} 71 | ``` 72 | 73 | @class component:async-await 74 | @extends Ember.Component 75 | */ 76 | export default Component.extend({ 77 | tagName: '', 78 | layout, 79 | 80 | /** 81 | The promise or hash of promises to await on (passed as a positional argument). 82 | 83 | @public 84 | @property argument 85 | @type any 86 | @required 87 | */ 88 | argument: UNINITIALIZED(), 89 | 90 | /** 91 | A callback to run when the promise rejects. By default, it calls 92 | `Ember.onerror` with an error object with its `reason` property set to the 93 | promise's rejection reason. You can pass a different function here to 94 | handle the rejection more locally. Pass `null` to silence the rejection 95 | completely. 96 | 97 | @public 98 | @property onReject 99 | @type Function | null 100 | @required 101 | */ 102 | onReject: DEFAULT_REJECTION_HANDLER, 103 | 104 | /** 105 | The most-recently awaited argument. 106 | 107 | @private 108 | @property awaited 109 | @type any 110 | */ 111 | awaited: UNINITIALIZED(), 112 | 113 | /** 114 | Whether the promise is pending, i.e. it has neither been resolved or 115 | rejected. This is the opposite of `isSettled`. Only one of `isPending`, 116 | `isResolved` or `isRejected` can be true at any given moment. 117 | 118 | @private 119 | @property isPending 120 | @type Boolean 121 | @default true 122 | */ 123 | isPending: true, 124 | 125 | /** 126 | Whether the promise is settled, i.e. it has either been resolved or 127 | rejected. This is the opposite of `isPending`. 128 | 129 | @private 130 | @property isSettled 131 | @type Boolean 132 | @default false 133 | */ 134 | isSettled: false, 135 | 136 | /** 137 | Whether the promise has been resolved. If `true`, the resolution value can 138 | be found in `resolvedValue`. Only one of `isPending`, `isResolved` or 139 | `isRejected` can be true at any given moment. 140 | 141 | @private 142 | @property isResolved 143 | @type Boolean 144 | @default false 145 | */ 146 | isResolved: false, 147 | 148 | /** 149 | Whether the promise has been rejected. If `true`, the rejection reason can 150 | be found in `rejectReason`. Only one of `isPending`, `isResolved` or 151 | `isRejected` can be true at any given moment. 152 | 153 | @private 154 | @property isRejected 155 | @type Boolean 156 | @default false 157 | */ 158 | isRejected: false, 159 | 160 | /** 161 | If the promise has been resolved, this will contain the resolved value. 162 | 163 | @private 164 | @property resolvedValue 165 | @type any 166 | */ 167 | resolvedValue: UNINITIALIZED(), 168 | 169 | /** 170 | If the promise has been resolved, this will contain the rejection reason. 171 | 172 | @private 173 | @property rejectReason 174 | @type any 175 | */ 176 | rejectReason: UNINITIALIZED(), 177 | 178 | didReceiveAttrs() { 179 | this._super(...arguments); 180 | this.didReceiveArgument(this.argument); 181 | }, 182 | 183 | didReceiveArgument(argument) { 184 | if (argument === this.awaited) { 185 | return; 186 | } 187 | 188 | this.setProperties({ 189 | awaited: argument, 190 | isPending: true, 191 | isSettled: false, 192 | isResolved: false, 193 | isRejected: false, 194 | resolvedValue: UNINITIALIZED(), 195 | rejectReason: UNINITIALIZED(), 196 | }); 197 | 198 | let target = isHash(argument) ? RSVP.hash(argument) : argument; 199 | 200 | Promise.resolve(target).then( 201 | bind(this, this.didResolve, argument), 202 | bind(this, this.didReject, argument) 203 | ); 204 | }, 205 | 206 | didResolve(resolvedArgument, value) { 207 | if (this.shouldIgnorePromise(resolvedArgument)) { 208 | return; 209 | } 210 | 211 | this.setProperties({ 212 | isPending: false, 213 | isSettled: true, 214 | isResolved: true, 215 | isRejected: false, 216 | resolvedValue: value, 217 | rejectReason: UNINITIALIZED(), 218 | }); 219 | }, 220 | 221 | didReject(rejectedArgument, reason) { 222 | if (this.shouldIgnorePromise(rejectedArgument)) { 223 | return; 224 | } 225 | 226 | this.setProperties({ 227 | isPending: false, 228 | isSettled: true, 229 | isResolved: false, 230 | isRejected: true, 231 | resolvedValue: UNINITIALIZED(), 232 | rejectReason: reason, 233 | }); 234 | 235 | let { onReject } = this; 236 | 237 | if (onReject) { 238 | onReject(reason); 239 | } 240 | }, 241 | 242 | shouldIgnorePromise(argument) { 243 | return this.isDestroyed || this.isDestroying || this.argument !== argument; 244 | }, 245 | }).reopenClass({ 246 | positionalParams: ['argument'], 247 | }); 248 | -------------------------------------------------------------------------------- /addon/templates/components/async-await.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.isResolved}} 2 | {{yield this.resolvedValue}} 3 | {{else}} 4 | {{yield to="inverse"}} 5 | {{/if}} 6 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/app/.gitkeep -------------------------------------------------------------------------------- /app/components/async-await.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-async-await-helper/components/async-await'; 2 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | useYarn: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-3.16', 12 | npm: { 13 | devDependencies: { 14 | 'ember-source': '~3.16.0', 15 | }, 16 | }, 17 | }, 18 | { 19 | name: 'ember-lts-3.20', 20 | npm: { 21 | devDependencies: { 22 | 'ember-source': '~3.20.5', 23 | }, 24 | }, 25 | }, 26 | { 27 | name: 'ember-lts-3.24', 28 | npm: { 29 | devDependencies: { 30 | 'ember-source': '~3.24.0', 31 | }, 32 | }, 33 | }, 34 | { 35 | name: 'ember-release', 36 | npm: { 37 | devDependencies: { 38 | 'ember-source': await getChannelURL('release'), 39 | }, 40 | }, 41 | }, 42 | { 43 | name: 'ember-beta', 44 | npm: { 45 | devDependencies: { 46 | 'ember-source': await getChannelURL('beta'), 47 | }, 48 | }, 49 | }, 50 | { 51 | name: 'ember-canary', 52 | npm: { 53 | devDependencies: { 54 | 'ember-source': await getChannelURL('canary'), 55 | }, 56 | }, 57 | }, 58 | { 59 | name: 'ember-default-with-jquery', 60 | env: { 61 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 62 | 'jquery-integration': true, 63 | }), 64 | }, 65 | npm: { 66 | devDependencies: { 67 | '@ember/jquery': '^1.1.0', 68 | }, 69 | }, 70 | }, 71 | { 72 | name: 'ember-classic', 73 | env: { 74 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 75 | 'application-template-wrapper': true, 76 | 'default-async-observers': false, 77 | 'template-only-glimmer-components': false, 78 | }), 79 | }, 80 | npm: { 81 | ember: { 82 | edition: 'classic', 83 | }, 84 | }, 85 | }, 86 | embroiderSafe(), 87 | embroiderOptimized(), 88 | ], 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | const { maybeEmbroider } = require('@embroider/test-setup'); 18 | return maybeEmbroider(app); 19 | }; 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-async-await-helper", 3 | "version": "1.0.0", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": "https://github.com/tildeio/ember-async-await-helper", 9 | "license": "MIT", 10 | "author": "Tilde Engineering ", 11 | "contributors": [ 12 | "Krystan HuffMenne ", 13 | "Godfrey Chan " 14 | ], 15 | "directories": { 16 | "doc": "doc", 17 | "test": "tests" 18 | }, 19 | "scripts": { 20 | "build": "ember build --environment=production", 21 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel 'lint:!(fix)'", 22 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 23 | "lint:hbs": "ember-template-lint .", 24 | "lint:hbs:fix": "ember-template-lint . --fix", 25 | "lint:js": "eslint . --cache", 26 | "lint:js:fix": "eslint . --fix", 27 | "start": "ember serve", 28 | "test": "npm-run-all lint test:*", 29 | "test:ember": "ember test", 30 | "test:ember-compatibility": "ember try:each" 31 | }, 32 | "dependencies": { 33 | "ember-cli-babel": "^7.26.3", 34 | "ember-cli-htmlbars": "^5.7.1" 35 | }, 36 | "peerDependencies": { 37 | "rsvp": ">= 1.0" 38 | }, 39 | "devDependencies": { 40 | "@ember/optional-features": "^2.0.0", 41 | "@ember/test-helpers": "^2.2.5", 42 | "@embroider/test-setup": "^0.37.0", 43 | "@glimmer/component": "^1.0.4", 44 | "@glimmer/tracking": "^1.0.4", 45 | "babel-eslint": "^10.1.0", 46 | "broccoli-asset-rev": "^3.0.0", 47 | "ember-auto-import": "^1.11.2", 48 | "ember-cli": "~3.26.1", 49 | "ember-cli-dependency-checker": "^3.2.0", 50 | "ember-cli-inject-live-reload": "^2.0.2", 51 | "ember-cli-sri": "^2.1.1", 52 | "ember-cli-terser": "^4.0.1", 53 | "ember-disable-prototype-extensions": "^1.1.3", 54 | "ember-export-application-global": "^2.0.1", 55 | "ember-load-initializers": "^2.1.2", 56 | "ember-maybe-import-regenerator": "^0.1.6", 57 | "ember-page-title": "^6.2.1", 58 | "ember-qunit": "^5.1.4", 59 | "ember-resolver": "^8.0.2", 60 | "ember-source": "~3.26.1", 61 | "ember-source-channel-url": "^3.0.0", 62 | "ember-template-lint": "^3.2.0", 63 | "ember-try": "^1.4.0", 64 | "eslint": "^7.23.0", 65 | "eslint-config-prettier": "^8.1.0", 66 | "eslint-plugin-ember": "^10.3.0", 67 | "eslint-plugin-node": "^11.1.0", 68 | "eslint-plugin-prettier": "^3.3.1", 69 | "loader.js": "^4.7.0", 70 | "npm-run-all": "^4.1.5", 71 | "prettier": "^2.2.1", 72 | "qunit": "^2.14.1", 73 | "qunit-dom": "^1.6.0" 74 | }, 75 | "engines": { 76 | "node": ">= 12" 77 | }, 78 | "ember": { 79 | "edition": "octane" 80 | }, 81 | "ember-addon": { 82 | "configPath": "tests/dummy/config" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () {}); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title "Dummy"}} 2 | 3 |

Welcome to Ember

4 | 5 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.26.1", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--yarn", 15 | "--no-welcome" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/integration/components/async-await-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, settled } from '@ember/test-helpers'; 4 | import { helper } from '@ember/component/helper'; 5 | import hbs from 'htmlbars-inline-precompile'; 6 | import Ember from 'ember'; 7 | import RSVP from 'rsvp'; 8 | 9 | module('Integration | Component | async-await', function (hooks) { 10 | setupRenderingTest(hooks); 11 | 12 | test('it does not produce a wrapper element', async function (assert) { 13 | await render(hbs`{{#async-await "unused"}}{{/async-await}}`); 14 | 15 | assert.dom('div', this.element).doesNotExist(); 16 | }); 17 | 18 | test('it can render non-promise values', async function (assert) { 19 | await render(hbs` 20 | {{#async-await "plain value" as |value|}} 21 | resolved {{value}} 22 | {{/async-await}} 23 | `); 24 | 25 | assert.dom().hasText('resolved plain value'); 26 | }); 27 | 28 | function ItBehavesLikePromises(label, Promise) { 29 | let _onerror; 30 | let expectRejection; 31 | 32 | function makePromise(label) { 33 | let resolve, reject; 34 | 35 | let promise = new Promise((res, rej) => { 36 | resolve = res; 37 | reject = rej; 38 | }); 39 | 40 | promise._label = label; 41 | 42 | return { promise, resolve, reject }; 43 | } 44 | 45 | function makeRejectedPromise(reason) { 46 | let promise = Promise.reject(reason); 47 | 48 | promise._label = `intentionally rejected (${reason})`; 49 | 50 | // This silences the browser/RSVP's "unhandled rejection" errors 51 | promise.catch(() => {}); 52 | 53 | return promise; 54 | } 55 | 56 | module(label, function (hooks) { 57 | hooks.beforeEach(function (assert) { 58 | _onerror = Ember.onerror; 59 | 60 | let failOnError = (error) => { 61 | assert.ok(false, `Unexpected error: ${error}`); 62 | }; 63 | 64 | Ember.onerror = failOnError; 65 | 66 | expectRejection = async (reason, callback) => { 67 | let called = 0; 68 | let _onerror = Ember.onerror; 69 | 70 | try { 71 | Ember.onerror = (error) => { 72 | called++; 73 | assert.ok(error instanceof Error, 'it should be an error'); 74 | assert.equal( 75 | typeof error.stack, 76 | 'string', 77 | 'it should have a stack trace' 78 | ); 79 | assert.equal( 80 | error.message, 81 | `Unhandled promise rejection in {{#async-await}}: ${reason}` 82 | ); 83 | assert.equal(error.reason, reason); 84 | }; 85 | 86 | await callback(); 87 | await settled(); 88 | } finally { 89 | assert.equal(called, 1, 'expected exactly one rejection'); 90 | Ember.onerror = _onerror; 91 | } 92 | }; 93 | }); 94 | 95 | hooks.afterEach(function () { 96 | Ember.onerror = _onerror; 97 | }); 98 | 99 | test('it can render resolved promise', async function (assert) { 100 | this.set('promise', Promise.resolve('value')); 101 | 102 | await render(hbs` 103 | {{#async-await this.promise as |value|}} 104 | resolved {{value}} 105 | {{/async-await}} 106 | `); 107 | 108 | assert.dom().hasText('resolved value'); 109 | }); 110 | 111 | test('it can take a hash of promises as arguments', async function (assert) { 112 | this.set('promiseA', Promise.resolve('valueA')); 113 | this.set('promiseB', Promise.resolve('valueB')); 114 | this.set('promiseC', Promise.resolve('valueC')); 115 | 116 | await render(hbs` 117 | {{#async-await (hash a=this.promiseA b=this.promiseB c=this.promiseC) as |h|}} 118 | resolved {{h.a}}, {{h.b}}, {{h.c}} 119 | {{/async-await}} 120 | `); 121 | 122 | assert.dom().hasText('resolved valueA, valueB, valueC'); 123 | }); 124 | 125 | test('it can take a mixed hash as arguments', async function (assert) { 126 | this.set('promiseA', Promise.resolve('valueA')); 127 | this.set('valueB', 'valueB'); 128 | this.set('promiseC', Promise.resolve('valueC')); 129 | 130 | await render(hbs` 131 | {{#async-await (hash a=this.promiseA b=this.valueB c=this.promiseC) as |h|}} 132 | resolved {{h.a}}, {{h.b}}, {{h.c}} 133 | {{/async-await}} 134 | `); 135 | 136 | assert.dom().hasText('resolved valueA, valueB, valueC'); 137 | }); 138 | 139 | test('it can take an object as the argument', async function (assert) { 140 | let obj = { 141 | toString() { 142 | return 'fancy object'; 143 | }, 144 | }; 145 | 146 | this.set('value', obj); 147 | 148 | let captured; 149 | 150 | this.owner.register( 151 | 'helper:capture', 152 | helper(function ([value]) { 153 | captured = value; 154 | return value; 155 | }) 156 | ); 157 | 158 | // Expect a straight pass-through 159 | await render(hbs` 160 | {{#async-await this.value as |v|}} 161 | resolved {{capture v}} 162 | {{/async-await}} 163 | `); 164 | 165 | assert.dom().hasText('resolved fancy object'); 166 | assert.strictEqual(captured, obj); 167 | }); 168 | 169 | test('it can render rejected promise', async function (assert) { 170 | this.set('promise', makeRejectedPromise('promise rejected')); 171 | 172 | await expectRejection('promise rejected', () => 173 | render(hbs` 174 | {{#async-await this.promise as |value|}} 175 | resolved {{value}} 176 | {{/async-await}} 177 | `) 178 | ); 179 | 180 | assert.dom().hasText(''); 181 | }); 182 | 183 | test('it can render eventually resolved promise', async function (assert) { 184 | let { promise, resolve } = makePromise(); 185 | 186 | this.set('promise', promise); 187 | 188 | await render(hbs` 189 | {{#async-await this.promise as |value|}} 190 | resolved {{value}} 191 | {{/async-await}} 192 | `); 193 | 194 | assert.dom().hasText(''); 195 | 196 | resolve('value'); 197 | await settled(); 198 | 199 | assert.dom().hasText('resolved value'); 200 | }); 201 | 202 | test('it renders the inverse block while the promise is pending', async function (assert) { 203 | let { promise, resolve } = makePromise(); 204 | 205 | this.set('promise', promise); 206 | 207 | await render(hbs` 208 | {{#async-await this.promise as |value|}} 209 | resolved {{value}} 210 | {{else}} 211 | pending... 212 | {{/async-await}} 213 | `); 214 | 215 | assert.dom().hasText('pending...'); 216 | 217 | resolve('value'); 218 | await settled(); 219 | 220 | assert.dom().doesNotContainText('pending...'); 221 | }); 222 | 223 | test('it remains in the inverse block if the promise rejects', async function (assert) { 224 | this.set('promise', makeRejectedPromise('promise rejected')); 225 | 226 | await expectRejection('promise rejected', () => 227 | render(hbs` 228 | {{#async-await this.promise as |value|}} 229 | resolved {{value}} 230 | {{else}} 231 | pending... 232 | {{/async-await}} 233 | `) 234 | ); 235 | 236 | assert.dom().hasText('pending...'); 237 | }); 238 | 239 | test('it calls Ember.onerror by default when the promise rejects', async function (assert) { 240 | let { promise, reject } = makePromise(); 241 | 242 | this.set('promise', promise); 243 | 244 | await render(hbs` 245 | {{#async-await this.promise as |value|}} 246 | resolved {{value}} 247 | {{else}} 248 | pending... 249 | {{/async-await}} 250 | `); 251 | 252 | assert.dom().hasText('pending...'); 253 | 254 | await expectRejection('promise rejected', () => { 255 | reject('promise rejected'); 256 | }); 257 | 258 | assert.dom().hasText('pending...'); 259 | }); 260 | 261 | test('it calls onReject when the promise rejects', async function (assert) { 262 | let { promise, reject } = makePromise(); 263 | 264 | this.set('promise', promise); 265 | 266 | let called = 0; 267 | 268 | this.set('onReject', (reason) => { 269 | called++; 270 | assert.equal(reason, 'promise rejected'); 271 | }); 272 | 273 | await render(hbs` 274 | {{#async-await this.promise onReject=this.onReject as |value|}} 275 | resolved {{value}} 276 | {{else}} 277 | pending... 278 | {{/async-await}} 279 | `); 280 | 281 | assert.dom().hasText('pending...'); 282 | assert.strictEqual(called, 0); 283 | 284 | reject('promise rejected'); 285 | await settled(); 286 | 287 | assert.dom().hasText('pending...'); 288 | assert.equal(called, 1); 289 | }); 290 | 291 | test('it silences the rejection when onReject is set to null', async function (assert) { 292 | let { promise, reject } = makePromise(); 293 | 294 | this.set('promise', promise); 295 | 296 | await render(hbs` 297 | {{#async-await this.promise onReject=null as |value|}} 298 | resolved {{value}} 299 | {{else}} 300 | pending... 301 | {{/async-await}} 302 | `); 303 | 304 | assert.dom().hasText('pending...'); 305 | 306 | reject('promise rejected'); 307 | await settled(); 308 | 309 | assert.dom().hasText('pending...'); 310 | }); 311 | 312 | test('it resets its state when the promise changes', async function (assert) { 313 | let { promise: first, resolve: resolveFirst } = makePromise('first'); 314 | 315 | this.set('promise', first); 316 | 317 | await render(hbs` 318 | {{#async-await this.promise as |value|}} 319 | resolved {{value}} 320 | {{else}} 321 | pending... 322 | {{/async-await}} 323 | `); 324 | 325 | assert 326 | .dom() 327 | .hasText( 328 | 'pending...', 329 | 'shows inverse block while awaiting first promise' 330 | ); 331 | 332 | resolveFirst('first'); 333 | await settled(); 334 | 335 | assert 336 | .dom() 337 | .hasText('resolved first', 'shows resolved value for first promise'); 338 | 339 | // We will resolve this later, after switching out the promise 340 | let { promise: second, resolve: resolveSecond } = makePromise('second'); 341 | 342 | this.set('promise', second); 343 | await settled(); 344 | 345 | assert 346 | .dom() 347 | .hasText( 348 | 'pending...', 349 | 'shows inverse block while awaiting second promise' 350 | ); 351 | 352 | // We will reject this later, after switching out the promise 353 | let { promise: third, reject: rejectThird } = makePromise('third'); 354 | 355 | this.set('promise', third); 356 | await settled(); 357 | 358 | assert 359 | .dom() 360 | .hasText( 361 | 'pending...', 362 | 'shows inverse block while awaiting third promise' 363 | ); 364 | 365 | this.set('promise', Promise.resolve('fourth')); 366 | await settled(); 367 | 368 | assert 369 | .dom() 370 | .hasText( 371 | 'resolved fourth', 372 | 'shows resolved value for fourth promise' 373 | ); 374 | 375 | await expectRejection('rejected fifth', () => { 376 | this.set('promise', makeRejectedPromise('rejected fifth')); 377 | }); 378 | 379 | assert 380 | .dom() 381 | .hasText( 382 | 'pending...', 383 | 'shows inverse block while awaiting fifth promise' 384 | ); 385 | 386 | // Resolving a no-longer-relevant promise should be no-op 387 | resolveSecond('second'); 388 | await settled(); 389 | 390 | assert 391 | .dom() 392 | .hasText( 393 | 'pending...', 394 | 'shows inverse block even though second promise was resolved' 395 | ); 396 | 397 | // Rejecting a no-longer-relevant promise should not error 398 | rejectThird('rejected third'); 399 | await settled(); 400 | 401 | assert 402 | .dom() 403 | .hasText( 404 | 'pending...', 405 | 'shows inverse block even though third promise was rejected' 406 | ); 407 | 408 | // Recycling an already-resolved promise is the same as Promise.resolve 409 | this.set('promise', first); 410 | await settled(); 411 | 412 | assert 413 | .dom() 414 | .hasText('resolved first', 'shows resolved value for first promise'); 415 | }); 416 | 417 | test('does nothing when the promise resolves if the component has been destroyed', async function (assert) { 418 | let { promise, resolve } = makePromise(); 419 | 420 | this.set('promise', promise); 421 | this.set('shouldShow', true); 422 | 423 | await render(hbs` 424 | {{#if this.shouldShow}} 425 | {{#async-await this.promise onReject=null as |value|}} 426 | resolved {{value}} 427 | {{else}} 428 | pending... 429 | {{/async-await}} 430 | {{/if}} 431 | `); 432 | 433 | assert.dom().hasText('pending...'); 434 | 435 | this.set('shouldShow', false); 436 | 437 | resolve('value'); 438 | await settled(); 439 | 440 | assert.dom().hasText(''); 441 | }); 442 | 443 | test('does nothing when the promise rejects if the component has been destroyed', async function (assert) { 444 | let { promise, reject } = makePromise(); 445 | 446 | this.set('promise', promise); 447 | this.set('shouldShow', true); 448 | 449 | await render(hbs` 450 | {{#if this.shouldShow}} 451 | {{#async-await this.promise onReject=null as |value|}} 452 | resolved {{value}} 453 | {{else}} 454 | pending... 455 | {{/async-await}} 456 | {{/if}} 457 | `); 458 | 459 | assert.dom().hasText('pending...'); 460 | 461 | this.set('shouldShow', false); 462 | 463 | reject('promise rejected'); 464 | await settled(); 465 | 466 | assert.dom().hasText(''); 467 | }); 468 | 469 | test('it does not rerender infinitely', async function (assert) { 470 | this.set('promise', Promise.resolve('value')); 471 | 472 | await render(hbs` 473 | {{#async-await this.promise}} 474 | {{#async-await this.promise}} 475 | {{#async-await this.promise}} 476 | {{#async-await this.promise}} 477 | {{#async-await this.promise}} 478 | {{#async-await this.promise}} 479 | {{#async-await this.promise}} 480 | {{#async-await this.promise}} 481 | {{#async-await this.promise}} 482 | {{#async-await this.promise}} 483 | {{#async-await this.promise}} 484 | {{#async-await this.promise as |value|}} 485 | resolved {{value}} 486 | {{/async-await}} 487 | {{/async-await}} 488 | {{/async-await}} 489 | {{/async-await}} 490 | {{/async-await}} 491 | {{/async-await}} 492 | {{/async-await}} 493 | {{/async-await}} 494 | {{/async-await}} 495 | {{/async-await}} 496 | {{/async-await}} 497 | {{/async-await}} 498 | `); 499 | 500 | assert.dom().hasText('resolved value'); 501 | }); 502 | 503 | module('with no Ember.onerror', function (hooks) { 504 | hooks.beforeEach(function (assert) { 505 | // NOTE: this gets reset in the outer module 506 | Ember.onerror = undefined; 507 | 508 | expectRejection = async (reason, callback) => { 509 | let called = 0; 510 | let _consoleAssert = console.assert; // eslint-disable-line no-console 511 | 512 | try { 513 | console.assert = (_boolean, error) => { 514 | // eslint-disable-line no-console 515 | called++; 516 | assert.ok(error instanceof Error, 'it should be an error'); 517 | assert.equal( 518 | typeof error.stack, 519 | 'string', 520 | 'it should have a stack trace' 521 | ); 522 | assert.equal( 523 | error.message, 524 | `Unhandled promise rejection in {{#async-await}}: ${reason}` 525 | ); 526 | assert.equal(error.reason, reason); 527 | }; 528 | 529 | await callback(); 530 | await settled(); 531 | } finally { 532 | assert.equal(called, 1, 'expected exactly one rejection'); 533 | console.assert = _consoleAssert; // eslint-disable-line no-console 534 | } 535 | }; 536 | }); 537 | 538 | test('if Ember.onerror is undefined, it console.asserts if the promise is rejecte', async function (assert) { 539 | this.set('promise', makeRejectedPromise('promise rejected')); 540 | 541 | await expectRejection('promise rejected', () => 542 | render(hbs` 543 | {{#async-await this.promise as |value|}} 544 | resolved {{value}} 545 | {{/async-await}} 546 | `) 547 | ); 548 | 549 | assert.dom().hasText(''); 550 | }); 551 | }); 552 | }); 553 | } 554 | 555 | ItBehavesLikePromises('native Promise', Promise); 556 | ItBehavesLikePromises('RSVP Promise', RSVP.Promise); 557 | }); 558 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/tests/unit/.gitkeep -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildeio/ember-async-await-helper/340fe2befdfbc1b2b5384fd3de8f18264aea8e45/vendor/.gitkeep --------------------------------------------------------------------------------