Use different formatter for different currency (#1432)

* Use different formatter for different currency

* Add functional tests for currency fraction digit format

* Revert "Add functional tests for currency fraction digit format"

This reverts commit bd8aab33847425f4dcfd0d76ce310c918729b2fd.

* Add TestCurrencyFormattingLogic in UnitConverterViewModelUnitTests

* Fix InitializeMultipleConverterTest

* Add comment for a line of code

* Add default case for switch in ConvertToLocalizedString

* Remove trailing decimal
Disable decimal input if maxFractionDigits is 0
Fix input may be blocked after switched active

* Fix: UpdateIsDecimalEnabled should do nothing for non-currency converter

* Remove unnecessary SetValue method

* Add a comment

* Add functional UI Tests for currency converter
Reset currency before tests
Fix: input is blocked after switching to currency with less fractional digits

* Set Priority=0 for currency format related tests

* Truncate digits in display value after switcing
To fix incorrect result after switching currency with less fractional digits
This commit is contained in:
Hongxu Xu 2020-12-03 03:04:22 +08:00 committed by GitHub
parent 6359a14a9b
commit 61d06b2d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 432 additions and 43 deletions

View File

@ -24,7 +24,7 @@ static constexpr uint32_t OPTIMALDIGITSALLOWED = 7U;
static constexpr wchar_t LEFTESCAPECHAR = L'{'; static constexpr wchar_t LEFTESCAPECHAR = L'{';
static constexpr wchar_t RIGHTESCAPECHAR = L'}'; static constexpr wchar_t RIGHTESCAPECHAR = L'}';
static const double OPTIMALDECIMALALLOWED = 1e-6; // pow(10, -1 * (OPTIMALDIGITSALLOWED - 1)); static const double OPTIMALDECIMALALLOWED = 1e-6; // pow(10, -1 * (OPTIMALDIGITSALLOWED - 1));
static const double MINIMUMDECIMALALLOWED = 1e-14; // pow(10, -1 * (MAXIMUMDIGITSALLOWED - 1)); static const double MINIMUMDECIMALALLOWED = 1e-14; // pow(10, -1 * (MAXIMUMDIGITSALLOWED - 1));
unordered_map<wchar_t, wstring> quoteConversions; unordered_map<wchar_t, wstring> quoteConversions;
@ -149,6 +149,11 @@ void UnitConverter::SetCurrentUnitTypes(const Unit& fromType, const Unit& toType
return; return;
} }
if (m_fromType != fromType)
{
m_switchedActive = true;
}
m_fromType = fromType; m_fromType = fromType;
m_toType = toType; m_toType = toType;
Calculate(); Calculate();
@ -191,6 +196,11 @@ void UnitConverter::SwitchActive(const wstring& newValue)
} }
} }
bool UnitConversionManager::UnitConverter::IsSwitchedActive() const
{
return m_switchedActive;
}
wstring UnitConverter::CategoryToString(const Category& c, wstring_view delimiter) wstring UnitConverter::CategoryToString(const Category& c, wstring_view delimiter)
{ {
return Quote(std::to_wstring(c.id)) return Quote(std::to_wstring(c.id))

View File

@ -223,6 +223,7 @@ namespace UnitConversionManager
virtual Category GetCurrentCategory() = 0; virtual Category GetCurrentCategory() = 0;
virtual void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) = 0; virtual void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) = 0;
virtual void SwitchActive(const std::wstring& newValue) = 0; virtual void SwitchActive(const std::wstring& newValue) = 0;
virtual bool IsSwitchedActive() const = 0;
virtual std::wstring SaveUserPreferences() = 0; virtual std::wstring SaveUserPreferences() = 0;
virtual void RestoreUserPreferences(_In_ std::wstring_view userPreferences) = 0; virtual void RestoreUserPreferences(_In_ std::wstring_view userPreferences) = 0;
virtual void SendCommand(Command command) = 0; virtual void SendCommand(Command command) = 0;
@ -246,6 +247,7 @@ namespace UnitConversionManager
Category GetCurrentCategory() override; Category GetCurrentCategory() override;
void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) override; void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) override;
void SwitchActive(const std::wstring& newValue) override; void SwitchActive(const std::wstring& newValue) override;
bool IsSwitchedActive() const override;
std::wstring SaveUserPreferences() override; std::wstring SaveUserPreferences() override;
void RestoreUserPreferences(std::wstring_view userPreference) override; void RestoreUserPreferences(std::wstring_view userPreference) override;
void SendCommand(Command command) override; void SendCommand(Command command) override;

View File

@ -134,7 +134,6 @@ UnitConverterViewModel::UnitConverterViewModel(const shared_ptr<UCM::IUnitConver
m_currencyFormatter->IsGrouped = true; m_currencyFormatter->IsGrouped = true;
m_currencyFormatter->Mode = CurrencyFormatterMode::UseCurrencyCode; m_currencyFormatter->Mode = CurrencyFormatterMode::UseCurrencyCode;
m_currencyFormatter->ApplyRoundingForCurrency(RoundingAlgorithm::RoundHalfDown); m_currencyFormatter->ApplyRoundingForCurrency(RoundingAlgorithm::RoundHalfDown);
m_currencyMaxFractionDigits = m_currencyFormatter->FractionDigits;
auto resourceLoader = AppResourceProvider::GetInstance(); auto resourceLoader = AppResourceProvider::GetInstance();
m_localizedValueFromFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ValueFromFormat); m_localizedValueFromFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ValueFromFormat);
@ -228,7 +227,9 @@ void UnitConverterViewModel::OnUnitChanged(Object ^ parameter)
return; return;
} }
UpdateCurrencyFormatter();
m_model->SetCurrentUnitTypes(UnitFrom->GetModelUnit(), UnitTo->GetModelUnit()); m_model->SetCurrentUnitTypes(UnitFrom->GetModelUnit(), UnitTo->GetModelUnit());
if (m_supplementaryResultsTimer != nullptr) if (m_supplementaryResultsTimer != nullptr)
{ {
// End timer to show results immediately // End timer to show results immediately
@ -246,7 +247,7 @@ void UnitConverterViewModel::OnSwitchActive(Platform::Object ^ unused)
if (m_relocalizeStringOnSwitch) if (m_relocalizeStringOnSwitch)
{ {
// clean up any ill-formed strings that were in progress before the switch // clean up any ill-formed strings that were in progress before the switch
ValueFrom = ConvertToLocalizedString(m_valueFromUnlocalized, false); ValueFrom = ConvertToLocalizedString(m_valueFromUnlocalized, false, CurrencyFormatterParameterFrom);
} }
SwitchConversionParameters(); SwitchConversionParameters();
@ -269,9 +270,11 @@ void UnitConverterViewModel::OnSwitchActive(Platform::Object ^ unused)
m_isInputBlocked = false; m_isInputBlocked = false;
m_model->SwitchActive(m_valueFromUnlocalized); m_model->SwitchActive(m_valueFromUnlocalized);
UpdateIsDecimalEnabled();
} }
String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings) String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings, CurrencyFormatterParameter cfp)
{ {
Platform::String ^ result; Platform::String ^ result;
@ -280,10 +283,33 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st
return result; return result;
} }
CurrencyFormatter ^ currencyFormatter;
switch (cfp)
{
case CurrencyFormatterParameter::ForValue1:
currencyFormatter = m_currencyFormatter1;
break;
case CurrencyFormatterParameter::ForValue2:
currencyFormatter = m_currencyFormatter2;
break;
default:
currencyFormatter = m_currencyFormatter;
break;
}
// If unit hasn't been set, currencyFormatter1/2 is nullptr. Fallback to default.
if (currencyFormatter == nullptr)
{
currencyFormatter = m_currencyFormatter;
}
int lastCurrencyFractionDigits = currencyFormatter->FractionDigits;
m_decimalFormatter->IsDecimalPointAlwaysDisplayed = false; m_decimalFormatter->IsDecimalPointAlwaysDisplayed = false;
m_decimalFormatter->FractionDigits = 0; m_decimalFormatter->FractionDigits = 0;
m_currencyFormatter->IsDecimalPointAlwaysDisplayed = false; currencyFormatter->IsDecimalPointAlwaysDisplayed = false;
m_currencyFormatter->FractionDigits = 0; currencyFormatter->FractionDigits = 0;
wstring::size_type posOfE = stringToLocalize.find(L'e'); wstring::size_type posOfE = stringToLocalize.find(L'e');
if (posOfE != wstring::npos) if (posOfE != wstring::npos)
@ -293,7 +319,8 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st
std::wstring significandStr(stringToLocalize.substr(0, posOfE)); std::wstring significandStr(stringToLocalize.substr(0, posOfE));
std::wstring exponentStr(stringToLocalize.substr(posOfSign + 1, stringToLocalize.length() - posOfSign)); std::wstring exponentStr(stringToLocalize.substr(posOfSign + 1, stringToLocalize.length() - posOfSign));
result += ConvertToLocalizedString(significandStr, allowPartialStrings) + "e" + signOfE + ConvertToLocalizedString(exponentStr, allowPartialStrings); result += ConvertToLocalizedString(significandStr, allowPartialStrings, cfp) + "e" + signOfE
+ ConvertToLocalizedString(exponentStr, allowPartialStrings, cfp);
} }
else else
{ {
@ -304,7 +331,7 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st
if (hasDecimal) if (hasDecimal)
{ {
if (allowPartialStrings) if (allowPartialStrings && lastCurrencyFractionDigits > 0)
{ {
// allow "in progress" strings, like "3." that occur during the composition of // allow "in progress" strings, like "3." that occur during the composition of
// a final number. Without this, when typing the three characters in "3.2" // a final number. Without this, when typing the three characters in "3.2"
@ -312,18 +339,18 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st
// typed a post-decimal digit. // typed a post-decimal digit.
m_decimalFormatter->IsDecimalPointAlwaysDisplayed = true; m_decimalFormatter->IsDecimalPointAlwaysDisplayed = true;
m_currencyFormatter->IsDecimalPointAlwaysDisplayed = true; currencyFormatter->IsDecimalPointAlwaysDisplayed = true;
} }
// force post-decimal digits so that trailing zeroes entered by the user aren't suddenly cut off. // force post-decimal digits so that trailing zeroes entered by the user aren't suddenly cut off.
m_decimalFormatter->FractionDigits = static_cast<int>(stringToLocalize.length() - (posOfDecimal + 1)); m_decimalFormatter->FractionDigits = static_cast<int>(stringToLocalize.length() - (posOfDecimal + 1));
m_currencyFormatter->FractionDigits = m_currencyMaxFractionDigits; currencyFormatter->FractionDigits = lastCurrencyFractionDigits;
} }
if (IsCurrencyCurrentCategory) if (IsCurrencyCurrentCategory)
{ {
wstring currencyResult = m_currencyFormatter->Format(stod(stringToLocalize))->Data(); wstring currencyResult = currencyFormatter->Format(stod(stringToLocalize))->Data();
wstring currencyCode = m_currencyFormatter->Currency->Data(); wstring currencyCode = currencyFormatter->Currency->Data();
// CurrencyFormatter always includes LangCode or Symbol. Make it include LangCode // CurrencyFormatter always includes LangCode or Symbol. Make it include LangCode
// because this includes a non-breaking space. Remove the LangCode. // because this includes a non-breaking space. Remove the LangCode.
@ -381,6 +408,10 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st
} }
result = L"-" + result; result = L"-" + result;
} }
// restore the original fraction digits
currencyFormatter->FractionDigits = lastCurrencyFractionDigits;
return result; return result;
} }
@ -394,9 +425,9 @@ void UnitConverterViewModel::DisplayPasteError()
void UnitConverterViewModel::UpdateDisplay(const wstring& from, const wstring& to) void UnitConverterViewModel::UpdateDisplay(const wstring& from, const wstring& to)
{ {
String ^ fromStr = this->ConvertToLocalizedString(from, true); String ^ fromStr = this->ConvertToLocalizedString(from, true, CurrencyFormatterParameterFrom);
UpdateInputBlocked(from); UpdateInputBlocked(from);
String ^ toStr = this->ConvertToLocalizedString(to, true); String ^ toStr = this->ConvertToLocalizedString(to, true, CurrencyFormatterParameterTo);
bool updatedValueFrom = ValueFrom != fromStr; bool updatedValueFrom = ValueFrom != fromStr;
bool updatedValueTo = ValueTo != toStr; bool updatedValueTo = ValueTo != toStr;
@ -473,14 +504,14 @@ void UnitConverterViewModel::OnButtonPressed(Platform::Object ^ parameter)
} }
static constexpr UCM::Command OPERANDS[] = { UCM::Command::Zero, UCM::Command::One, UCM::Command::Two, UCM::Command::Three, UCM::Command::Four, static constexpr UCM::Command OPERANDS[] = { UCM::Command::Zero, UCM::Command::One, UCM::Command::Two, UCM::Command::Three, UCM::Command::Four,
UCM::Command::Five, UCM::Command::Six, UCM::Command::Seven, UCM::Command::Eight, UCM::Command::Nine }; UCM::Command::Five, UCM::Command::Six, UCM::Command::Seven, UCM::Command::Eight, UCM::Command::Nine };
if (m_isInputBlocked &&
command != UCM::Command::Clear && // input should be allowed if user just switches active, because we will clear values in such cases
command != UCM::Command::Backspace) if (m_isInputBlocked && !m_model->IsSwitchedActive() && command != UCM::Command::Clear && command != UCM::Command::Backspace)
{ {
return; return;
} }
m_model->SendCommand(command); m_model->SendCommand(command);
TraceLogger::GetInstance()->LogConverterInputReceived(Mode); TraceLogger::GetInstance()->LogConverterInputReceived(Mode);
} }
@ -755,8 +786,8 @@ void UnitConverterViewModel::RefreshSupplementaryResults()
for (tuple<wstring, UCM::Unit> suggestedValue : m_cachedSuggestedValues) for (tuple<wstring, UCM::Unit> suggestedValue : m_cachedSuggestedValues)
{ {
SupplementaryResult ^ result = SupplementaryResult ^ result = ref new SupplementaryResult(
ref new SupplementaryResult(this->ConvertToLocalizedString(get<0>(suggestedValue), false), ref new Unit(get<1>(suggestedValue))); this->ConvertToLocalizedString(get<0>(suggestedValue), false, CurrencyFormatterParameter::Default), ref new Unit(get<1>(suggestedValue)));
if (result->IsWhimsical()) if (result->IsWhimsical())
{ {
whimsicals.push_back(result); whimsicals.push_back(result);
@ -803,10 +834,46 @@ void UnitConverterViewModel::UpdateInputBlocked(_In_ const wstring& currencyInpu
m_isInputBlocked = false; m_isInputBlocked = false;
if (posOfDecimal != wstring::npos && IsCurrencyCurrentCategory) if (posOfDecimal != wstring::npos && IsCurrencyCurrentCategory)
{ {
m_isInputBlocked = (posOfDecimal + static_cast<size_t>(m_currencyMaxFractionDigits) + 1 == currencyInput.length()); m_isInputBlocked = (posOfDecimal + static_cast<size_t>(CurrencyFormatterFrom->FractionDigits) + 1 == currencyInput.length());
} }
} }
std::wstring TruncateFractionDigits(const std::wstring& n, int digitCount)
{
auto i = n.find('.');
if (i == std::wstring::npos)
return n;
size_t actualDigitCount = n.size() - i - 1;
return n.substr(0, n.size() - (actualDigitCount - digitCount));
}
void UnitConverterViewModel::UpdateCurrencyFormatter()
{
if (!IsCurrencyCurrentCategory || m_Unit1->Abbreviation->IsEmpty() || m_Unit2->Abbreviation->IsEmpty())
return;
m_currencyFormatter1 = ref new CurrencyFormatter(m_Unit1->Abbreviation);
m_currencyFormatter1->IsGrouped = true;
m_currencyFormatter1->Mode = CurrencyFormatterMode::UseCurrencyCode;
m_currencyFormatter1->ApplyRoundingForCurrency(RoundingAlgorithm::RoundHalfDown);
m_currencyFormatter2 = ref new CurrencyFormatter(m_Unit2->Abbreviation);
m_currencyFormatter2->IsGrouped = true;
m_currencyFormatter2->Mode = CurrencyFormatterMode::UseCurrencyCode;
m_currencyFormatter2->ApplyRoundingForCurrency(RoundingAlgorithm::RoundHalfDown);
UpdateIsDecimalEnabled();
OnPaste(ref new String(TruncateFractionDigits(m_valueFromUnlocalized, CurrencyFormatterFrom->FractionDigits).data()));
}
void UnitConverterViewModel::UpdateIsDecimalEnabled()
{
if (!IsCurrencyCurrentCategory || CurrencyFormatterFrom == nullptr)
return;
IsDecimalEnabled = CurrencyFormatterFrom->FractionDigits > 0;
}
NumbersAndOperatorsEnum UnitConverterViewModel::MapCharacterToButtonId(const wchar_t ch, bool& canSendNegate) NumbersAndOperatorsEnum UnitConverterViewModel::MapCharacterToButtonId(const wchar_t ch, bool& canSendNegate)
{ {
static_assert(NumbersAndOperatorsEnum::Zero < NumbersAndOperatorsEnum::One, "NumbersAndOperatorsEnum order is invalid"); static_assert(NumbersAndOperatorsEnum::Zero < NumbersAndOperatorsEnum::One, "NumbersAndOperatorsEnum order is invalid");
@ -934,14 +1001,19 @@ void UnitConverterViewModel::OnPaste(String ^ stringToPaste)
} }
} }
String ^ UnitConverterViewModel::GetLocalizedAutomationName(_In_ String ^ displayvalue, _In_ String ^ unitname, _In_ String ^ format) String
^ UnitConverterViewModel::GetLocalizedAutomationName(
_In_ String ^ displayvalue,
_In_ String ^ unitname,
_In_ String ^ format,
_In_ CurrencyFormatterParameter cfp)
{ {
String ^ valueToLocalize = displayvalue; String ^ valueToLocalize = displayvalue;
if (displayvalue == ValueFrom && Utils::IsLastCharacterTarget(m_valueFromUnlocalized, m_decimalSeparator)) if (displayvalue == ValueFrom && Utils::IsLastCharacterTarget(m_valueFromUnlocalized, m_decimalSeparator))
{ {
// Need to compute a second localized value for the automation // Need to compute a second localized value for the automation
// name that does not include the decimal separator. // name that does not include the decimal separator.
displayvalue = ConvertToLocalizedString(m_valueFromUnlocalized, false /*allowTrailingDecimal*/); displayvalue = ConvertToLocalizedString(m_valueFromUnlocalized, false /*allowTrailingDecimal*/, cfp);
format = m_localizedValueFromDecimalFormat; format = m_localizedValueFromDecimalFormat;
} }
@ -962,7 +1034,7 @@ void UnitConverterViewModel::UpdateValue1AutomationName()
{ {
if (Unit1) if (Unit1)
{ {
Value1AutomationName = GetLocalizedAutomationName(Value1, Unit1->AccessibleName, m_localizedValueFromFormat); Value1AutomationName = GetLocalizedAutomationName(Value1, Unit1->AccessibleName, m_localizedValueFromFormat, CurrencyFormatterParameter::ForValue1);
} }
} }
@ -970,7 +1042,7 @@ void UnitConverterViewModel::UpdateValue2AutomationName()
{ {
if (Unit2) if (Unit2)
{ {
Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2->AccessibleName, m_localizedValueToFormat); Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2->AccessibleName, m_localizedValueToFormat, CurrencyFormatterParameter::ForValue1);
} }
} }

View File

@ -227,8 +227,19 @@ namespace CalculatorApp
void OnCopyCommand(Platform::Object ^ parameter); void OnCopyCommand(Platform::Object ^ parameter);
void OnPasteCommand(Platform::Object ^ parameter); void OnPasteCommand(Platform::Object ^ parameter);
enum class CurrencyFormatterParameter
{
Default,
ForValue1,
ForValue2,
};
Platform::String Platform::String
^ GetLocalizedAutomationName(_In_ Platform::String ^ displayvalue, _In_ Platform::String ^ unitname, _In_ Platform::String ^ format); ^ GetLocalizedAutomationName(
_In_ Platform::String ^ displayvalue,
_In_ Platform::String ^ unitname,
_In_ Platform::String ^ format,
_In_ CurrencyFormatterParameter cfp);
Platform::String Platform::String
^ GetLocalizedConversionResultStringFormat( ^ GetLocalizedConversionResultStringFormat(
_In_ Platform::String ^ fromValue, _In_ Platform::String ^ fromValue,
@ -276,11 +287,13 @@ namespace CalculatorApp
void SupplementaryResultsTimerCancel(Windows::System::Threading::ThreadPoolTimer ^ timer); void SupplementaryResultsTimerCancel(Windows::System::Threading::ThreadPoolTimer ^ timer);
void RefreshSupplementaryResults(); void RefreshSupplementaryResults();
void UpdateInputBlocked(_In_ const std::wstring& currencyInput); void UpdateInputBlocked(_In_ const std::wstring& currencyInput);
void UpdateCurrencyFormatter();
void UpdateIsDecimalEnabled();
bool UnitsAreValid(); bool UnitsAreValid();
void ResetCategory(); void ResetCategory();
void OnButtonPressed(Platform::Object ^ parameter); void OnButtonPressed(Platform::Object ^ parameter);
Platform::String ^ ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings); Platform::String ^ ConvertToLocalizedString(const std::wstring& stringToLocalize, bool allowPartialStrings, CurrencyFormatterParameter cfp);
std::shared_ptr<UnitConversionManager::IUnitConverter> m_model; std::shared_ptr<UnitConversionManager::IUnitConverter> m_model;
wchar_t m_decimalSeparator; wchar_t m_decimalSeparator;
@ -290,6 +303,34 @@ namespace CalculatorApp
Source, Source,
Target Target
} m_value1cp; } m_value1cp;
property CurrencyFormatterParameter CurrencyFormatterParameterFrom
{
CurrencyFormatterParameter get()
{
return m_value1cp == ConversionParameter::Source ? CurrencyFormatterParameter::ForValue1 : CurrencyFormatterParameter::ForValue2;
}
}
property CurrencyFormatterParameter CurrencyFormatterParameterTo
{
CurrencyFormatterParameter get()
{
return m_value1cp == ConversionParameter::Target ? CurrencyFormatterParameter::ForValue1 : CurrencyFormatterParameter::ForValue2;
}
}
property Windows::Globalization::NumberFormatting::CurrencyFormatter^ CurrencyFormatterFrom
{
Windows::Globalization::NumberFormatting::CurrencyFormatter^ get()
{
return m_value1cp == ConversionParameter::Source ? m_currencyFormatter1 : m_currencyFormatter2;
}
}
property Windows::Globalization::NumberFormatting::CurrencyFormatter^ CurrencyFormatterTo
{
Windows::Globalization::NumberFormatting::CurrencyFormatter^ get()
{
return m_value1cp == ConversionParameter::Target ? m_currencyFormatter1 : m_currencyFormatter2;
}
}
property Platform::String^ ValueFrom property Platform::String^ ValueFrom
{ {
Platform::String^ get() { return m_value1cp == ConversionParameter::Source ? Value1 : Value2; } Platform::String^ get() { return m_value1cp == ConversionParameter::Source ? Value1 : Value2; }
@ -323,7 +364,8 @@ namespace CalculatorApp
std::mutex m_cacheMutex; std::mutex m_cacheMutex;
Windows::Globalization::NumberFormatting::DecimalFormatter ^ m_decimalFormatter; Windows::Globalization::NumberFormatting::DecimalFormatter ^ m_decimalFormatter;
Windows::Globalization::NumberFormatting::CurrencyFormatter ^ m_currencyFormatter; Windows::Globalization::NumberFormatting::CurrencyFormatter ^ m_currencyFormatter;
int m_currencyMaxFractionDigits; Windows::Globalization::NumberFormatting::CurrencyFormatter ^ m_currencyFormatter1;
Windows::Globalization::NumberFormatting::CurrencyFormatter ^ m_currencyFormatter2;
std::wstring m_valueFromUnlocalized; std::wstring m_valueFromUnlocalized;
std::wstring m_valueToUnlocalized; std::wstring m_valueToUnlocalized;
bool m_relocalizeStringOnSwitch; bool m_relocalizeStringOnSwitch;

View File

@ -35,6 +35,14 @@ public static void EnsureCalculatorHasFocus()
AppName.Click(); AppName.Click();
} }
/// <summary>
/// Click the window (to lose focus on components)
/// </summary>
public static void ClickOnWindow()
{
Window.Click();
}
/// <summary> /// <summary>
/// If the the Dock Panel for the History and Memory lists is not displayed, resize the window /// If the the Dock Panel for the History and Memory lists is not displayed, resize the window
/// Two attempts are made, the the lable is not found a "not found" exception is thrown /// Two attempts are made, the the lable is not found a "not found" exception is thrown

View File

@ -13,5 +13,7 @@ public class UnitConverterOperatorsPanel
public NumberPad NumberPad = new NumberPad(); public NumberPad NumberPad = new NumberPad();
public WindowsElement ClearButton => this.session.TryFindElementByAccessibilityId("ClearEntryButtonPos0"); public WindowsElement ClearButton => this.session.TryFindElementByAccessibilityId("ClearEntryButtonPos0");
public WindowsElement BackSpaceButton => this.session.TryFindElementByAccessibilityId("BackSpaceButtonSmall"); public WindowsElement BackSpaceButton => this.session.TryFindElementByAccessibilityId("BackSpaceButtonSmall");
public WindowsElement Units1 => this.session.TryFindElementByAccessibilityId("Units1");
public WindowsElement Units2 => this.session.TryFindElementByAccessibilityId("Units2");
} }
} }

View File

@ -64,5 +64,44 @@ public void EnsureCalculatorIsCurrencyMode()
} }
} }
/// <summary>
/// Ensure Units1 and Units2 are the same
/// </summary>
public void EnsureSameUnitsAreSelected()
{
CalculatorApp.ClickOnWindow();
UnitConverterOperators.Units1.SendKeys(OpenQA.Selenium.Keys.Home);
UnitConverterOperators.Units1.SendKeys(OpenQA.Selenium.Keys.Enter);
CalculatorApp.ClickOnWindow();
UnitConverterOperators.Units2.SendKeys(OpenQA.Selenium.Keys.Home);
UnitConverterOperators.Units2.SendKeys(OpenQA.Selenium.Keys.Enter);
CalculatorApp.ClickOnWindow();
}
/// <summary>
/// Select value in Units1 ComboBox
/// </summary>
/// <param name="value">Value in ComboBox Units1</param>
public void SelectUnits1(string value)
{
CalculatorApp.ClickOnWindow();
UnitConverterOperators.Units1.SendKeys(value);
UnitConverterOperators.Units1.SendKeys(OpenQA.Selenium.Keys.Enter);
CalculatorApp.ClickOnWindow();
}
/// <summary>
/// Select value in Units2 ComboBox
/// </summary>
/// <param name="value">Value in ComboBox Units2</param>
public void SelectUnits2(string value)
{
CalculatorApp.ClickOnWindow();
UnitConverterOperators.Units2.SendKeys(value);
UnitConverterOperators.Units2.SendKeys(OpenQA.Selenium.Keys.Enter);
CalculatorApp.ClickOnWindow();
}
} }
} }

View File

@ -8,5 +8,7 @@
</DataCollectionRunSettings> </DataCollectionRunSettings>
<TestRunParameters> <TestRunParameters>
<Parameter Name="AppId" Value="Microsoft.WindowsCalculator.Dev_8wekyb3d8bbwe!App" /> <Parameter Name="AppId" Value="Microsoft.WindowsCalculator.Dev_8wekyb3d8bbwe!App" />
<Parameter Name="CurrencyWith3FractionalDigits" Value="Test Fractional Digits - Test Fractional Digits" />
<Parameter Name="CurrencyWithoutFractionalDigits" Value="Test No Fractional Digits - Test No Fractional Digits" />
</TestRunParameters> </TestRunParameters>
</RunSettings> </RunSettings>

View File

@ -8,5 +8,7 @@
</DataCollectionRunSettings> </DataCollectionRunSettings>
<TestRunParameters> <TestRunParameters>
<Parameter Name="AppId" Value="Microsoft.WindowsCalculator_8wekyb3d8bbwe!App" /> <Parameter Name="AppId" Value="Microsoft.WindowsCalculator_8wekyb3d8bbwe!App" />
<Parameter Name="CurrencyWith3FractionalDigits" Value="Jordan - Dinar" />
<Parameter Name="CurrencyWithoutFractionalDigits" Value="Japan - Yen" />
</TestRunParameters> </TestRunParameters>
</RunSettings> </RunSettings>

View File

@ -12,6 +12,8 @@ public class CurrencyConverterFunctionalTests
{ {
private static UnitConverterPage page = new UnitConverterPage(); private static UnitConverterPage page = new UnitConverterPage();
public TestContext TestContext { get; set; }
/// <summary> /// <summary>
/// Initializes the WinAppDriver web driver session. /// Initializes the WinAppDriver web driver session.
/// </summary> /// </summary>
@ -42,6 +44,7 @@ public void TestInit()
CalculatorApp.EnsureCalculatorHasFocus(); CalculatorApp.EnsureCalculatorHasFocus();
page.EnsureCalculatorIsCurrencyMode(); page.EnsureCalculatorIsCurrencyMode();
page.EnsureCalculatorResultTextIsZero(); page.EnsureCalculatorResultTextIsZero();
page.EnsureSameUnitsAreSelected();
} }
[TestCleanup] [TestCleanup]
@ -50,6 +53,23 @@ public void TestCleanup()
page.ClearAll(); page.ClearAll();
} }
private string NormalizeCurrencyText(string realValue, int fractionDigits)
{
if (!realValue.Contains('.')) return realValue;
var parts = realValue.Split('.');
if (parts[1].Length < fractionDigits)
{
parts[1] += new string('0', fractionDigits - parts[1].Length);
}
else
{
parts[1] = parts[1].Substring(0, fractionDigits);
}
return $"{parts[0]}.{parts[1]}".TrimEnd('.');
}
#region Basic UI Functionality via Mouse Input Tests #region Basic UI Functionality via Mouse Input Tests
/// <summary> /// <summary>
/// These automated tests verify clicking each of the buttons in the Calculator UI and getting an expected result /// These automated tests verify clicking each of the buttons in the Calculator UI and getting an expected result
@ -129,6 +149,153 @@ public void MouseInput_EnterInputWithFullDecimalAndClearWithBackspace()
Assert.AreEqual("0", page.UnitConverterResults.GetCalculationResult2Text()); //verifies Backspace button clicks Assert.AreEqual("0", page.UnitConverterResults.GetCalculationResult2Text()); //verifies Backspace button clicks
} }
/// <summary>
/// These automated tests verify the currency has been formatted to 3 fractional digits
/// Via mouse input, all basic UI functionality is checked
/// </summary>
[TestMethod]
[Priority(0)]
public void MouseInput_SelectCurrencyWith3FractionalDigitsEnterInputAndCheckTheFormat()
{
var currency = (string)TestContext.Properties["CurrencyWith3FractionalDigits"];
var fractionDigits = 3;
//Verifies fraction digits in given currency
page.SelectUnits1(currency);
Assert.AreEqual(currency.Replace(" - ", " "), page.UnitConverterOperators.Units1.Text); // Text is the AccessibleName of Unit
page.UnitConverterOperators.NumberPad.Num2Button.Click();
Assert.AreEqual(NormalizeCurrencyText("2", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 2 button
page.UnitConverterOperators.NumberPad.DecimalButton.Click();
Assert.AreEqual(NormalizeCurrencyText("2.", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies decimal button
page.UnitConverterOperators.NumberPad.Num4Button.Click();
Assert.AreEqual(NormalizeCurrencyText("2.4", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 4 button
page.UnitConverterOperators.NumberPad.Num3Button.Click();
Assert.AreEqual(NormalizeCurrencyText("2.43", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 3 button
page.UnitConverterOperators.NumberPad.Num5Button.Click();
Assert.AreEqual(NormalizeCurrencyText("2.435", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 5 button
page.UnitConverterOperators.NumberPad.Num6Button.Click();
Assert.AreEqual(NormalizeCurrencyText("2.4356", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 6 button
}
/// <summary>
/// These automated tests verify the currency has been formatted to no fractional digit
/// Via mouse input, all basic UI functionality is checked
/// </summary>
[TestMethod]
[Priority(0)]
public void MouseInput_SelectCurrencyWithoutFractionalDigitEnterInputAndCheckTheFormat()
{
var currency = (string)TestContext.Properties["CurrencyWithoutFractionalDigits"];
var fractionDigits = 0;
//Verifies fraction digits in given currency
page.SelectUnits1(currency);
Assert.AreEqual(currency.Replace(" - ", " "), page.UnitConverterOperators.Units1.Text); // Text is the AccessibleName of Unit
page.UnitConverterOperators.NumberPad.Num2Button.Click();
Assert.AreEqual(NormalizeCurrencyText("2", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 2 button
page.UnitConverterOperators.NumberPad.DecimalButton.Click(); // It should be disabled, so no decimal will be displayed
Assert.AreEqual(NormalizeCurrencyText("2", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies decimal button
page.UnitConverterOperators.NumberPad.Num4Button.Click(); // As decimal is disabled, 4 won't be part of fraction digits
Assert.AreEqual(NormalizeCurrencyText("24", fractionDigits), page.UnitConverterResults.GetCalculationResult1Text()); //verifies 4 button
}
/// <summary>
/// These automated tests verify the currency format is updated after switching
/// Via mouse input, all basic UI functionality is checked
/// </summary>
[TestMethod]
[Priority(0)]
public void MouseInput_SwitchCurrencyWithDifferentFractionalDigitsAndCheckTheFormat()
{
var currencyWith3FractionalDigits = (string)TestContext.Properties["CurrencyWith3FractionalDigits"];
var currencyWithoutFractionalDigits = (string)TestContext.Properties["CurrencyWithoutFractionalDigits"];
//Verifies fraction digits in given currency
page.SelectUnits1(currencyWith3FractionalDigits);
page.UnitConverterOperators.NumberPad.Num2Button.Click();
page.UnitConverterOperators.NumberPad.DecimalButton.Click();
page.UnitConverterOperators.NumberPad.Num4Button.Click();
page.UnitConverterOperators.NumberPad.Num3Button.Click();
page.UnitConverterOperators.NumberPad.Num5Button.Click();
Assert.AreEqual("2.435", page.UnitConverterResults.GetCalculationResult1Text());
page.SelectUnits1(currencyWithoutFractionalDigits);
Assert.AreEqual("2", page.UnitConverterResults.GetCalculationResult1Text());
// The digits will be truncated forever, even if swiching back
page.SelectUnits1(currencyWith3FractionalDigits);
Assert.AreEqual("2", page.UnitConverterResults.GetCalculationResult1Text());
}
/// <summary>
/// These automated tests verify input is not blocked after swiching to currency with less fractional digits
/// Via mouse input, all basic UI functionality is checked
/// </summary>
[TestMethod]
[Priority(0)]
public void MouseInput_SwitchCurrencyWithLessFractionalDigitsAndEnterInput()
{
var currencyWith3FractionalDigits = (string)TestContext.Properties["CurrencyWith3FractionalDigits"];
var currencyWithoutFractionalDigits = (string)TestContext.Properties["CurrencyWithoutFractionalDigits"];
//Verifies fraction digits in given currency
page.SelectUnits1(currencyWith3FractionalDigits);
page.UnitConverterOperators.NumberPad.Num2Button.Click();
page.UnitConverterOperators.NumberPad.DecimalButton.Click();
page.UnitConverterOperators.NumberPad.Num4Button.Click();
page.UnitConverterOperators.NumberPad.Num3Button.Click();
page.UnitConverterOperators.NumberPad.Num5Button.Click();
Assert.AreEqual("2.435", page.UnitConverterResults.GetCalculationResult1Text());
page.SelectUnits1(currencyWithoutFractionalDigits);
Assert.AreEqual("2", page.UnitConverterResults.GetCalculationResult1Text());
page.UnitConverterOperators.NumberPad.Num4Button.Click(); // Enter new number will replace the previous value
Assert.AreEqual("4", page.UnitConverterResults.GetCalculationResult1Text());
page.SelectUnits1(currencyWith3FractionalDigits);
Assert.AreEqual("4", page.UnitConverterResults.GetCalculationResult1Text());
}
/// <summary>
/// These automated tests verify the result consists after swiching currency
/// Via mouse input, all basic UI functionality is checked
/// </summary>
[TestMethod]
[Priority(0)]
public void MouseInput_SwitchCurrencyWithLessFractionalDigitsAndCheckIfTheResultIsConsistent()
{
var currencyWith3FractionalDigits = (string)TestContext.Properties["CurrencyWith3FractionalDigits"];
var currencyWithoutFractionalDigits = (string)TestContext.Properties["CurrencyWithoutFractionalDigits"];
page.SelectUnits1(currencyWith3FractionalDigits);
page.SelectUnits2(currencyWith3FractionalDigits);
page.UnitConverterOperators.NumberPad.Num2Button.Click();
page.UnitConverterOperators.NumberPad.Num0Button.Click();
page.UnitConverterOperators.NumberPad.Num0Button.Click();
page.UnitConverterOperators.NumberPad.DecimalButton.Click();
page.UnitConverterOperators.NumberPad.Num9Button.Click();
page.UnitConverterOperators.NumberPad.Num9Button.Click();
page.UnitConverterOperators.NumberPad.Num9Button.Click();
Assert.AreEqual("200.999", page.UnitConverterResults.GetCalculationResult1Text());
Assert.AreEqual("200.999", page.UnitConverterResults.GetCalculationResult2Text());
page.SelectUnits1(currencyWithoutFractionalDigits);
Assert.AreEqual("200", page.UnitConverterResults.GetCalculationResult1Text());
var result = page.UnitConverterResults.GetCalculationResult2Text();
page.UnitConverterOperators.ClearButton.Click();
page.UnitConverterOperators.NumberPad.Num2Button.Click();
page.UnitConverterOperators.NumberPad.Num0Button.Click();
page.UnitConverterOperators.NumberPad.Num0Button.Click();
Assert.AreEqual("200", page.UnitConverterResults.GetCalculationResult1Text());
Assert.AreEqual(result, page.UnitConverterResults.GetCalculationResult2Text());
}
#endregion #endregion
} }
} }

View File

@ -685,7 +685,7 @@ TEST_METHOD(InitializeMultipleConverterTest)
viewModels[i] = ref new UnitConverterViewModel(unitConverterMocks[i]); viewModels[i] = ref new UnitConverterViewModel(unitConverterMocks[i]);
IObservableVector<Category ^> ^ cats = viewModels[i]->Categories; IObservableVector<Category ^> ^ cats = viewModels[i]->Categories;
VERIFY_ARE_EQUAL((UINT)1, unitConverterMocks[i]->m_getCategoriesCallCount); VERIFY_ARE_EQUAL((UINT)1, unitConverterMocks[i]->m_getCategoriesCallCount);
VERIFY_ARE_EQUAL((UINT)3, cats->Size); VERIFY_ARE_EQUAL((UINT)4, cats->Size);
// Verify that we match current category // Verify that we match current category
VERIFY_IS_TRUE(CAT2 == viewModels[i]->CurrentCategory->GetModelCategory()); VERIFY_IS_TRUE(CAT2 == viewModels[i]->CurrentCategory->GetModelCategory());
} }
@ -697,7 +697,7 @@ TEST_METHOD(InitializeMultipleConverterTest)
// Verify that the instance properties were set independently // Verify that the instance properties were set independently
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
VERIFY_ARE_EQUAL((UINT)3, viewModels[i]->Categories->Size); VERIFY_ARE_EQUAL((UINT)4, viewModels[i]->Categories->Size);
VERIFY_ARE_EQUAL((UINT)3, viewModels[i]->Units->Size); VERIFY_ARE_EQUAL((UINT)3, viewModels[i]->Units->Size);
} }

View File

@ -140,6 +140,7 @@ vector<UCM::Category> UnitConverterMock::GetCategories()
cats.push_back(CAT1); cats.push_back(CAT1);
cats.push_back(CAT2); cats.push_back(CAT2);
cats.push_back(CAT3); cats.push_back(CAT3);
cats.push_back(CAT_CURRENCY);
m_curCategory = CAT2; m_curCategory = CAT2;
@ -174,6 +175,10 @@ UCM::CategorySelectionInitializer UnitConverterMock::SetCurrentCategory(const UC
units.push_back(UNIT9); units.push_back(UNIT9);
break; break;
} }
case CURRENCY_ID:
units.push_back(UNITJPY);
units.push_back(UNITJOD);
break;
default: default:
throw; throw;
} }
@ -215,10 +220,15 @@ void UnitConverterMock::SwitchActive(const std::wstring& newValue)
m_curValue = newValue; m_curValue = newValue;
} }
std::wstring UnitConverterMock::SaveUserPreferences() bool UnitConverterMock::IsSwitchedActive() const
{ {
return L"TEST"; return false;
}; }
std::wstring UnitConverterMock::SaveUserPreferences()
{
return L"TEST";
};
void UnitConverterMock::RestoreUserPreferences(_In_ std::wstring_view /*userPreferences*/){}; void UnitConverterMock::RestoreUserPreferences(_In_ std::wstring_view /*userPreferences*/){};
@ -341,7 +351,7 @@ TEST_METHOD(TestUnitConverterLoadSetsUpCategories)
VM::UnitConverterViewModel vm(mock); VM::UnitConverterViewModel vm(mock);
IObservableVector<VM::Category ^> ^ cats = vm.Categories; IObservableVector<VM::Category ^> ^ cats = vm.Categories;
VERIFY_ARE_EQUAL((UINT)1, mock->m_getCategoriesCallCount); VERIFY_ARE_EQUAL((UINT)1, mock->m_getCategoriesCallCount);
VERIFY_ARE_EQUAL((UINT)3, cats->Size); VERIFY_ARE_EQUAL((UINT)4, cats->Size);
// Verify that we match current category // Verify that we match current category
VERIFY_IS_TRUE(CAT2 == vm.CurrentCategory->GetModelCategory()); VERIFY_IS_TRUE(CAT2 == vm.CurrentCategory->GetModelCategory());
} }
@ -935,6 +945,33 @@ TEST_METHOD(TestDecimalFormattingLogic)
VERIFY_IS_TRUE(vm.Value1 == L"3"); VERIFY_IS_TRUE(vm.Value1 == L"3");
VERIFY_IS_TRUE(vm.Value2 == L"2.50"); VERIFY_IS_TRUE(vm.Value2 == L"2.50");
} }
TEST_METHOD(TestCurrencyFormattingLogic)
{
// verify that currency fraction digits is formatted per currency type
shared_ptr<UnitConverterMock> mock = make_shared<UnitConverterMock>();
VM::UnitConverterViewModel vm(mock);
// Establish base condition
vm.CurrentCategory = vm.Categories->GetAt(3); // Currency
vm.Unit1 = vm.Units->GetAt(0); // JPY
vm.Unit2 = vm.Units->GetAt(1); // JOD
vm.UnitChanged->Execute(nullptr);
const WCHAR *vFrom = L"1.2340", *vTo = L"0.0070";
vm.UpdateDisplay(vFrom, vTo);
VERIFY_IS_TRUE(vm.Value1 == L"1");
VERIFY_IS_TRUE(vm.Value2 == L"0.007");
vm.SwitchActive->Execute(nullptr);
VERIFY_IS_TRUE(vm.Value1 == L"1");
VERIFY_IS_TRUE(vm.Value2 == L"0.007");
vm.SwitchActive->Execute(nullptr);
VERIFY_IS_TRUE(vm.Value1 == L"1");
VERIFY_IS_TRUE(vm.Value2 == L"0.007");
}
// Tests that when we switch the active field and get display // Tests that when we switch the active field and get display
// updates, the correct automation names are are being updated. // updates, the correct automation names are are being updated.
TEST_METHOD(TestValue1AndValue2AutomationNameChanges) TEST_METHOD(TestValue1AndValue2AutomationNameChanges)

View File

@ -9,6 +9,8 @@ namespace UCM = UnitConversionManager;
namespace CalculatorUnitTests namespace CalculatorUnitTests
{ {
static constexpr int CURRENCY_ID = 16;
static UCM::Unit UNIT1 = { 1, L"UNIT1", L"U1", true, false, false }; static UCM::Unit UNIT1 = { 1, L"UNIT1", L"U1", true, false, false };
static UCM::Unit UNIT2 = { 2, L"UNIT2", L"U2", false, true, false }; static UCM::Unit UNIT2 = { 2, L"UNIT2", L"U2", false, true, false };
static UCM::Unit UNIT3 = { 3, L"UNIT3", L"U3", false, false, false }; static UCM::Unit UNIT3 = { 3, L"UNIT3", L"U3", false, false, false };
@ -19,10 +21,13 @@ namespace CalculatorUnitTests
static UCM::Unit UNIT8 = { 8, L"UNIT8", L"U8", false, false, false }; static UCM::Unit UNIT8 = { 8, L"UNIT8", L"U8", false, false, false };
static UCM::Unit UNIT9 = { 9, L"UNIT9", L"U9", true, false, false }; static UCM::Unit UNIT9 = { 9, L"UNIT9", L"U9", true, false, false };
static UCM::Unit UNITWHIMSY = { 10, L"Whimsy", L"UW", true, false, true }; static UCM::Unit UNITWHIMSY = { 10, L"Whimsy", L"UW", true, false, true };
static UCM::Unit UNITJPY = { 11, L"Japan - Yen", L"JPY", true, true, false };
static UCM::Unit UNITJOD = { 12, L"Jordan - Dinar", L"JOD", true, true, false };
static UCM::Category CAT1 = { 1, L"CAT1", false }; // contains Unit1 - Unit3 static UCM::Category CAT1 = { 1, L"CAT1", false }; // contains Unit1 - Unit3
static UCM::Category CAT2 = { 2, L"CAT2", false }; // contains Unit4 - Unit6 static UCM::Category CAT2 = { 2, L"CAT2", false }; // contains Unit4 - Unit6
static UCM::Category CAT3 = { 3, L"CAT3", false }; // contains Unit7 - Unit9 static UCM::Category CAT3 = { 3, L"CAT3", false }; // contains Unit7 - Unit9
static UCM::Category CAT_CURRENCY = { CURRENCY_ID, L"Currency", false }; // contains UnitJPY and UnitJOD
class UnitConverterMock : public UnitConversionManager::IUnitConverter class UnitConverterMock : public UnitConversionManager::IUnitConverter
{ {
@ -33,7 +38,8 @@ namespace CalculatorUnitTests
UCM::CategorySelectionInitializer SetCurrentCategory(const UCM::Category& input) override; UCM::CategorySelectionInitializer SetCurrentCategory(const UCM::Category& input) override;
UCM::Category GetCurrentCategory(); UCM::Category GetCurrentCategory();
void SetCurrentUnitTypes(const UCM::Unit& fromType, const UCM::Unit& toType) override; void SetCurrentUnitTypes(const UCM::Unit& fromType, const UCM::Unit& toType) override;
void SwitchActive(const std::wstring& newValue); void SwitchActive(const std::wstring& newValue) override;
bool IsSwitchedActive() const override;
std::wstring SaveUserPreferences() override; std::wstring SaveUserPreferences() override;
void RestoreUserPreferences(_In_ std::wstring_view userPreferences) override; void RestoreUserPreferences(_In_ std::wstring_view userPreferences) override;
void SendCommand(UCM::Command command) override; void SendCommand(UCM::Command command) override;