From 88a51964f18a3975438e8dcd7ec3691e9e5a5d63 Mon Sep 17 00:00:00 2001 From: Jeldrik Hanschke Date: Sun, 5 Nov 2023 12:19:47 +0100 Subject: [PATCH] report poll saving errors using a modal (#729) --- app/components/save-button.hbs | 13 +- app/components/save-button.ts | 4 + app/controllers/create/settings.ts | 16 +- app/helpers/noop.ts | 13 ++ app/templates/create/settings.hbs | 33 +++- package-lock.json | 185 +++++++++++++++++ package.json | 2 + tests/acceptance/create-a-poll-test.js | 186 ++++++++++++++++-- tests/test-helper.ts | 3 + translations/ca.yml | 6 +- translations/de.yml | 7 +- translations/en.yml | 5 +- translations/es.yml | 6 +- translations/fr.yml | 6 +- translations/it.yml | 6 +- translations/nb.yml | 4 +- types/ember-bootstrap/components/bs-form.d.ts | 1 + .../ember-bootstrap/components/bs-modal.d.ts | 1 + 18 files changed, 464 insertions(+), 33 deletions(-) create mode 100644 app/helpers/noop.ts diff --git a/app/components/save-button.hbs b/app/components/save-button.hbs index d013514..1377135 100644 --- a/app/components/save-button.hbs +++ b/app/components/save-button.hbs @@ -1,11 +1,22 @@ - {{t "action.save"}} + {{#if (has-block)}} + {{yield}} + {{else}} + {{t "action.save"}} + {{/if}} {{#if @isPending}} diff --git a/app/components/save-button.ts b/app/components/save-button.ts index d459250..e922824 100644 --- a/app/components/save-button.ts +++ b/app/components/save-button.ts @@ -4,8 +4,12 @@ interface SaveButtonSignature { Args: { Named: { isPending: boolean; + onClick?: () => void; }; }; + Blocks: { + default: []; + }; Element: HTMLButtonElement; } diff --git a/app/controllers/create/settings.ts b/app/controllers/create/settings.ts index eaee98f..fb17af0 100644 --- a/app/controllers/create/settings.ts +++ b/app/controllers/create/settings.ts @@ -9,6 +9,7 @@ import type RouterService from '@ember/routing/router-service'; import type { CreateSettingsRouteModel } from 'croodle/routes/create/settings'; import type IntlService from 'ember-intl/services/intl'; import type FlashMessagesService from 'ember-cli-flash/services/flash-messages'; +import { tracked } from '@glimmer/tracking'; export default class CreateSettings extends Controller { @service declare flashMessages: FlashMessagesService; @@ -17,6 +18,8 @@ export default class CreateSettings extends Controller { declare model: CreateSettingsRouteModel; + @tracked savingPollFailed = false; + get anonymousUser() { return this.model.anonymousUser; } @@ -94,7 +97,7 @@ export default class CreateSettings extends Controller { } @action - async submit() { + async createPoll() { const { model } = this; const { anonymousUser, @@ -158,15 +161,20 @@ export default class CreateSettings extends Controller { ); // redirect to new poll - await this.router.transitionTo('poll', poll.id, { + await this.router.transitionTo('poll.participation', poll.id, { queryParams: { encryptionKey, }, }); } catch (err) { - this.flashMessages.danger('error.poll.savingFailed'); + this.savingPollFailed = true; - throw err; + reportError(err); } } + + @action + resetSavingPollFailedState() { + this.savingPollFailed = false; + } } diff --git a/app/helpers/noop.ts b/app/helpers/noop.ts new file mode 100644 index 0000000..a648482 --- /dev/null +++ b/app/helpers/noop.ts @@ -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; + } +} diff --git a/app/templates/create/settings.hbs b/app/templates/create/settings.hbs index bb253b5..e458468 100644 --- a/app/templates/create/settings.hbs +++ b/app/templates/create/settings.hbs @@ -3,7 +3,7 @@ @formLayout="horizontal" @model={{this}} @onInvalid={{(scroll-first-invalid-element-into-view-port)}} - @onSubmit={{this.submit}} + @onSubmit={{this.createPoll}} novalidate as |form| > @@ -78,5 +78,36 @@ + + + + +

+ {{t "error.poll.savingFailed.description"}} +

+
+ + + {{t "action.abort"}} + + + {{t "modal.save-retry.button-retry"}} + + +
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5d598d4..e56a3cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "ember-power-calendar-luxon": "^0.5.0", "ember-qunit": "^7.0.0", "ember-resolver": "^11.0.1", + "ember-sinon-qunit": "^7.4.0", "ember-source": "~5.4.0", "ember-template-lint": "^5.11.2", "ember-test-selectors": "^6.0.0", @@ -82,6 +83,7 @@ "qunit-dom": "^3.0.0", "release-it": "^16.0.0", "sass": "^1.19.0", + "sinon": "^17.0.1", "sjcl": "^1.0.8", "stylelint": "^15.10.3", "stylelint-config-standard": "^34.0.0", @@ -9679,6 +9681,50 @@ "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -10214,6 +10260,21 @@ "@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": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.5.tgz", @@ -38972,6 +39033,21 @@ "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": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/ember-source/-/ember-source-5.4.0.tgz", @@ -46825,6 +46901,12 @@ "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": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -48842,6 +48924,61 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "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": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -52764,6 +52901,45 @@ "integrity": "sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==", "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/sjcl/-/sjcl-1.0.8.tgz", @@ -55552,6 +55728,15 @@ "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": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", diff --git a/package.json b/package.json index 517aad6..0637f70 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "ember-power-calendar-luxon": "^0.5.0", "ember-qunit": "^7.0.0", "ember-resolver": "^11.0.1", + "ember-sinon-qunit": "^7.4.0", "ember-source": "~5.4.0", "ember-template-lint": "^5.11.2", "ember-test-selectors": "^6.0.0", @@ -101,6 +102,7 @@ "qunit-dom": "^3.0.0", "release-it": "^16.0.0", "sass": "^1.19.0", + "sinon": "^17.0.1", "sjcl": "^1.0.8", "stylelint": "^15.10.3", "stylelint-config-standard": "^34.0.0", diff --git a/tests/acceptance/create-a-poll-test.js b/tests/acceptance/create-a-poll-test.js index 06ca9e6..73c6b89 100644 --- a/tests/acceptance/create-a-poll-test.js +++ b/tests/acceptance/create-a-poll-test.js @@ -26,6 +26,7 @@ import pageCreateSettings from 'croodle/tests/pages/create/settings'; import pagePollParticipation from 'croodle/tests/pages/poll/participation'; import asyncThrowsAssertion from '../assertions/async-throws'; import { calendarSelect } from 'ember-power-calendar/test-support'; +import sinon from 'sinon'; module('Acceptance | create a poll', function (hooks) { hooks.beforeEach(function () { @@ -141,25 +142,16 @@ module('Acceptance | create a poll', function (hooks) { '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 // defer creation for testing loading spinner let resolveSubmission; - let resolveSubmissionWith; this.server.post('/polls', function (schema) { return new Promise((resolve) => { - let attrs = this.normalizedRequestAttrs(); - - resolveSubmission = resolve; - resolveSubmissionWith = schema.polls.create(attrs); + resolveSubmission = () => { + const attrs = this.normalizedRequestAttrs(); + const poll = schema.polls.create(attrs); + resolve(poll); + }; }); }); @@ -171,7 +163,7 @@ module('Acceptance | create a poll', function (hooks) { }); assert.ok(true, 'loading spinner is shown'); - resolveSubmission(resolveSubmissionWith); + resolveSubmission(); await settled(); assert.strictEqual(currentRouteName(), 'poll.participation'); @@ -958,6 +950,170 @@ module('Acceptance | create a poll', function (hooks) { 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 () { test('validates user input when creating a poll with dates and times', async function (assert) { const day = DateTime.now(); diff --git a/tests/test-helper.ts b/tests/test-helper.ts index a259d25..b979826 100644 --- a/tests/test-helper.ts +++ b/tests/test-helper.ts @@ -4,6 +4,7 @@ import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; import { start } from 'ember-qunit'; +import setupSinon from 'ember-sinon-qunit'; document.addEventListener( 'securitypolicyviolation', @@ -20,4 +21,6 @@ setApplication(Application.create(config.APP)); setup(QUnit.assert); +setupSinon(); + start(); diff --git a/translations/ca.yml b/translations/ca.yml index 22d36e1..a96579c 100644 --- a/translations/ca.yml +++ b/translations/ca.yml @@ -107,8 +107,10 @@ error: expired: 'El sondeig ha caducat i ha estat eliminat.' 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ó.' - savingFailed: 'No hem pogut guardar el sondeig, si us plau, intenta''l de - nou en uns segons.' + savingFailed: + title: 'Saving failed' + description: 'No hem pogut guardar el sondeig, si us plau, intenta''l de + nou en uns segons.' generic: unexpected: title: 'Va ocórrer un error inesperat' diff --git a/translations/de.yml b/translations/de.yml index 6d4bb19..d848b66 100644 --- a/translations/de.yml +++ b/translations/de.yml @@ -112,8 +112,11 @@ error: typo: 'Die URL ist fehlerhaft. Bitte prüfe, dass die URL vollständig und korrekt ist. Achte dabei insbesondere auf den Teil vor dem Fragezeichen.' - savingFailed: 'Die Umfrage konnte nicht gespeichert werden. Bitte versuche - es in einigen Sekunden erneut.' + savingFailed: + 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: unexpected: title: 'Ein unerwarteter Fehler ist aufgetreten' diff --git a/translations/en.yml b/translations/en.yml index e8cf862..36ccd2f 100644 --- a/translations/en.yml +++ b/translations/en.yml @@ -107,7 +107,10 @@ error: expired: 'The poll is expired and has been deleted.' typo: 'There is a typo in the URL. You may want to double-check it – 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: unexpected: title: 'An unexpected error occured' diff --git a/translations/es.yml b/translations/es.yml index 7bbbd0e..dd0187b 100644 --- a/translations/es.yml +++ b/translations/es.yml @@ -109,8 +109,10 @@ error: expired: 'La encuesta ha expirado y ha sido borrada.' 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.' - savingFailed: 'La encuesta no pude ser guardada. Por favor prueba de nuevo - en unos segundos.' + savingFailed: + title: 'Saving failed' + description: 'La encuesta no pude ser guardada. Por favor prueba de nuevo + en unos segundos.' generic: unexpected: title: 'Ocurrió un error inesperado' diff --git a/translations/fr.yml b/translations/fr.yml index 5f8a36d..35c598c 100644 --- a/translations/fr.yml +++ b/translations/fr.yml @@ -108,8 +108,10 @@ error: expired: 'Le sondage a expiré et a été supprimé.' 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." - savingFailed: "Le sondage n'a pas pu être enregistré. Veuillez réessayer dans\ - \ quelques secondes." + savingFailed: + title: "Saving failed" + description: "Le sondage n'a pas pu être enregistré. Veuillez réessayer dans\ + \ quelques secondes." generic: unexpected: title: "Une erreur inattendue s'est produite" diff --git a/translations/it.yml b/translations/it.yml index aa92fdd..eca4203 100644 --- a/translations/it.yml +++ b/translations/it.yml @@ -108,8 +108,10 @@ error: expired: 'Il sondaggio è scaduto ed è stato rimosso.' typo: 'L''URL è errato. Prova a ricontrollarlo, specialmente la parte prima del punto interrogativo.' - savingFailed: 'Non è stato possible salvare il sondaggio. Riprova tra qualche - secondo.' + savingFailed: + title: "Saving failed" + description: 'Non è stato possible salvare il sondaggio. Riprova tra qualche + secondo.' generic: unexpected: title: 'Si è verificato un errore inaspettato' diff --git a/translations/nb.yml b/translations/nb.yml index cb2816d..8ac8709 100644 --- a/translations/nb.yml +++ b/translations/nb.yml @@ -87,7 +87,9 @@ error: delen før spørsmålstegnet. expired: Avstemmingen er utløpt, og har blitt slettet. 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: description: Dekryptering av avstemmingsdata mislyktes. Dette skjer antageligvis fordi krypteringsnøkkelen ikke er riktig. Dobbeltsjekk nettadressen diff --git a/types/ember-bootstrap/components/bs-form.d.ts b/types/ember-bootstrap/components/bs-form.d.ts index 355e1a6..7caa6f3 100644 --- a/types/ember-bootstrap/components/bs-form.d.ts +++ b/types/ember-bootstrap/components/bs-form.d.ts @@ -15,6 +15,7 @@ type BsFormComponent = ComponentLike<{ { element: BsFormElementComponent; isSubmitting: boolean; + submit: () => void; }, ]; }; diff --git a/types/ember-bootstrap/components/bs-modal.d.ts b/types/ember-bootstrap/components/bs-modal.d.ts index 267f36d..34667b6 100644 --- a/types/ember-bootstrap/components/bs-modal.d.ts +++ b/types/ember-bootstrap/components/bs-modal.d.ts @@ -9,6 +9,7 @@ declare module '@glint/environment-ember-loose/registry' { closeButton?: boolean; footer?: boolean; keyboard?: boolean; + onHide?: () => void; onHidden?: () => void; onSubmit?: () => void; open: boolean;