-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Calendar / Datepicker: improve tests and add missing events #1755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
fnagel
commented
Sep 30, 2016
- Fix datepicker min / max attribute parsing
- Add change event incl. basic tests
- Add unit tests for calendar select event
- Add unit tests for calendar value option
- Add unit tests for datepicker min / max option
- Introduce dateEqual assertion as proposed in wiki
- Make use of setup / teardown methods and remove helper file as proposed in wiki
Unit tests work fine in browser and cli when running locally. Any ideas? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be a bit easier to get these landed with a smaller scope. Mixing refactoring unit tests and implementation changes isn't necessary, as far as I can tell.
As for the failing tests, that looks like a timezone issue. I can reproduce it locally by running tests in a different timezone, like this:
TZ=:/usr/share/zoneinfo/America/Sao_Paulo npm test
Specifically for calendar/datepicker, we should start doing this by default.
module( "calendar: core", { | ||
setup: function() { | ||
element = $( "#calendar" ).calendar(); | ||
widget = element.calendar( "widget" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could assign this.widget =
here, then reference this.widget
inside the test, to avoid the top-level vars.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
}, 50 ); | ||
} | ||
|
||
step1(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is called immediately, the function seems unnecessary. Could inline the setTimeout
call here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do this a lot because it makes it easier to read the order of the test from top to bottom.
_handleKeydown: function( event ) { | ||
var pageAltKey = ( event.altKey || event.ctrlKey && event.shiftKey ); | ||
|
||
switch ( event.keyCode ) { | ||
case $.ui.keyCode.ENTER: | ||
this.activeDescendant.mousedown(); | ||
case $.ui.keyCode.ENTER: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indent shouldn't change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
_select: function( event ) { | ||
var oldValue = this.options.value ? this.options.value.getTime() : ""; | ||
|
||
this._setOption( "value", new Date( $( event.currentTarget ).data( "timestamp" ) ) ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this access this.activeDescendant
instead? Would make the ENTER key event handler simpler, since it wouldn't need to pass the event along.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're suggesting to replace new Date( $( event.currentTarget ).data( "timestamp" ) )
with this.activeDescendant
right?
Don't think so as this.activeDescendant
would be the current day, not the selected one. this.activeDescendant
will be updated in the next line, in _updateDayElement
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, makes sense.
|
||
if ( $.type( this.options.max ) === "string" ) { | ||
this.options.max = Globalize.parseDate( this.options.max, { raw: "yyyy-MM-dd" } ); | ||
this.options.max = this._parse( this.options.max ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to http://wiki.jqueryui.com/w/page/12137778/Datepicker both min
and max
should be date
, not string
. Is that document out of date?
If so, wasn't it on purpose that we used a fixed iso format for min/max as string, instead of matching the dateFormat
option? With this change, the developer would need to update min/max whenever the (user facing) dateFormat
option changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the parsing happening in _getCreateOptions
, I'm even more confused, since that uses a different format.
Since I doubt these changes are happening by accident, let's update the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation is:
min / max
option passed as datemin / max
option passed as string: format value according todateFormat
optionmin / max
attributes on input element: format value asyyyy-MM-dd
(according to HTML specs)
I'm not able to find the reference in the GH comments, but we wanted it this way and I guess we need to update the wiki specs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recall the discussions about this, but I don't recall where the discussions occurred. The main thing we want to support is Date
instances. I believe what we wanted to do for strings was parse the string into a date and store the date, not the string. For attributes, in order to support valid HTML5, we must follow the spec, and not support custom formatting; we'd still store the options as Date
instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe what we wanted to do for strings was parse the string into a date and store the date, not the string. For attributes, in order to support valid HTML5, we must follow the spec, and not support custom formatting; we'd still store the options as Date instances.
That's what we have now. We only store date objects and attributes formatting is hard coded.
Changing the wiki specs then, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The above makes sense. Let's update the spec with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -172,6 +176,9 @@ var widget = $.widget( "ui.datepicker", { | |||
}, | |||
blur: function() { | |||
this.suppressExpandOnFocus = false; | |||
}, | |||
change: function( event ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the native change event reliable enough? Usually the answer to that is "no", may need to do a manual dirty-check in blur
and trigger change
from there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure. Had massive issues creating reliable tests for this use case.
Checking the blur events is komplex due to the timeout handling. I wasn't able to prevent additional events from firing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The native change
event should be fine for user input. It's only programmatic value changes that would cause the event to not be fired.
Totally agree. I will make sure next PRs are more specific.
Mhh, as always, it's not that easy when using windows. Not sure if it's even possible without changing system time globally... :-/ More important would be how to deal with this for the travis tests? Do we want to test multiple zones? Or just setting a specific one for the tests? |
Does Git Bash help? https://git-for-windows.github.io/
On another project where I ran into a similar problem, we decided to run the test suite twice, once on the CI machine's local timezone, once on a specific, different one. That way we make sure that the tests pass in different timezones. Not necessarily in all of them, but testing 2 is much better than just the default. Its also possible that these issues only affect the testsuite, in which case it might be much more pragmatic to just set a fixed timezone. |
} ); | ||
|
||
test( "change", function() { | ||
expect( 6 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is using globals. Should use assert.expect()
(assert
gets passed as a parameter to the test()
callback).
Looks like the datepicker branch is missing some updates to not allow the use of the QUnit globals. I guess we'll address that separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, as I'm already doing this for the custom dateEqual assert this makes sense to me.
Looks like the datepicker branch is missing some updates to not allow the use of the QUnit globals.
A merge with master is needed anyway when this PR is merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
equal( | ||
event.originalEvent.type, | ||
eventType, | ||
"select originalEvent on calendar button " + eventType |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/select/change/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
message = "on calendar button " + eventType; | ||
element.find( "table button:eq(1)" ).simulate( eventType ); | ||
step2(); | ||
}, 50 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unwrap the timeouts inside the steps. Use the timeout for invoking the next step:
function step1() {
eventType = "mousedown";
message = "on calendar button " + eventType;
element.find( "table button:eq(1)" ).simulate( eventType );
setTimeout( step2, 50 );
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
var date = new Date( 2015, 8 - 1, 1 ); | ||
|
||
// Number of month option does not work after init |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to work. Can you just make this a TODO for now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
element.calendar( "option", "value", date ); | ||
assert.dateEqual( element.calendar( "option", "value" ), date, "Value set" ); | ||
equal( widget.find( "table button.ui-state-active" ).data( "timestamp" ), 1463954400000, "Active button timestamp" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to prefix our data keys, so this should be ui-calendar-timestamp
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
equal( widget.find( "table button.ui-state-active" ).data( "timestamp" ), 1463954400000, "Active button timestamp" ); | ||
|
||
element.calendar( "option", "value", "invalid" ); | ||
assert.dateEqual( element.calendar( "option", "value" ), date, "Value after invalid parameter" ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we've discussed this before, but I don't remember talking through this. Is there a reason we leave the previous value instead of reverting to null
? Native date inputs will clear their value if you try to set an invalid value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, no reason afaik and I don't remember talking trough this. I'll see if this can be done easily within the _setOption
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@scottgonzalez What about dates invalid due to min / max option? Upcoming change will include resetting value to null when it's not within min / max limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those get reverted to null
as well.
@@ -13,8 +13,7 @@ | |||
<div id="qunit"></div> | |||
<div id="qunit-fixture"> | |||
|
|||
<input type="text" id="datepicker"> | |||
<input type="text" id="datepicker2"> | |||
<input type="text" id="datepicker" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the trailing slash. We don't use XHTML.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -79,30 +81,33 @@ var widget = $.widget( "ui.datepicker", { | |||
_getCreateOptions: function() { | |||
var max = this.element.attr( "max" ), | |||
min = this.element.attr( "min" ), | |||
parser = new Globalize( "en" ).dateParser( { raw: "yyyy-MM-dd" } ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not directly related to this PR, but does Globalize always contain the en
data or do we need to tell users to always include this to handle the attribute parsing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, but I guess it needs to be included.
@rxaviers Can you help us out here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this raw pattern, the locale might not be required, since there's nothing locale-specific, I think. In general, Globalize doesn't bundle any locales.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, there's one detail.
since there's nothing locale-specific
For the above pattern, some stuff are locale independent, but some are locale-specific.
Locale independent:
- Your pattern will result in numeric output only and given you're passing a raw format, the order of the fields are independent of any locale preferences.
Locale dependent:
- The only difference you would get is numbering system, for example, by using Arabic you would can expect something like
'٢٠١٦-١٠-١٣'
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, refresh my mind here? Is this parser being used to parse developer input? If so, I believe it's expected to be in English (or English POSIX, or root) form.
By the way, this is a very simple parsing, so it's also worth considering not using Globalize and instead split the string at '-' and constructing the Date using the array pieces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for manual parsing without Globalize
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to set the correct offset for the month, since it is defined as "Integer value representing the month, beginning with 0 for January to 11 for December."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like tests are littered with x - 1
for the month everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No idea why, but I was focused on the missing hour not the month. Stupid me.
Pushed a new commit with this.
@@ -172,6 +176,9 @@ var widget = $.widget( "ui.datepicker", { | |||
}, | |||
blur: function() { | |||
this.suppressExpandOnFocus = false; | |||
}, | |||
change: function( event ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The native change
event should be fine for user input. It's only programmatic value changes that would cause the event to not be fired.
}, | ||
|
||
_setOption: function( key, value ) { | ||
if ( key === "max" || key === "min" ) { | ||
if ( $.type( value ) === "string" ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just use typeof
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Just for my curiosity: Why not using the internals?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jQuery's type detection is mostly unneeded in modern browsers. For strings, it was never needed. $.type()
was just an abstraction away from the $.is*()
methods which were limited to types that mattered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the info.
Done.
@jzaefferer Already using git bash, but tried power shell as well :-/ |
Added timezone / travis functionality as a todo task in wiki. |
Can you get the test to fail locally by changing your system timezone? Have you tried running tests inside a Docker container? If not, I can help set that up. |
Oh man, changing the system timezone via the UI works (but does not using similar console command). Seems this messes with some of local tools but for a quick test this seems to work. |
I'm using VirtualBox / Vagrant VMs for other projects but always tried to get around this for jQuery UI. |
aaac0d5
to
0891b50
Compare
@jzaefferer @scottgonzalez Updated the branch. Let my know what you think! |
@@ -56,7 +56,7 @@ test( "value", function( assert ) { | |||
equal( this.element.calendar( "value" ), "1/1/14", "getter" ); | |||
|
|||
this.element.calendar( "value", "abc" ); | |||
equal( this.element.calendar( "value" ), "1/1/14", "Setting invalid values should be ignored." ); | |||
equal( this.element.calendar( "value" ), null, "Setting invalid values." ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be assert.equal
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are currently using this for assert.equalDate
and assert.expect
only. Do we want this for all qunit globals?
@@ -646,7 +646,8 @@ return $.widget( "ui.calendar", { | |||
if ( arguments.length ) { | |||
this.valueAsDate( this._parse( value ) ); | |||
} else { | |||
return this._format( this.option( "value" ) ); | |||
return this.option( "value" ) === null ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about return this.option( "value" ) === null ? null : ...
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Updates look (mostly) good. Still need to fix the CI-failing test. |
0891b50
to
bd488f9
Compare
Correct. |
I've looked at the timezone-dependent failure. When running the calendar tests with Sao Paulo timezone, the actual/expected values are only 4 seconds apart: >> calendar: options - value
>> Message: Active button timestamp
>> Actual: 1463972400000
>> Expected: 1463954400000
> Date(1463972400000)
'Tue Oct 25 2016 12:07:23 GMT+0200 (CEST)'
> Date(1463954400000)
'Tue Oct 25 2016 12:07:27 GMT+0200 (CEST)' Still not sure what's causing this. If we can't figure it out, we can probably make the test a little less precise. We don't care about the time component here, only the date, right? |
This reverts commit ea986f2.
Correct, calendar and datepicker don't care about time at the moment, but we should keep in mind that timepicker addon is a widely used one. |
@scottgonzalez Added invalid min / max reset incl. tests |
Let's keep the timestamp then, but update the test to only look at the date, not the time. |
So, using our dateequal helper everywhere should help, as we already check for year / month / date only: |
Fixed tests by replacing a timestamp compare with a dateEqual assrert. Ready to merge? |
CLA error? |
this.widget.find( "table button.ui-state-active" ).data( "ui-calendar-timestamp" ), | ||
1463954400000, | ||
assert.dateEqual( | ||
new Date( this.widget.find( "table button.ui-state-active" ).data( "ui-calendar-timestamp" ) / 1000 ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is odd - why is the timestamp data attribute in a different format than the one accepted by new Date
? Shouldn't those match?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'r right, both should be milliseconds. Changed,
Only a +1/kudos comment remains
The new Foundation is still working on a new CLA check. Until that is ready we can ignore the check at least for team members. |
34faf02
to
fb3841f
Compare
Fix travis unit test issue due to timezone difference.
fb3841f
to
68028e7
Compare
@jzaefferer So, merge ready now, right? I would merge master in datepicker and push that right away, ok? |
Yep |