report poll saving errors using a modal (#729)

This commit is contained in:
Jeldrik Hanschke 2023-11-05 12:19:47 +01:00 committed by GitHub
parent 0c4ef6fc5b
commit 88a51964f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 464 additions and 33 deletions

View file

@ -1,11 +1,22 @@
<BsButton <BsButton
@type="primary" @type="primary"
{{!
Due to a bug in Ember, conditional modifiers cannot be used with the "on"
modifier. Need to always apply the modifier and fallback to a noop function
(returned by noop helper) instead of only applying the modifier when needed.
See https://github.com/emberjs/ember.js/issues/19869.
}}
{{on "click" (if @onClick @onClick (noop))}}
class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next" class="cr-steps-bottom-nav__button cr-steps-bottom-nav__next-button next"
type="submit" type="submit"
...attributes ...attributes
> >
<span class="cr-steps-bottom-nav__label"> <span class="cr-steps-bottom-nav__label">
{{t "action.save"}} {{#if (has-block)}}
{{yield}}
{{else}}
{{t "action.save"}}
{{/if}}
</span> </span>
{{#if @isPending}} {{#if @isPending}}

View file

@ -4,8 +4,12 @@ interface SaveButtonSignature {
Args: { Args: {
Named: { Named: {
isPending: boolean; isPending: boolean;
onClick?: () => void;
}; };
}; };
Blocks: {
default: [];
};
Element: HTMLButtonElement; Element: HTMLButtonElement;
} }

View file

@ -9,6 +9,7 @@ import type RouterService from '@ember/routing/router-service';
import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings'; import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings';
import type IntlService from 'ember-intl/services/intl'; import type IntlService from 'ember-intl/services/intl';
import type FlashMessagesService from 'ember-cli-flash/services/flash-messages'; import type FlashMessagesService from 'ember-cli-flash/services/flash-messages';
import { tracked } from '@glimmer/tracking';
export default class CreateSettings extends Controller { export default class CreateSettings extends Controller {
@service declare flashMessages: FlashMessagesService; @service declare flashMessages: FlashMessagesService;
@ -17,6 +18,8 @@ export default class CreateSettings extends Controller {
declare model: CreateSettingsRouteModel; declare model: CreateSettingsRouteModel;
@tracked savingPollFailed = false;
get anonymousUser() { get anonymousUser() {
return this.model.anonymousUser; return this.model.anonymousUser;
} }
@ -94,7 +97,7 @@ export default class CreateSettings extends Controller {
} }
@action @action
async submit() { async createPoll() {
const { model } = this; const { model } = this;
const { const {
anonymousUser, anonymousUser,
@ -158,15 +161,20 @@ export default class CreateSettings extends Controller {
); );
// redirect to new poll // redirect to new poll
await this.router.transitionTo('poll', poll.id, { await this.router.transitionTo('poll.participation', poll.id, {
queryParams: { queryParams: {
encryptionKey, encryptionKey,
}, },
}); });
} catch (err) { } catch (err) {
this.flashMessages.danger('error.poll.savingFailed'); this.savingPollFailed = true;
throw err; reportError(err);
} }
} }
@action
resetSavingPollFailedState() {
this.savingPollFailed = false;
}
} }

13
app/helpers/noop.ts Normal file
View file

@ -0,0 +1,13 @@
import { helper } from '@ember/component/helper';
const noop = helper(() => {
return () => {};
});
export default noop;
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
noop: typeof noop;
}
}

View file

@ -3,7 +3,7 @@
@formLayout="horizontal" @formLayout="horizontal"
@model={{this}} @model={{this}}
@onInvalid={{(scroll-first-invalid-element-into-view-port)}} @onInvalid={{(scroll-first-invalid-element-into-view-port)}}
@onSubmit={{this.submit}} @onSubmit={{this.createPoll}}
novalidate novalidate
as |form| as |form|
> >
@ -78,5 +78,36 @@
<BackButton @onClick={{this.previousPage}} /> <BackButton @onClick={{this.previousPage}} />
</div> </div>
</div> </div>
<BsModal
@onHidden={{this.resetSavingPollFailedState}}
@onSubmit={{form.submit}}
@open={{this.savingPollFailed}}
data-test-modal="saving-failed"
as |modal|
>
<modal.header
@closeButton={{false}}
@title={{t "error.poll.savingFailed.title"}}
/>
<modal.body>
<p>
{{t "error.poll.savingFailed.description"}}
</p>
</modal.body>
<modal.footer>
<BsButton @onClick={{modal.close}} data-test-button="abort">
{{t "action.abort"}}
</BsButton>
<SaveButton
@isPending={{form.isSubmitting}}
@onClick={{modal.submit}}
data-test-button="retry"
type="button"
>
{{t "modal.save-retry.button-retry"}}
</SaveButton>
</modal.footer>
</BsModal>
</BsForm> </BsForm>
</div> </div>

185
package-lock.json generated
View file

@ -62,6 +62,7 @@
"ember-power-calendar-luxon": "^0.5.0", "ember-power-calendar-luxon": "^0.5.0",
"ember-qunit": "^7.0.0", "ember-qunit": "^7.0.0",
"ember-resolver": "^11.0.1", "ember-resolver": "^11.0.1",
"ember-sinon-qunit": "^7.4.0",
"ember-source": "~5.4.0", "ember-source": "~5.4.0",
"ember-template-lint": "^5.11.2", "ember-template-lint": "^5.11.2",
"ember-test-selectors": "^6.0.0", "ember-test-selectors": "^6.0.0",
@ -82,6 +83,7 @@
"qunit-dom": "^3.0.0", "qunit-dom": "^3.0.0",
"release-it": "^16.0.0", "release-it": "^16.0.0",
"sass": "^1.19.0", "sass": "^1.19.0",
"sinon": "^17.0.1",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
"stylelint": "^15.10.3", "stylelint": "^15.10.3",
"stylelint-config-standard": "^34.0.0", "stylelint-config-standard": "^34.0.0",
@ -9679,6 +9681,50 @@
"url": "https://github.com/sindresorhus/is?sponsor=1" "url": "https://github.com/sindresorhus/is?sponsor=1"
} }
}, },
"node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "11.2.2",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@sinonjs/samsam": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"lodash.get": "^4.4.2",
"type-detect": "^4.0.8"
}
},
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/@sinonjs/text-encoding": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
"dev": true
},
"node_modules/@socket.io/component-emitter": { "node_modules/@socket.io/component-emitter": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
@ -10214,6 +10260,21 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/sinon": {
"version": "10.0.20",
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.20.tgz",
"integrity": "sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==",
"dev": true,
"dependencies": {
"@types/sinonjs__fake-timers": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.4.tgz",
"integrity": "sha512-GDV68H0mBSN449sa5HEj51E0wfpVQb8xNSMzxf/PrypMFcLTMwJMOM/cgXiv71Mq5drkOQmUGvL1okOZcu6RrQ==",
"dev": true
},
"node_modules/@types/sizzle": { "node_modules/@types/sizzle": {
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz",
@ -38972,6 +39033,21 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/ember-sinon-qunit": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ember-sinon-qunit/-/ember-sinon-qunit-7.4.0.tgz",
"integrity": "sha512-BcH2scgJ4Vpq5Fnjeq5Z2ESnHLsmcfFRaq/gOujy3my+8w7WTtrHyaUgWzmd5mLw+tfCYssAUEalQhk1ZNpV+g==",
"dev": true,
"dependencies": {
"@embroider/addon-shim": "^1.8.6",
"@types/sinon": "^10.0.19"
},
"peerDependencies": {
"ember-source": ">=3.28.0",
"qunit": "^2.0.0",
"sinon": "^15.0.3 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/ember-source": { "node_modules/ember-source": {
"version": "5.4.0", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/ember-source/-/ember-source-5.4.0.tgz", "resolved": "https://registry.npmjs.org/ember-source/-/ember-source-5.4.0.tgz",
@ -46825,6 +46901,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/just-extend": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
"dev": true
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -48842,6 +48924,61 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true "dev": true
}, },
"node_modules/nise": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
"integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^2.0.0",
"@sinonjs/fake-timers": "^10.0.2",
"@sinonjs/text-encoding": "^0.7.1",
"just-extend": "^4.0.2",
"path-to-regexp": "^1.7.0"
}
},
"node_modules/nise/node_modules/@sinonjs/commons": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
"integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
"integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
"dev": true,
"dependencies": {
"type-detect": "4.0.8"
}
},
"node_modules/nise/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"dev": true
},
"node_modules/nise/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dev": true,
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/no-case": { "node_modules/no-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
@ -52764,6 +52901,45 @@
"integrity": "sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==", "integrity": "sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==",
"dev": true "dev": true
}, },
"node_modules/sinon": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
"dev": true,
"dependencies": {
"@sinonjs/commons": "^3.0.0",
"@sinonjs/fake-timers": "^11.2.2",
"@sinonjs/samsam": "^8.0.0",
"diff": "^5.1.0",
"nise": "^5.1.5",
"supports-color": "^7.2.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/sinon"
}
},
"node_modules/sinon/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/sinon/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/sjcl": { "node_modules/sjcl": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz",
@ -55552,6 +55728,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",

View file

@ -81,6 +81,7 @@
"ember-power-calendar-luxon": "^0.5.0", "ember-power-calendar-luxon": "^0.5.0",
"ember-qunit": "^7.0.0", "ember-qunit": "^7.0.0",
"ember-resolver": "^11.0.1", "ember-resolver": "^11.0.1",
"ember-sinon-qunit": "^7.4.0",
"ember-source": "~5.4.0", "ember-source": "~5.4.0",
"ember-template-lint": "^5.11.2", "ember-template-lint": "^5.11.2",
"ember-test-selectors": "^6.0.0", "ember-test-selectors": "^6.0.0",
@ -101,6 +102,7 @@
"qunit-dom": "^3.0.0", "qunit-dom": "^3.0.0",
"release-it": "^16.0.0", "release-it": "^16.0.0",
"sass": "^1.19.0", "sass": "^1.19.0",
"sinon": "^17.0.1",
"sjcl": "^1.0.8", "sjcl": "^1.0.8",
"stylelint": "^15.10.3", "stylelint": "^15.10.3",
"stylelint-config-standard": "^34.0.0", "stylelint-config-standard": "^34.0.0",

View file

@ -26,6 +26,7 @@ import pageCreateSettings from 'croodle/tests/pages/create/settings';
import pagePollParticipation from 'croodle/tests/pages/poll/participation'; import pagePollParticipation from 'croodle/tests/pages/poll/participation';
import asyncThrowsAssertion from '../assertions/async-throws'; import asyncThrowsAssertion from '../assertions/async-throws';
import { calendarSelect } from 'ember-power-calendar/test-support'; import { calendarSelect } from 'ember-power-calendar/test-support';
import sinon from 'sinon';
module('Acceptance | create a poll', function (hooks) { module('Acceptance | create a poll', function (hooks) {
hooks.beforeEach(function () { hooks.beforeEach(function () {
@ -141,25 +142,16 @@ module('Acceptance | create a poll', function (hooks) {
'available answers selection has autofocus', 'available answers selection has autofocus',
); );
// simulate temporary server error
this.server.logging = true;
this.server.post('/polls', undefined, 503);
await assert.asyncThrows(async () => {
await pageCreateSettings.save();
}, 'Unexpected server-side error. Server responded with 503 (Service Unavailable)');
assert.strictEqual(currentRouteName(), 'create.settings');
// simulate server is available again // simulate server is available again
// defer creation for testing loading spinner // defer creation for testing loading spinner
let resolveSubmission; let resolveSubmission;
let resolveSubmissionWith;
this.server.post('/polls', function (schema) { this.server.post('/polls', function (schema) {
return new Promise((resolve) => { return new Promise((resolve) => {
let attrs = this.normalizedRequestAttrs(); resolveSubmission = () => {
const attrs = this.normalizedRequestAttrs();
resolveSubmission = resolve; const poll = schema.polls.create(attrs);
resolveSubmissionWith = schema.polls.create(attrs); resolve(poll);
};
}); });
}); });
@ -171,7 +163,7 @@ module('Acceptance | create a poll', function (hooks) {
}); });
assert.ok(true, 'loading spinner is shown'); assert.ok(true, 'loading spinner is shown');
resolveSubmission(resolveSubmissionWith); resolveSubmission();
await settled(); await settled();
assert.strictEqual(currentRouteName(), 'poll.participation'); assert.strictEqual(currentRouteName(), 'poll.participation');
@ -958,6 +950,170 @@ module('Acceptance | create a poll', function (hooks) {
assert.strictEqual(currentRouteName(), 'create.index'); assert.strictEqual(currentRouteName(), 'create.index');
}); });
test('informs user if saving fails', async function (assert) {
const reportErrorFake = sinon.replace(window, 'reportError', sinon.fake());
await pageCreateIndex.visit();
assert.strictEqual(
currentRouteName(),
'create.index',
'assumption: can open start page of poll creation',
);
await pageCreateIndex.next();
assert.strictEqual(
currentRouteName(),
'create.meta',
'assumption: can go to title and description input step',
);
await pageCreateMeta.title('foo').next();
assert.strictEqual(
currentRouteName(),
'create.options',
'assumption: can go to options input step',
);
await pageCreateOptions.selectDates([new Date()]);
await pageCreateOptions.next();
assert.strictEqual(
currentRouteName(),
'create.options-datetime',
'assumption: can go to times input for dates after selecting one day',
);
await pageCreateOptionsDatetime.next();
assert.strictEqual(
currentRouteName(),
'create.settings',
'assumption: can go to settings page',
);
// simulate temporary server error
this.server.logging = true;
this.server.post('/polls', undefined, 503);
await click('form button[type="submit"]');
assert.strictEqual(
currentRouteName(),
'create.settings',
'user stays at settings route if saving fails',
);
assert
.dom('[data-test-modal="saving-failed"]')
.isVisible(
'modal is shown informing the user that saving the poll failed',
);
assert
.dom('[data-test-modal="saving-failed"] .modal-header')
.hasText(
t('error.poll.savingFailed.title'),
'modal has a meaningful title',
);
assert
.dom('[data-test-modal="saving-failed"] .modal-body')
.hasText(
t('error.poll.savingFailed.description'),
'modal has a meaningful body',
);
assert
.dom('[data-test-modal="saving-failed"] .modal-footer button')
.exists({ count: 2 }, 'modal has two buttons');
assert
.dom(
'[data-test-modal="saving-failed"] .modal-footer button[data-test-button="abort"]',
)
.hasText(t('action.abort'), 'abort button has meaningful text');
assert
.dom(
'[data-test-modal="saving-failed"] .modal-footer button[data-test-button="retry"]',
)
.hasText(
t('modal.save-retry.button-retry'),
'retry button has meaningful text',
);
assert.ok(reportErrorFake.calledOnce, 'error is reported to console');
assert.ok(
reportErrorFake.firstCall.args[0] instanceof Error,
'reported error is an instance of Error',
);
assert.strictEqual(
reportErrorFake.firstCall.args[0].message,
'Unexpected server-side error. Server responded with 503 (Service Unavailable)',
'reported error has meaningful error message',
);
await click(
'[data-test-modal="saving-failed"] button[data-test-button="retry"]',
);
assert.strictEqual(
currentRouteName(),
'create.settings',
'user stays at settings route if saving failed even on retry',
);
assert
.dom('[data-test-modal="saving-failed"]')
.isVisible('modal is still shown if retry fails');
assert.ok(
reportErrorFake.calledTwice,
'error is reported to console on failed retry',
);
await click(
'[data-test-modal="saving-failed"] button[data-test-button="abort"]',
);
assert
.dom('[data-test-modal="saving-failed"]')
.isNotVisible('user can close the modal that saving failed');
assert.strictEqual(
currentRouteName(),
'create.settings',
'user stays at settings route if closing the modal',
);
await click('form button[type="submit"]');
assert
.dom('[data-test-modal="saving-failed"]')
.isVisible('modal is visible again if saving fails again');
assert.ok(
reportErrorFake.calledThrice,
'error is reported to console on failed retry',
);
// simulate server is available again
// defer creation for testing loading spinner
let resolveSubmission;
this.server.post('/polls', function (schema) {
return new Promise((resolve) => {
resolveSubmission = () => {
const attrs = this.normalizedRequestAttrs();
const poll = schema.polls.create(attrs);
resolve(poll);
};
});
});
click('[data-test-modal="saving-failed"] button[data-test-button="retry"]');
// shows loading spinner while saving
await waitFor(
'[data-test-modal="saving-failed"] button[data-test-button="retry"] .spinner-border',
{
timeoutMessage: 'timeout while waiting for loading spinner to appear',
},
);
assert.ok(true, 'loading spinner is shown');
resolveSubmission();
await settled();
assert.strictEqual(
currentRouteName(),
'poll.participation',
'user is transitioned to poll participation page after successful retry',
);
});
module('validation', function () { module('validation', function () {
test('validates user input when creating a poll with dates and times', async function (assert) { test('validates user input when creating a poll with dates and times', async function (assert) {
const day = DateTime.now(); const day = DateTime.now();

View file

@ -4,6 +4,7 @@ import * as QUnit from 'qunit';
import { setApplication } from '@ember/test-helpers'; import { setApplication } from '@ember/test-helpers';
import { setup } from 'qunit-dom'; import { setup } from 'qunit-dom';
import { start } from 'ember-qunit'; import { start } from 'ember-qunit';
import setupSinon from 'ember-sinon-qunit';
document.addEventListener( document.addEventListener(
'securitypolicyviolation', 'securitypolicyviolation',
@ -20,4 +21,6 @@ setApplication(Application.create(config.APP));
setup(QUnit.assert); setup(QUnit.assert);
setupSinon();
start(); start();

View file

@ -107,8 +107,10 @@ error:
expired: 'El sondeig ha caducat i ha estat eliminat.' expired: 'El sondeig ha caducat i ha estat eliminat.'
typo: 'Hi ha un error tipogràfic en la URL. Pots tornar a comprovar typo: 'Hi ha un error tipogràfic en la URL. Pots tornar a comprovar
el que has posat, especialment la part anterior al signe d''interrogació.' el que has posat, especialment la part anterior al signe d''interrogació.'
savingFailed: 'No hem pogut guardar el sondeig, si us plau, intenta''l de savingFailed:
nou en uns segons.' title: 'Saving failed'
description: 'No hem pogut guardar el sondeig, si us plau, intenta''l de
nou en uns segons.'
generic: generic:
unexpected: unexpected:
title: 'Va ocórrer un error inesperat' title: 'Va ocórrer un error inesperat'

View file

@ -112,8 +112,11 @@ error:
typo: 'Die URL ist fehlerhaft. Bitte prüfe, dass die URL vollständig typo: 'Die URL ist fehlerhaft. Bitte prüfe, dass die URL vollständig
und korrekt ist. Achte dabei insbesondere auf den Teil vor dem und korrekt ist. Achte dabei insbesondere auf den Teil vor dem
Fragezeichen.' Fragezeichen.'
savingFailed: 'Die Umfrage konnte nicht gespeichert werden. Bitte versuche savingFailed:
es in einigen Sekunden erneut.' title: 'Speichern fehlgeschlagen'
description: 'Die Umfrage konnte nicht gespeichert werden. Bitte prüfe deine
Internetverbindung und versuche es erneut. Sollte der Fehler weiterhin
auftreten, wende dich bitte an den Administrator der Seite.'
generic: generic:
unexpected: unexpected:
title: 'Ein unerwarteter Fehler ist aufgetreten' title: 'Ein unerwarteter Fehler ist aufgetreten'

View file

@ -107,7 +107,10 @@ error:
expired: 'The poll is expired and has been deleted.' expired: 'The poll is expired and has been deleted.'
typo: 'There is a typo in the URL. You may want to double-check it typo: 'There is a typo in the URL. You may want to double-check it
especially the part before the question mark.' especially the part before the question mark.'
savingFailed: 'The poll could not be saved. Please try again in a few seconds.' savingFailed:
title: 'Saving failed'
description: 'The poll could not be saved. Please check your network connection
and try again. Please contact the site administrator if the problem persists.'
generic: generic:
unexpected: unexpected:
title: 'An unexpected error occured' title: 'An unexpected error occured'

View file

@ -109,8 +109,10 @@ error:
expired: 'La encuesta ha expirado y ha sido borrada.' expired: 'La encuesta ha expirado y ha sido borrada.'
typo: 'Hay un error al teclear la URL. Por favor, comprueba que la typo: 'Hay un error al teclear la URL. Por favor, comprueba que la
has escrito bien, sobre todo la parte anterior al signo de interrogación.' has escrito bien, sobre todo la parte anterior al signo de interrogación.'
savingFailed: 'La encuesta no pude ser guardada. Por favor prueba de nuevo savingFailed:
en unos segundos.' title: 'Saving failed'
description: 'La encuesta no pude ser guardada. Por favor prueba de nuevo
en unos segundos.'
generic: generic:
unexpected: unexpected:
title: 'Ocurrió un error inesperado' title: 'Ocurrió un error inesperado'

View file

@ -108,8 +108,10 @@ error:
expired: 'Le sondage a expiré et a été supprimé.' expired: 'Le sondage a expiré et a été supprimé.'
typo: "Il y a une faute de frappe dans l'URL. Vous voudrez peut-être\ typo: "Il y a une faute de frappe dans l'URL. Vous voudrez peut-être\
\ revérifier - en particulier la partie avant le point d'interrogation." \ revérifier - en particulier la partie avant le point d'interrogation."
savingFailed: "Le sondage n'a pas pu être enregistré. Veuillez réessayer dans\ savingFailed:
\ quelques secondes." title: "Saving failed"
description: "Le sondage n'a pas pu être enregistré. Veuillez réessayer dans\
\ quelques secondes."
generic: generic:
unexpected: unexpected:
title: "Une erreur inattendue s'est produite" title: "Une erreur inattendue s'est produite"

View file

@ -108,8 +108,10 @@ error:
expired: 'Il sondaggio è scaduto ed è stato rimosso.' expired: 'Il sondaggio è scaduto ed è stato rimosso.'
typo: 'L''URL è errato. Prova a ricontrollarlo, specialmente la parte typo: 'L''URL è errato. Prova a ricontrollarlo, specialmente la parte
prima del punto interrogativo.' prima del punto interrogativo.'
savingFailed: 'Non è stato possible salvare il sondaggio. Riprova tra qualche savingFailed:
secondo.' title: "Saving failed"
description: 'Non è stato possible salvare il sondaggio. Riprova tra qualche
secondo.'
generic: generic:
unexpected: unexpected:
title: 'Si è verificato un errore inaspettato' title: 'Si è verificato un errore inaspettato'

View file

@ -87,7 +87,9 @@ error:
delen før spørsmålstegnet. delen før spørsmålstegnet.
expired: Avstemmingen er utløpt, og har blitt slettet. expired: Avstemmingen er utløpt, og har blitt slettet.
title: Kunne ikke finne avstemming title: Kunne ikke finne avstemming
savingFailed: Kunne ikke lagre avstemming. Prøv igjen om noen sekunder. savingFailed:
title: Saving failed
description: Kunne ikke lagre avstemming. Prøv igjen om noen sekunder.
decryptionFailed: decryptionFailed:
description: Dekryptering av avstemmingsdata mislyktes. Dette skjer antageligvis description: Dekryptering av avstemmingsdata mislyktes. Dette skjer antageligvis
fordi krypteringsnøkkelen ikke er riktig. Dobbeltsjekk nettadressen fordi krypteringsnøkkelen ikke er riktig. Dobbeltsjekk nettadressen

View file

@ -15,6 +15,7 @@ type BsFormComponent = ComponentLike<{
{ {
element: BsFormElementComponent; element: BsFormElementComponent;
isSubmitting: boolean; isSubmitting: boolean;
submit: () => void;
}, },
]; ];
}; };

View file

@ -9,6 +9,7 @@ declare module '@glint/environment-ember-loose/registry' {
closeButton?: boolean; closeButton?: boolean;
footer?: boolean; footer?: boolean;
keyboard?: boolean; keyboard?: boolean;
onHide?: () => void;
onHidden?: () => void; onHidden?: () => void;
onSubmit?: () => void; onSubmit?: () => void;
open: boolean; open: boolean;