From bb9f7a62c807b4c186a22fc111de780ddc1de8a9 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Fri, 4 Apr 2025 09:56:00 +1100 Subject: DateFormatter: Fix exception if user date option is not available If the user's date option is not available in the selected user language, fall back to the site default and then to "dmy", the last being guaranteed to exist since all languages are merged with MessagesEn.php. Add test. Change-Id: I9ccc6ebe747070fec2b80398a8251924c9a28fbc --- .../src/mediawiki.DateFormatter/DateFormatter.js | 33 +++++++++++- .../mediawiki.DateFormatter/DateFormatter.test.js | 62 +++++++++++++++++----- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/resources/src/mediawiki.DateFormatter/DateFormatter.js b/resources/src/mediawiki.DateFormatter/DateFormatter.js index 4281a1e0163c..f67ba0cca381 100644 --- a/resources/src/mediawiki.DateFormatter/DateFormatter.js +++ b/resources/src/mediawiki.DateFormatter/DateFormatter.js @@ -595,7 +595,7 @@ class DateFormatter { * @return {string} */ formatInternal( style, type, date ) { - const formatName = style ? `${ style } ${ type }` : type; + const formatName = this.makeValidFormatName( style, type ); const formatter = this.getIntlFormatInternal( formatName ); const pattern = this.formats[ formatName ].pattern; if ( pattern ) { @@ -613,6 +613,35 @@ class DateFormatter { } } + /** + * Validate a style/type and combine them into a single string, falling + * back to the default style if the user style is not available with the + * specified type. + * + * @internal + * @ignore + * + * @param {string|null} style + * @param {string} type + * @return {string} + */ + makeValidFormatName( style, type ) { + if ( !style ) { + return type; + } + // Try the specified style, then the site default style, then "dmy", a + // final fallback which should always exist, because localised date + // format arrays are merged with English, which has "dmy". + for ( const tryStyle of [ style, config.defaultStyle, 'dmy' ] ) { + const name = `${ tryStyle } ${ type }`; + if ( name in this.formats ) { + return name; + } + } + // Perhaps an invalid type, or bad config? + throw new Error( `Unable to find a valid date format for "${ style } ${ type }"` ); + } + /** * Format a time/date range with a specified style * @@ -626,7 +655,7 @@ class DateFormatter { * @return {string} */ formatRangeInternal( style, type, date1, date2 ) { - const formatName = style ? `${ style } ${ type }` : type; + const formatName = this.makeValidFormatName( style, type ); const formatter = this.getIntlFormatInternal( formatName ); const pattern = this.formats[ formatName ].rangePattern; if ( pattern ) { diff --git a/tests/qunit/resources/mediawiki.DateFormatter/DateFormatter.test.js b/tests/qunit/resources/mediawiki.DateFormatter/DateFormatter.test.js index be6efbf412b6..a0cda3528b20 100644 --- a/tests/qunit/resources/mediawiki.DateFormatter/DateFormatter.test.js +++ b/tests/qunit/resources/mediawiki.DateFormatter/DateFormatter.test.js @@ -3,17 +3,19 @@ const midnightZulu = new Date( '2025-01-01T00:00:00Z' ); const oneZulu = new Date( '2025-01-01T01:00:00Z' ); const nextDay = new Date( '2025-01-02T00:00:00Z' ); -function fakeOptionsGet( key, fallback ) { - const options = { - timecorrection: 'Offset|60' - }; - return key in options ? options[ key ] : fallback; -} - QUnit.module( 'mediawiki.DateFormatter static functions', ( hooks ) => { + let userOptions; + + function fakeOptionsGet( key, fallback ) { + return key in userOptions ? userOptions[ key ] : fallback; + } hooks.beforeEach( function () { + userOptions = { + timecorrection: 'Offset|60' + }; this.sandbox.stub( mw.user.options, 'get', fakeOptionsGet ); + DateFormatter.clearInstanceCache(); } ); QUnit.test( 'forUser', ( assert ) => { @@ -41,13 +43,36 @@ QUnit.module( 'mediawiki.DateFormatter static functions', ( hooks ) => { assert.strictEqual( instance.formatTime( midnightZulu ), '04:00' ); } ); - QUnit.test( 'formatTimeAndDate', ( assert ) => { - const { formatTimeAndDate } = DateFormatter; - assert.strictEqual( - formatTimeAndDate( midnightZulu ), - '01:00, 1 (january) 2025' - ); - } ); + const formatTimeAndDateCases = [ + { + title: 'null', + dateOption: null, + expected: '01:00, 1 (january) 2025' + }, + { + title: 'mdy', + dateOption: 'mdy', + expected: '01:00, (january) 1, 2025' + }, + { + title: 'bad option', + dateOption: 'bad', + expected: '01:00, 1 (january) 2025' + } + ]; + + QUnit.test.each( + 'formatTimeAndDate', + formatTimeAndDateCases, + ( assert, { dateOption, expected } ) => { + userOptions.date = dateOption; + const { formatTimeAndDate } = DateFormatter; + assert.strictEqual( + formatTimeAndDate( midnightZulu ), + expected + ); + } + ); QUnit.test( 'formatTime', ( assert ) => { const { formatTime } = DateFormatter; @@ -188,9 +213,18 @@ QUnit.module( 'mediawiki.DateFormatter static functions', ( hooks ) => { } ); QUnit.module( 'mediawiki.DateFormatter instance methods', ( hooks ) => { + let userOptions; + + function fakeOptionsGet( key, fallback ) { + userOptions = { + timecorrection: 'Offset|60' + }; + return key in userOptions ? userOptions[ key ] : fallback; + } hooks.beforeEach( function () { this.sandbox.stub( mw.user.options, 'get', fakeOptionsGet ); + DateFormatter.clearInstanceCache(); } ); function getInstance() { -- cgit v1.2.3