Bug 773271 - GCLI needs a date type; r=jwalker,harth

This commit is contained in:
Quentin Pradet 2013-05-21 10:18:55 +01:00
parent e3890d9334
commit afc2b10c07
5 changed files with 520 additions and 2 deletions

View File

@ -310,7 +310,7 @@ exports.testTsv = function(options) {
status: 'ERROR',
predictions: [ ],
unassigned: [ ],
tooltipState: 'true:isError',
tooltipState: 'false:default',
args: {
command: { name: 'tsv' },
optionType: {

View File

@ -0,0 +1,247 @@
/*
* Copyright 2009-2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE.txt or:
* http://opensource.org/licenses/BSD-3-Clause
*/
// define(function(require, exports, module) {
// <INJECTED SOURCE:START>
// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
// DO NOT EDIT IT DIRECTLY
var exports = {};
const TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testDate.js</p>";
function test() {
helpers.addTabWithToolbar(TEST_URI, function(options) {
return helpers.runTests(options, exports);
}).then(finish);
}
// <INJECTED SOURCE:END>
'use strict';
// var assert = require('test/assert');
var types = require('gcli/types');
var Argument = require('gcli/argument').Argument;
var Status = require('gcli/types').Status;
// var helpers = require('gclitest/helpers');
// var mockCommands = require('gclitest/mockCommands');
exports.setup = function(options) {
mockCommands.setup();
};
exports.shutdown = function(options) {
mockCommands.shutdown();
};
exports.testParse = function(options) {
var date = types.createType('date');
return date.parse(new Argument('now')).then(function(conversion) {
// Date comparison - these 2 dates may not be the same, but how close is
// close enough? If this test takes more than 30secs to run the it will
// probably time out, so we'll assume that these 2 values must be within
// 1 min of each other
var gap = new Date().getTime() - conversion.value.getTime();
assert.ok(gap < 60000, 'now is less than a minute away');
assert.is(conversion.getStatus(), Status.VALID, 'now parse');
});
};
exports.testMaxMin = function(options) {
var max = new Date();
var min = new Date();
var date = types.createType({ name: 'date', max: max, min: min });
assert.is(date.getMax(), max, 'max setup');
var incremented = date.increment(min);
assert.is(incremented, max, 'incremented');
};
exports.testIncrement = function(options) {
var date = types.createType('date');
return date.parse(new Argument('now')).then(function(conversion) {
var plusOne = date.increment(conversion.value);
var minusOne = date.decrement(plusOne);
// See comments in testParse
var gap = new Date().getTime() - minusOne.getTime();
assert.ok(gap < 60000, 'now is less than a minute away');
});
};
exports.testInput = function(options) {
helpers.audit(options, [
{
setup: 'tsdate 2001-01-01 1980-01-03',
check: {
input: 'tsdate 2001-01-01 1980-01-03',
hints: '',
markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVV',
status: 'VALID',
message: '',
args: {
command: { name: 'tsdate' },
d1: {
value: function(d1) {
assert.is(d1.getFullYear(), 2001, 'd1 year');
assert.is(d1.getMonth(), 0, 'd1 month');
assert.is(d1.getDate(), 1, 'd1 date');
assert.is(d1.getHours(), 0, 'd1 hours');
assert.is(d1.getMinutes(), 0, 'd1 minutes');
assert.is(d1.getSeconds(), 0, 'd1 seconds');
assert.is(d1.getMilliseconds(), 0, 'd1 millis');
},
arg: ' 2001-01-01',
status: 'VALID',
message: ''
},
d2: {
value: function(d2) {
assert.is(d2.getFullYear(), 1980, 'd1 year');
assert.is(d2.getMonth(), 0, 'd1 month');
assert.is(d2.getDate(), 3, 'd1 date');
assert.is(d2.getHours(), 0, 'd1 hours');
assert.is(d2.getMinutes(), 0, 'd1 minutes');
assert.is(d2.getSeconds(), 0, 'd1 seconds');
assert.is(d2.getMilliseconds(), 0, 'd1 millis');
},
arg: ' 1980-01-03',
status: 'VALID',
message: ''
},
}
},
exec: {
output: [ /^Exec: tsdate/, /2001/, /1980/ ],
completed: true,
type: 'string',
error: false
}
}
]);
};
exports.testIncrDecr = function(options) {
helpers.audit(options, [
{
setup: 'tsdate 2001-01-01<UP>',
check: {
input: 'tsdate 2001-01-02',
hints: ' <d2>',
markup: 'VVVVVVVVVVVVVVVVV',
status: 'ERROR',
message: '',
args: {
command: { name: 'tsdate' },
d1: {
value: function(d1) {
assert.is(d1.getFullYear(), 2001, 'd1 year');
assert.is(d1.getMonth(), 0, 'd1 month');
assert.is(d1.getDate(), 2, 'd1 date');
assert.is(d1.getHours(), 0, 'd1 hours');
assert.is(d1.getMinutes(), 0, 'd1 minutes');
assert.is(d1.getSeconds(), 0, 'd1 seconds');
assert.is(d1.getMilliseconds(), 0, 'd1 millis');
},
arg: ' 2001-01-02',
status: 'VALID',
message: ''
},
d2: {
value: undefined,
status: 'INCOMPLETE',
message: ''
},
}
}
},
{
// Check wrapping on decrement
setup: 'tsdate 2001-02-01<DOWN>',
check: {
input: 'tsdate 2001-01-31',
hints: ' <d2>',
markup: 'VVVVVVVVVVVVVVVVV',
status: 'ERROR',
message: '',
args: {
command: { name: 'tsdate' },
d1: {
value: function(d1) {
assert.is(d1.getFullYear(), 2001, 'd1 year');
assert.is(d1.getMonth(), 0, 'd1 month');
assert.is(d1.getDate(), 31, 'd1 date');
assert.is(d1.getHours(), 0, 'd1 hours');
assert.is(d1.getMinutes(), 0, 'd1 minutes');
assert.is(d1.getSeconds(), 0, 'd1 seconds');
assert.is(d1.getMilliseconds(), 0, 'd1 millis');
},
arg: ' 2001-01-31',
status: 'VALID',
message: ''
},
d2: {
value: undefined,
status: 'INCOMPLETE',
message: ''
},
}
}
},
{
// Check 'max' value capping on increment
setup: 'tsdate 2001-02-01 "27 feb 2000"<UP>',
check: {
input: 'tsdate 2001-02-01 "2000-02-28"',
hints: '',
markup: 'VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV',
status: 'VALID',
message: '',
args: {
command: { name: 'tsdate' },
d1: {
value: function(d1) {
assert.is(d1.getFullYear(), 2001, 'd1 year');
assert.is(d1.getMonth(), 1, 'd1 month');
assert.is(d1.getDate(), 1, 'd1 date');
assert.is(d1.getHours(), 0, 'd1 hours');
assert.is(d1.getMinutes(), 0, 'd1 minutes');
assert.is(d1.getSeconds(), 0, 'd1 seconds');
assert.is(d1.getMilliseconds(), 0, 'd1 millis');
},
arg: ' 2001-02-01',
status: 'VALID',
message: ''
},
d2: {
value: function(d1) {
assert.is(d1.getFullYear(), 2000, 'd1 year');
assert.is(d1.getMonth(), 1, 'd1 month');
assert.is(d1.getDate(), 28, 'd1 date');
assert.is(d1.getHours(), 0, 'd1 hours');
assert.is(d1.getMinutes(), 0, 'd1 minutes');
assert.is(d1.getSeconds(), 0, 'd1 seconds');
assert.is(d1.getMilliseconds(), 0, 'd1 millis');
},
arg: ' "2000-02-28"',
status: 'VALID',
message: ''
},
}
}
}
]);
};
// });

View File

@ -405,6 +405,27 @@ var tslong = {
exec: createExec('tslong')
};
var tsdate = {
name: 'tsdate',
description: 'long param tests to catch problems with the jsb command',
params: [
{
name: 'd1',
type: 'date',
},
{
name: 'd2',
type: {
name: 'date',
min: '1 jan 2000',
max: '28 feb 2000',
step: 2
}
},
],
exec: createExec('tsdate')
};
var tsfail = {
name: 'tsfail',
description: 'test errors',
@ -510,6 +531,7 @@ mockCommands.setup = function(opts) {
mockCommands.commands.tshidden = canon.addCommand(tshidden);
mockCommands.commands.tscook = canon.addCommand(tscook);
mockCommands.commands.tslong = canon.addCommand(tslong);
mockCommands.commands.tsdate = canon.addCommand(tsdate);
mockCommands.commands.tsfail = canon.addCommand(tsfail);
};
@ -540,6 +562,7 @@ mockCommands.shutdown = function(opts) {
canon.removeCommand(tshidden);
canon.removeCommand(tscook);
canon.removeCommand(tslong);
canon.removeCommand(tsdate);
canon.removeCommand(tsfail);
types.removeType(mockCommands.optionType);

View File

@ -114,6 +114,21 @@ typesNumberMin=%1$S is smaller than minimum allowed: %2$S.
# number, but the number has a decimal part and floats are not allowed.
typesNumberNotInt2=Can't convert "%S" to an integer.
# LOCALIZATION NOTE (typesDateNan): When the command line is passed a date,
# however the input string is not a valid date, this error message is
# displayed.
typesDateNan=Can't convert "%S" to a date.
# LOCALIZATION NOTE (typesDateMax): When the command line is passed a date,
# but the number is later than the latest allowed date, this error message is
# displayed.
typesDateMax=%1$S is later than maximum allowed: %2$S.
# LOCALIZATION NOTE (typesDateMin): When the command line is passed a date,
# but the date is earlier than the earliest allowed number, this error message
# is displayed.
typesDateMin=%1$S is earlier than minimum allowed: %2$S.
# LOCALIZATION NOTE (typesSelectionNomatch): When the command line is passed
# an option with a limited number of correct values, but the passed value is
# not one of them, this error message is displayed.

View File

@ -104,7 +104,7 @@ var mozl10n = {};
})(mozl10n);
define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/selection', 'gcli/types/command', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/connect', 'gcli/commands/context', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/canon', 'gcli/converters', 'gcli/ui/ffdisplay'], function(require, exports, module) {
define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli/types/selection', 'gcli/types/command', 'gcli/types/date', 'gcli/types/javascript', 'gcli/types/node', 'gcli/types/resource', 'gcli/types/setting', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/focus', 'gcli/ui/fields/basic', 'gcli/ui/fields/javascript', 'gcli/ui/fields/selection', 'gcli/commands/connect', 'gcli/commands/context', 'gcli/commands/help', 'gcli/commands/pref', 'gcli/canon', 'gcli/converters', 'gcli/ui/ffdisplay'], function(require, exports, module) {
'use strict';
@ -114,6 +114,7 @@ define('gcli/index', ['require', 'exports', 'module' , 'gcli/types/basic', 'gcli
require('gcli/types/selection').startup();
require('gcli/types/command').startup();
require('gcli/types/date').startup();
require('gcli/types/javascript').startup();
require('gcli/types/node').startup();
require('gcli/types/resource').startup();
@ -3940,6 +3941,237 @@ function CommandOutputManager() {
exports.CommandOutputManager = CommandOutputManager;
});
/*
* Copyright 2009-2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE.txt or:
* http://opensource.org/licenses/BSD-3-Clause
*/
define('gcli/types/date', ['require', 'exports', 'module' , 'util/promise', 'util/l10n', 'gcli/types'], function(require, exports, module) {
'use strict';
var Promise = require('util/promise');
var l10n = require('util/l10n');
var types = require('gcli/types');
var Type = require('gcli/types').Type;
var Status = require('gcli/types').Status;
var Conversion = require('gcli/types').Conversion;
function DateType(typeSpec) {
// ECMA 5.1 §15.9.1.1
// @see http://stackoverflow.com/questions/11526504/minimum-and-maximum-date
typeSpec = typeSpec || {};
this._step = typeSpec.step || 1;
this._min = new Date(-8640000000000000);
this._max = new Date(8640000000000000);
if (typeSpec.min != null) {
if (typeof typeSpec.min === 'string') {
this._min = toDate(typeSpec.min);
}
else if (isDate(typeSpec.min) || typeof typeSpec.min === 'function') {
this._min = typeSpec.min;
}
else {
throw new Error('date min value must be a string a date or a function');
}
}
if (typeSpec.max != null) {
if (typeof typeSpec.max === 'string') {
this._max = toDate(typeSpec.max);
}
else if (isDate(typeSpec.max) || typeof typeSpec.max === 'function') {
this._max = typeSpec.max;
}
else {
throw new Error('date max value must be a string a date or a function');
}
}
}
DateType.prototype = Object.create(Type.prototype);
/**
* Helper for stringify() to left pad a single digit number with a single '0'
* so 1 -> '01', 42 -> '42', etc.
*/
function pad(number) {
var r = String(number);
return r.length === 1 ? '0' + r : r;
}
DateType.prototype.stringify = function(value) {
if (!isDate(value)) {
return '';
}
var str = pad(value.getFullYear()) + '-' +
pad(value.getMonth() + 1) + '-' +
pad(value.getDate());
// Only add in the time if it's not midnight
if (value.getHours() !== 0 || value.getMinutes() !== 0 ||
value.getSeconds() !== 0 || value.getMilliseconds() !== 0) {
// What string should we use to separate the date from the time?
// There are 3 options:
// 'T': This is the standard from ISO8601. i.e. 2013-05-20T11:05
// The good news - it's a standard. The bad news - it's weird and
// alien to many if not most users
// ' ': This looks nicest, but needs escaping (which GCLI will do
// automatically) so it would look like: '2013-05-20 11:05'
// Good news: looks best, bad news: on completion we place the cursor
// after the final ', so repeated increment/decrement doesn't work
// '\ ': It's possible that we could find a way to use a \ to escape the
// space, so the output would look like: 2013-05-20\ 11:05
// This would involve changes to a number of parts, and is probably
// too complex a solution for this problem for now
// In the short term I'm going for ' ', and raising the priority of cursor
// positioning on actions like increment/decrement/tab.
str += ' ' + pad(value.getHours());
str += ':' + pad(value.getMinutes());
// Only add in seconds/milliseconds if there is anything to report
if (value.getSeconds() !== 0 || value.getMilliseconds() !== 0) {
str += ':' + pad(value.getSeconds());
if (value.getMilliseconds() !== 0) {
str += '.' + String((value.getUTCMilliseconds()/1000).toFixed(3)).slice(2, 5);
}
}
}
return str;
};
DateType.prototype.getMin = function(context) {
if (typeof this._min === 'function') {
return this._min(context);
}
if (isDate(this._min)) {
return this._min;
}
return undefined;
};
DateType.prototype.getMax = function(context) {
if (typeof this._max === 'function') {
return this._max(context);
}
if (isDate(this._max)) {
return this._max;
}
return undefined;
};
DateType.prototype.parse = function(arg, context) {
var value;
if (arg.text.replace(/\s/g, '').length === 0) {
return Promise.resolve(new Conversion(undefined, arg, Status.INCOMPLETE, ''));
}
// Lots of room for improvement here: 1h ago, in two days, etc.
// Should "1h ago" dynamically update the step?
if (arg.text === 'now') {
value = new Date();
}
else if (arg.text === 'yesterday') {
value = new Date().setDate(new Date().getDate() - 1);
}
else if (arg.text === 'tomorrow') {
value = new Date().setDate(new Date().getDate() + 1);
}
else {
var millis = Date.parse(arg.text);
if (isNaN(millis)) {
var msg = l10n.lookupFormat('typesDateNan', [ arg.text ]);
return Promise.resolve(new Conversion(undefined, arg, Status.ERROR, msg));
}
value = new Date(millis);
}
return Promise.resolve(new Conversion(value, arg));
};
DateType.prototype.decrement = function(value, context) {
if (!isDate(value)) {
return new Date();
}
var newValue = new Date(value);
newValue.setDate(value.getDate() - this._step);
if (newValue >= this.getMin(context)) {
return newValue;
}
else {
return this.getMin(context);
}
};
DateType.prototype.increment = function(value, context) {
if (!isDate(value)) {
return new Date();
}
var newValue = new Date(value);
newValue.setDate(value.getDate() + this._step);
if (newValue <= this.getMax(context)) {
return newValue;
}
else {
return this.getMax();
}
};
DateType.prototype.name = 'date';
/**
* Utility to convert a string to a date, throwing if the date can't be
* parsed rather than having an invalid date
*/
function toDate(str) {
var millis = Date.parse(str);
if (isNaN(millis)) {
throw new Error(l10n.lookupFormat('typesDateNan', [ str ]));
}
return new Date(millis);
}
/**
* Is |thing| a valid date?
* @see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
*/
function isDate(thing) {
return Object.prototype.toString.call(thing) === '[object Date]'
&& !isNaN(thing.getTime());
};
/**
* Registration and de-registration.
*/
exports.startup = function() {
types.addType(DateType);
};
exports.shutdown = function() {
types.removeType(DateType);
};
});
/*
* Copyright 2012, Mozilla Foundation and contributors
@ -10978,6 +11210,7 @@ Inputter.prototype._checkAssignment = function(start) {
* result of a keyboard event on this.element or bug 676520 could be triggered.
*/
Inputter.prototype.setInput = function(str) {
this._caretChange = Caret.TO_END;
return this.requisition.update(str);
};