refactor poll creation to not rely on Ember Data model (#710)

This commit is contained in:
Jeldrik Hanschke 2023-10-28 17:50:17 +02:00 committed by GitHub
parent 8a4954f4e8
commit 1072953cd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 366 additions and 401 deletions

View file

@ -9,17 +9,9 @@ export default class CreateOptionsDates extends Component {
this.selectedDays.length >= 1 ? this.selectedDays[0] : DateTime.local(); this.selectedDays.length >= 1 ? this.selectedDays[0] : DateTime.local();
get selectedDays() { get selectedDays() {
// Options may contain the same date multiple times with different ime return Array.from(this.args.options).map(({ value }) =>
// Must filter out those duplicates as otherwise unselect would only DateTime.fromISO(value),
// remove one entry but not all duplicates. );
return Array.from(
// using Set to remove duplicate values
new Set(
this.args.options.map(({ value }) =>
DateTime.fromISO(value).toISODate(),
),
),
).map((isoDate) => DateTime.fromISO(isoDate));
} }
get calendarCenterNext() { get calendarCenterNext() {
@ -34,53 +26,8 @@ export default class CreateOptionsDates extends Component {
return; return;
} }
// A date has either been added or removed. If it has been removed, we must
// remove all options for that date. It may be multiple options with
// different times at that date.
// If any date received as an input argument is _not_ yet in the list of
// dates, it has been added.
const dateAdded = newDatesAsLuxonDateTime.find((newDateAsLuxonDateTime) => {
return !this.selectedDays.some(
(selectedDay) =>
selectedDay.toISODate() === newDateAsLuxonDateTime.toISODate(),
);
});
if (dateAdded) {
this.args.updateOptions( this.args.updateOptions(
[ newDatesAsLuxonDateTime.map((datetime) => datetime.toISODate()),
...this.args.options.map(({ value }) => value),
dateAdded.toISODate(),
].sort(),
);
return;
}
// If no date has been added, one date must have been removed. It has been
// removed if there is one date in current selectedDays but not in the new
// dates received as input argument to the function.
const dateRemoved = this.selectedDays.find((selectedDay) => {
return !newDatesAsLuxonDateTime.some(
(newDateAsLuxonDateTime) =>
newDateAsLuxonDateTime.toISODate() === selectedDay.toISODate(),
);
});
if (dateRemoved) {
this.args.updateOptions(
this.args.options
.filter(
({ value }) =>
DateTime.fromISO(value).toISODate() !== dateRemoved.toISODate(),
)
.map(({ value }) => value),
);
return;
}
throw new Error(
'No date has been added or removed. This cannot be the case. Something spooky is going on.',
); );
} }

View file

@ -14,47 +14,39 @@
as |form| as |form|
> >
<div class="days"> <div class="days">
{{#each this.formData.options as |option index|}} {{#each-in this.formData.datetimes as |date timeOptions|}}
{{#each timeOptions as |timeOption indexInTimeOptions|}}
{{! {{!
show summarized validation state for all times in a day show summarized validation state for all times in a day
}} }}
<div <div
{{!
TODO: daysValidationState is not defined!
}}
class={{if class={{if
(get this.daysValidationState option.day) (get this.daysValidationState date)
(concat "label-has-" (get this.daysValidationState option.day)) (concat "label-has-" (get this.daysValidationState date))
"label-has-no-validation" "label-has-no-validation"
}} }}
data-test-day={{option.day}} data-test-day={{date}}
> >
<form.element <form.element
{{! {{!
TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6 TODO: Simplify to dateStyle="full" after upgrading to Ember Intl v6
}} }}
@label={{format-date @label={{format-date
option.jsDate timeOption.jsDate
weekday="long" weekday="long"
day="numeric" day="numeric"
month="long" month="long"
year="numeric" year="numeric"
}} }}
{{! {{!
show label only if it differ from label before show label only for the first time of this date
Nested-helpers are called first and object-at requires a positive integer
but returns undefined if an element with the passed in index does not exist.
Therefore we pass in array length if index is null. Cause index starting
by zero there can't be any element with an index === array.length.
}} }}
@invisibleLabel={{eq @invisibleLabel={{gt indexInTimeOptions 0}}
option.day @model={{timeOption}}
(get
(object-at
(if index (sub index 1) this.formData.options.length)
this.formData.options
)
"day"
)
}}
@model={{option}}
@property="time" @property="time"
class="option" class="option"
as |el| as |el|
@ -65,10 +57,10 @@
@type="time" @type="time"
@value={{el.value}} @value={{el.value}}
{{! focus input if it's the first one }} {{! focus input if it's the first one }}
{{autofocus enabled=(eq index 0)}} {{autofocus enabled=timeOption.isFirstTimeOnFirstDate}}
{{! run validation for partially filled input on focusout event }} {{! run validation for partially filled input on focusout event }}
{{on "focusout" (fn this.validateInput option)}} {{on "focusout" (fn this.validateInput timeOption)}}
{{on "change" (fn this.validateInput option)}} {{on "change" (fn this.validateInput timeOption)}}
{{! {{!
Validation for partially input field must be reset if input is cleared. Validation for partially input field must be reset if input is cleared.
But `@onChange` is not called and `focusout` event not triggered in that But `@onChange` is not called and `focusout` event not triggered in that
@ -83,13 +75,13 @@
partially filling in first place and Desktop Safari as well as IE 11 partially filling in first place and Desktop Safari as well as IE 11
do not support `<input type="time">`. do not support `<input type="time">`.
}} }}
{{on "focusin" (fn this.updateInputValidation option)}} {{on "focusin" (fn this.updateInputValidation timeOption)}}
{{on "keyup" (fn this.updateInputValidation option)}} {{on "keyup" (fn this.updateInputValidation timeOption)}}
id={{el.id}} id={{el.id}}
/> />
<div class="input-group-append"> <div class="input-group-append">
<BsButton <BsButton
@onClick={{fn this.formData.deleteOption option}} @onClick={{fn this.formData.deleteOption timeOption}}
@type="link" @type="link"
class="delete" class="delete"
data-test-action="delete" data-test-action="delete"
@ -107,7 +99,7 @@
</div> </div>
<BsButton <BsButton
@onClick={{fn this.formData.addOption index option.day}} @onClick={{fn this.formData.addOption date}}
@type="link" @type="link"
@size="sm" @size="sm"
class="add cr-option-menu__button cr-option-menu__add-button float-left" class="add cr-option-menu__button cr-option-menu__add-button float-left"
@ -125,6 +117,7 @@
</form.element> </form.element>
</div> </div>
{{/each}} {{/each}}
{{/each-in}}
</div> </div>
{{#if this.formData.hasMultipleDays}} {{#if this.formData.hasMultipleDays}}
@ -145,7 +138,7 @@
<NextButton /> <NextButton />
</div> </div>
<div class="col-6 col-md-4 order-1 text-right"> <div class="col-6 col-md-4 order-1 text-right">
<BackButton @onClick={{action "previousPage"}} /> <BackButton @onClick={{this.previousPage}} />
</div> </div>
</div> </div>
</BsForm> </BsForm>

View file

@ -2,15 +2,15 @@ import Component from '@glimmer/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins'; import { TrackedMap, TrackedSet } from 'tracked-built-ins';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import IntlMessage from '../utils/intl-message'; import IntlMessage from '../utils/intl-message';
class FormDataOption { class FormDataTimeOption {
formData; formData;
// ISO 8601 date string: YYYY-MM-DD // ISO 8601 date string: YYYY-MM-DD
day; date;
// ISO 8601 time string without seconds: HH:mm // ISO 8601 time string without seconds: HH:mm
@tracked time; @tracked time;
@ -31,11 +31,11 @@ class FormDataOption {
// It should show a validation error if the same time has been entered for // It should show a validation error if the same time has been entered for
// the same day already before. Only the second input field containing the // the same day already before. Only the second input field containing the
// duplicated time should show the validation error. // duplicated time should show the validation error.
const { formData, day } = this; const { formData, date } = this;
const optionsForThisDay = formData.optionsGroupedByDay[day]; const timesForThisDate = Array.from(formData.datetimes.get(date));
const isDuplicate = optionsForThisDay const isDuplicate = timesForThisDate
.slice(0, optionsForThisDay.indexOf(this)) .slice(0, timesForThisDate.indexOf(this))
.some((option) => option.time == this.time); .some((timeOption) => timeOption.time == this.time);
if (isDuplicate) { if (isDuplicate) {
return new IntlMessage('create.options-datetime.error.duplicatedDate'); return new IntlMessage('create.options-datetime.error.duplicatedDate');
} }
@ -44,8 +44,8 @@ class FormDataOption {
} }
get datetime() { get datetime() {
const { day, time } = this; const { date, time } = this;
const isoString = time === null ? day : `${day}T${time}`; const isoString = time === null ? date : `${date}T${time}`;
return DateTime.fromISO(isoString); return DateTime.fromISO(isoString);
} }
@ -59,66 +59,61 @@ class FormDataOption {
return timeValidation === null; return timeValidation === null;
} }
constructor(formData, { day, time }) { get isFirstTimeOnFirstDate() {
const { formData, date } = this;
const { datetimes } = formData;
return (
Array.from(datetimes.keys())[0] === date &&
Array.from(datetimes.get(date))[0] === this
);
}
constructor(formData, { date, time }) {
this.formData = formData; this.formData = formData;
this.day = day; this.date = date;
this.time = time; this.time = time;
} }
} }
class FormData { class FormData {
@tracked options; @tracked datetimes;
get optionsValidation() { get optionsValidation() {
const { options } = this; const { datetimes } = this;
const allOptionsAreValid = options.every((option) => option.isValid); const allTimeOptionsAreValid = Array.from(datetimes.values()).every(
if (!allOptionsAreValid) { (timeOptionsForDate) =>
Array.from(timeOptionsForDate).every(
(timeOption) => timeOption.isValid,
),
);
if (!allTimeOptionsAreValid) {
return IntlMessage('create.options-datetime.error.invalidTime'); return IntlMessage('create.options-datetime.error.invalidTime');
} }
return null; return null;
} }
get optionsGroupedByDay() {
const { options } = this;
const groupedOptions = {};
for (const option of options) {
const { day } = option;
if (!groupedOptions[day]) {
groupedOptions[day] = [];
}
groupedOptions[day].push(option);
}
return groupedOptions;
}
get hasMultipleDays() { get hasMultipleDays() {
return Object.keys(this.optionsGroupedByDay).length > 1; return this.datetimes.size > 1;
} }
@action @action
addOption(position, day) { addOption(date) {
this.options.splice( this.datetimes
position + 1, .get(date)
0, .add(new FormDataTimeOption(this, { date, time: null }));
new FormDataOption(this, { day, time: null }),
);
} }
/* /*
* removes target option if it's not the only date for this day * removes target option if it's not the only time for this date
* otherwise it deletes time for this date * otherwise it deletes time for this date
*/ */
@action @action
deleteOption(option) { deleteOption(option) {
const optionsForThisDay = this.optionsGroupedByDay[option.day]; const timeOptionsForDate = this.datetimes.get(option.date);
if (optionsForThisDay.length > 1) { if (timeOptionsForDate.size > 1) {
this.options.splice(this.options.indexOf(option), 1); timeOptionsForDate.delete(option);
} else { } else {
option.time = null; option.time = null;
} }
@ -126,42 +121,53 @@ class FormData {
@action @action
adoptTimesOfFirstDay() { adoptTimesOfFirstDay() {
const { optionsGroupedByDay } = this; const timeOptionsForFirstDay = Array.from(
const days = Object.keys(optionsGroupedByDay).sort(); Array.from(this.datetimes.values())[0],
const firstDay = days[0]; );
const optionsForFirstDay = optionsGroupedByDay[firstDay]; const timesForFirstDayAreValid = timeOptionsForFirstDay.every(
(timeOption) => timeOption.isValid,
const timesForFirstDayAreValid = optionsForFirstDay.every(
(option) => option.isValid,
); );
if (!timesForFirstDayAreValid) { if (!timesForFirstDayAreValid) {
return false; return false;
} }
const timesForFirstDay = optionsForFirstDay.map((option) => option.time); for (const date of Array.from(this.datetimes.keys()).slice(1)) {
this.datetimes.set(
this.options = new TrackedArray( date,
days new TrackedSet(
.map((day) => timeOptionsForFirstDay.map(
timesForFirstDay.map( ({ time }) => new FormDataTimeOption(this, { date, time }),
(time) => new FormDataOption(this, { day, time }), ),
),
);
}
}
constructor({ dates, times }) {
const datetimes = new Map();
for (const date of dates) {
const timesForDate = times.has(date)
? Array.from(times.get(date))
: [null];
datetimes.set(
date,
new TrackedSet(
timesForDate.map(
(time) => new FormDataTimeOption(this, { date, time }),
),
), ),
)
.flat(),
); );
} }
constructor(options) { this.datetimes = new TrackedMap(datetimes);
this.options = new TrackedArray(
options.map(({ day, time }) => new FormDataOption(this, { day, time })),
);
} }
} }
export default class CreateOptionsDatetime extends Component { export default class CreateOptionsDatetime extends Component {
@service router; @service router;
formData = new FormData(this.args.dates); formData = new FormData({ dates: this.args.dates, times: this.args.times });
@tracked errorMesage = null; @tracked errorMesage = null;
@ -208,7 +214,25 @@ export default class CreateOptionsDatetime extends Component {
@action @action
handleTransition(transition) { handleTransition(transition) {
if (transition.from?.name === 'create.options-datetime') { if (transition.from?.name === 'create.options-datetime') {
this.args.updateOptions(this.formData.options); this.args.updateOptions(
// FormData.datetimes Map has a Set of FormDataTime object as values
// We need to transform it to a Set of plain time strings
new Map(
Array.from(this.formData.datetimes.entries())
.map(([key, timeOptions]) => [
key,
new Set(
Array.from(timeOptions)
.map(({ time }) => time)
// There might be FormDataTime objects without a time, which
// we need to filter out
.filter((time) => time !== null),
),
])
// There might be dates without any time, which we need to filter out
.filter(([, times]) => times.size > 0),
),
);
this.router.off('routeWillChange', this.handleTransition); this.router.off('routeWillChange', this.handleTransition);
} }
} }

View file

@ -79,10 +79,12 @@ class FormData {
constructor({ options }, { defaultOptionCount }) { constructor({ options }, { defaultOptionCount }) {
const normalizedOptions = const normalizedOptions =
options.length === 0 && defaultOptionCount > 0 ? ['', ''] : options; options.size === 0 && defaultOptionCount > 0
? ['', '']
: Array.from(options);
this.options = new TrackedArray( this.options = new TrackedArray(
normalizedOptions.map(({ title }) => new FormDataOption(this, title)), normalizedOptions.map((value) => new FormDataOption(this, value)),
); );
} }
} }

View file

@ -23,12 +23,16 @@ export default class CreateController extends Controller {
get canEnterOptionsDatetimeStep() { get canEnterOptionsDatetimeStep() {
return ( return (
this.visitedSteps.has('options-datetime') && this.visitedSteps.has('options-datetime') &&
this.model.options.length >= 1 this.model.dateOptions.size >= 1
); );
} }
get canEnterSettingsStep() { get canEnterSettingsStep() {
return this.visitedSteps.has('settings') && this.model.options.length >= 1; const { model, visitedSteps } = this;
const { dateOptions, freetextOptions, pollType } = model;
const options = pollType === 'FindADate' ? dateOptions : freetextOptions;
return visitedSteps.has('settings') && options.size >= 1;
} }
get isFindADate() { get isFindADate() {

View file

@ -1,6 +1,5 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { DateTime } from 'luxon';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
export default class CreateOptionsDatetimeController extends Controller { export default class CreateOptionsDatetimeController extends Controller {
@ -18,14 +17,7 @@ export default class CreateOptionsDatetimeController extends Controller {
} }
@action @action
updateOptions(options) { updateOptions(datetimes) {
this.model.options = options this.model.timesForDateOptions = new Map(datetimes.entries());
.map(({ day, time }) =>
time ? DateTime.fromISO(`${day}T${time}`).toISO() : day,
)
.sort()
.map((isoString) => {
return this.store.createFragment('option', { title: isoString });
});
} }
} }

View file

@ -1,6 +1,7 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { TrackedSet } from 'tracked-built-ins/.';
export default class CreateOptionsController extends Controller { export default class CreateOptionsController extends Controller {
@service router; @service router;
@ -8,9 +9,9 @@ export default class CreateOptionsController extends Controller {
@action @action
nextPage() { nextPage() {
const { isFindADate } = this.model; const { pollType } = this.model;
if (isFindADate) { if (pollType === 'FindADate') {
this.router.transitionTo('create.options-datetime'); this.router.transitionTo('create.options-datetime');
} else { } else {
this.router.transitionTo('create.settings'); this.router.transitionTo('create.settings');
@ -24,8 +25,13 @@ export default class CreateOptionsController extends Controller {
@action @action
updateOptions(newOptions) { updateOptions(newOptions) {
this.model.options = newOptions.map(({ value }) => const { pollType } = this.model;
this.store.createFragment('option', { title: value }), const options = newOptions.map(({ value }) => value);
);
if (pollType === 'FindADate') {
this.model.dateOptions = new TrackedSet(options.sort());
} else {
this.model.freetextOptions = new TrackedSet(options);
}
} }
} }

View file

@ -9,6 +9,7 @@ export default class CreateSettings extends Controller {
@service flashMessages; @service flashMessages;
@service intl; @service intl;
@service router; @service router;
@service store;
get anonymousUser() { get anonymousUser() {
return this.model.anonymousUser; return this.model.anonymousUser;
@ -77,9 +78,9 @@ export default class CreateSettings extends Controller {
@action @action
previousPage() { previousPage() {
let { isFindADate } = this.model; let { pollType } = this.model;
if (isFindADate) { if (pollType === 'FindADate') {
this.router.transitionTo('create.options-datetime'); this.router.transitionTo('create.options-datetime');
} else { } else {
this.router.transitionTo('create.options'); this.router.transitionTo('create.options');
@ -88,19 +89,68 @@ export default class CreateSettings extends Controller {
@action @action
async submit() { async submit() {
const { model: poll } = this; const { model } = this;
const {
anonymousUser,
answerType,
description,
expirationDate,
forceAnswer,
freetextOptions,
dateOptions,
timesForDateOptions,
pollType,
title,
} = model;
let options = [];
if (pollType === 'FindADate') {
// merge date with times
for (const date of dateOptions) {
if (timesForDateOptions.has(date)) {
for (const time of timesForDateOptions.get(date)) {
const [hour, minute] = time.split(':');
options.push(
DateTime.fromISO(date)
.set({
hour,
minute,
second: 0,
millisecond: 0,
})
.toISO(),
);
}
} else {
options.push(date);
}
}
} else {
options.push(...freetextOptions);
}
const poll = this.store.createRecord('poll', {
anonymousUser,
answerType,
creationDate: new Date().toISOString(),
description,
expirationDate,
forceAnswer,
options: options.map((option) =>
this.store.createFragment('option', { title: option }),
),
pollType,
title,
});
// set timezone if there is atleast one option with time // set timezone if there is atleast one option with time
if ( if (
poll.isFindADate && pollType === 'FindADate' &&
poll.options.toArray().some((option) => { Array.from(timesForDateOptions.values()).some((timesForDateOptions) => {
return option.hasTime; return timesForDateOptions.size > 0;
}) })
) { ) {
this.set( poll.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
'model.timezone',
Intl.DateTimeFormat().resolvedOptions().timeZone,
);
} }
// save poll // save poll

View file

@ -1,13 +1,20 @@
import { modifier } from 'ember-modifier'; import Modifier from 'ember-modifier';
export default class AutofocusModifier extends Modifier {
isInstalled = false;
modify(element, positional, { enabled = true }) {
// element should be only autofocused on initial render
// not when `enabled` option is invalidated
if (this.isInstalled) {
return;
}
this.isInstalled = true;
export default modifier(function autofocus(
element,
params,
{ enabled = true },
) {
if (!enabled) { if (!enabled) {
return; return;
} }
element.focus(); element.focus();
}); }
}

View file

@ -1,10 +1,22 @@
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import config from 'croodle/config/environment';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { tracked } from '@glimmer/tracking';
import { TrackedSet } from 'tracked-built-ins';
class PollData {
@tracked anonymousUser = false;
@tracked answerType = 'YesNo';
@tracked description = '';
@tracked expirationDate = DateTime.local().plus({ months: 3 }).toISO();
@tracked forceAnswer = true;
@tracked freetextOptions = new TrackedSet();
@tracked dateOptions = new TrackedSet();
@tracked timesForDateOptions = new Map();
@tracked pollType = 'FindADate';
@tracked title = '';
}
@classic
export default class CreateRoute extends Route { export default class CreateRoute extends Route {
@service encryption; @service encryption;
@service router; @service router;
@ -21,17 +33,7 @@ export default class CreateRoute extends Route {
} }
model() { model() {
// create empty poll return new PollData();
return this.store.createRecord('poll', {
answerType: 'YesNo',
creationDate: new Date(),
forceAnswer: true,
anonymousUser: false,
pollType: 'FindADate',
timezone: null,
expirationDate: DateTime.local().plus({ months: 3 }).toISO(),
version: config.APP.version,
});
} }
activate() { activate() {

View file

@ -1,5 +1,6 @@
<CreateOptionsDatetime <CreateOptionsDatetime
@dates={{@model.options}} @dates={{@model.dateOptions}}
@times={{@model.timesForDateOptions}}
@onNextPage={{this.nextPage}} @onNextPage={{this.nextPage}}
@onPrevPage={{this.previousPage}} @onPrevPage={{this.previousPage}}
@updateOptions={{this.updateOptions}} @updateOptions={{this.updateOptions}}

View file

@ -1,7 +1,11 @@
<CreateOptions <CreateOptions
@isFindADate={{@model.isFindADate}} @isFindADate={{eq @model.pollType "FindADate"}}
@isMakeAPoll={{@model.isMakeAPoll}} @isMakeAPoll={{eq @model.pollType "MakeAPoll"}}
@options={{@model.options}} @options={{if
(eq @model.pollType "FindADate")
@model.dateOptions
@model.freetextOptions
}}
@onPrevPage={{this.previousPage}} @onPrevPage={{this.previousPage}}
@onNextPage={{this.nextPage}} @onNextPage={{this.nextPage}}
@updateOptions={{this.updateOptions}} @updateOptions={{this.updateOptions}}

View file

@ -923,7 +923,7 @@ module('Acceptance | create a poll', function (hooks) {
await pageCreateMeta.next(); await pageCreateMeta.next();
assert.equal(currentRouteName(), 'create.options'); assert.equal(currentRouteName(), 'create.options');
await pageCreateOptions.textOptions.objectAt(0).add(); await pageCreateOptions.textOptions.objectAt(0).title('foo');
await pageCreateOptions.textOptions.objectAt(1).title('bar'); await pageCreateOptions.textOptions.objectAt(1).title('bar');
await pageCreateOptions.next(); await pageCreateOptions.next();
assert.equal(currentRouteName(), 'create.settings'); assert.equal(currentRouteName(), 'create.settings');

View file

@ -1,9 +1,7 @@
import { run } from '@ember/runloop';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { render, click, find, findAll } from '@ember/test-helpers'; import { render, click, find, findAll } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { DateTime } from 'luxon';
module('Integration | Component | create options datetime', function (hooks) { module('Integration | Component | create options datetime', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -20,20 +18,11 @@ module('Integration | Component | create options datetime', function (hooks) {
*/ */
test('it generates input field for options iso 8601 date string (without time)', async function (assert) { test('it generates input field for options iso 8601 date string (without time)', async function (assert) {
// validation is based on validation of every option fragment this.set('dates', new Set(['2015-01-01']));
// which validates according to poll model it belongs to this.set('times', new Map());
// therefore each option needs to be pushed to poll model to have it as await render(
// it's owner hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
run(() => {
this.set(
'poll',
this.store.createRecord('poll', {
pollType: 'FindADate',
options: [{ title: '2015-01-01' }],
}),
); );
});
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
assert.equal( assert.equal(
findAll('.days .form-group input').length, findAll('.days .form-group input').length,
@ -48,20 +37,11 @@ module('Integration | Component | create options datetime', function (hooks) {
}); });
test('it generates input field for options iso 8601 datetime string (with time)', async function (assert) { test('it generates input field for options iso 8601 datetime string (with time)', async function (assert) {
// validation is based on validation of every option fragment this.set('dates', new Set(['2015-01-01']));
// which validates according to poll model it belongs to this.set('times', new Map([['2015-01-01', new Set(['11:11'])]]));
// therefore each option needs to be pushed to poll model to have it as await render(
// it's owner hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
run(() => {
this.set(
'poll',
this.store.createRecord('poll', {
pollType: 'FindADate',
options: [{ title: '2015-01-01T11:11:00.000Z' }],
}),
); );
});
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
assert.equal( assert.equal(
findAll('.days .form-group input').length, findAll('.days .form-group input').length,
@ -70,30 +50,17 @@ module('Integration | Component | create options datetime', function (hooks) {
); );
assert.equal( assert.equal(
find('.days .form-group input').value, find('.days .form-group input').value,
DateTime.fromISO('2015-01-01T11:11:00.000Z').toFormat('HH:mm'), '11:11',
'it has time in option as value', 'it has time in option as value',
); );
}); });
test('it hides repeated labels', async function (assert) { test('it hides repeated labels', async function (assert) {
// validation is based on validation of every option fragment this.set('dates', new Set(['2015-01-01', '2015-02-02']));
// which validates according to poll model it belongs to this.set('times', new Map([['2015-01-01', new Set(['11:11', '22:22'])]]));
// therefore each option needs to be pushed to poll model to have it as await render(
// it's owner hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
run(() => {
this.set(
'poll',
this.store.createRecord('poll', {
pollType: 'FindADate',
options: [
{ title: DateTime.fromISO('2015-01-01T10:11').toISO() },
{ title: DateTime.fromISO('2015-01-01T22:22').toISO() },
{ title: '2015-02-02' },
],
}),
); );
});
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
assert.equal( assert.equal(
findAll('.days label').length, findAll('.days label').length,
@ -126,19 +93,11 @@ module('Integration | Component | create options datetime', function (hooks) {
}); });
test('allows to add another option', async function (assert) { test('allows to add another option', async function (assert) {
// validation is based on validation of every option fragment this.set('dates', new Set(['2015-01-01', '2015-02-02']));
// which validates according to poll model it belongs to this.set('times', new Map());
// therefore each option needs to be pushed to poll model to have it as await render(
// it's owner hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
run(() => {
this.set(
'poll',
this.store.createRecord('poll', {
options: [{ title: '2015-01-01' }, { title: '2015-02-02' }],
}),
); );
});
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
assert.equal( assert.equal(
findAll('.days .form-group input').length, findAll('.days .form-group input').length,
@ -166,23 +125,11 @@ module('Integration | Component | create options datetime', function (hooks) {
}); });
test('allows to delete an option', async function (assert) { test('allows to delete an option', async function (assert) {
// validation is based on validation of every option fragment this.set('dates', new Set(['2015-01-01']));
// which validates according to poll model it belongs to this.set('times', new Map([['2015-01-01', new Set(['11:11', '22:22'])]]));
// therefore each option needs to be pushed to poll model to have it as await render(
// it's owner hbs`<CreateOptionsDatetime @dates={{this.dates}} @times={{this.times}} />`,
run(() => {
this.set(
'poll',
this.store.createRecord('poll', {
pollType: 'FindADate',
options: [
{ title: DateTime.fromISO('2015-01-01T11:11').toISO() },
{ title: DateTime.fromISO('2015-01-01T22:22').toISO() },
],
}),
); );
});
await render(hbs`<CreateOptionsDatetime @dates={{this.poll.options}} />`);
assert.equal( assert.equal(
findAll('.days input').length, findAll('.days input').length,

View file

@ -1,8 +1,8 @@
import { run } from '@ember/runloop';
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import { render, findAll, blur, fillIn, focus } from '@ember/test-helpers'; import { render, findAll, blur, fillIn, focus } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { TrackedSet } from 'tracked-built-ins';
module('Integration | Component | create options', function (hooks) { module('Integration | Component | create options', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -14,25 +14,14 @@ module('Integration | Component | create options', function (hooks) {
test('shows validation errors if options are not unique (makeAPoll)', async function (assert) { test('shows validation errors if options are not unique (makeAPoll)', async function (assert) {
assert.expect(5); assert.expect(5);
// validation is based on validation of every option fragment this.set('options', new TrackedSet());
// which validates according to poll model it belongs to
// therefore each option needs to be pushed to poll model to have it as
// it's owner
let poll;
run(() => {
poll = this.store.createRecord('poll', {
pollType: 'MakeAPoll',
});
});
this.set('poll', poll);
this.set('options', poll.get('options'));
await render(hbs` await render(hbs`
<CreateOptions <CreateOptions
@options={{this.options}} @options={{this.options}}
@isDateTime={{false}} @isDateTime={{false}}
@isFindADate={{this.poll.isFindADate}} @isFindADate={{false}}
@isMakeAPoll={{this.poll.isMakeAPoll}} @isMakeAPoll={{true}}
/> />
`); `);
@ -64,25 +53,14 @@ module('Integration | Component | create options', function (hooks) {
}); });
test('shows validation errors if option is empty (makeAPoll)', async function (assert) { test('shows validation errors if option is empty (makeAPoll)', async function (assert) {
// validation is based on validation of every option fragment this.set('options', new TrackedSet());
// which validates according to poll model it belongs to
// therefore each option needs to be pushed to poll model to have it as
// it's owner
let poll;
run(() => {
poll = this.store.createRecord('poll', {
pollType: 'MakeAPoll',
});
});
this.set('poll', poll);
this.set('options', poll.get('options'));
await render(hbs` await render(hbs`
<CreateOptions <CreateOptions
@options={{this.options}} @options={{this.options}}
@isDateTime={{false}} @isDateTime={{false}}
@isFindADate={{this.poll.isFindADate}} @isFindADate={{false}}
@isMakeAPoll={{this.poll.isMakeAPoll}} @isMakeAPoll={{true}}
/> />
`); `);

View file

@ -25,4 +25,12 @@ module('Integration | Modifier | autofocus', function (hooks) {
assert.dom('input').isNotFocused(); assert.dom('input').isNotFocused();
}); });
test('it does not focus the element if `enabled` changes', async function (assert) {
this.set('enabled', false);
await render(hbs`<input {{autofocus enabled=this.enabled}} />`);
this.set('enabled', true);
assert.dom('input').isNotFocused();
});
}); });