From 61d06b2d2fbb3770883a2dc898d9962d40a04a02 Mon Sep 17 00:00:00 2001 From: Hongxu Xu Date: Thu, 3 Dec 2020 03:04:22 +0800 Subject: [PATCH] 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 --- src/CalcManager/UnitConverter.cpp | 12 +- src/CalcManager/UnitConverter.h | 2 + src/CalcViewModel/UnitConverterViewModel.cpp | 128 +++++++++++--- src/CalcViewModel/UnitConverterViewModel.h | 48 ++++- .../CalculatorApp.cs | 8 + .../UnitConverterOperatorsPanel.cs | 2 + .../UnitConverterPage.cs | 39 ++++ .../CalculatorUITests.ci.runsettings | 2 + .../CalculatorUITests.release.runsettings | 2 + .../CurrencyConverterFunctionalTests.cs | 167 ++++++++++++++++++ .../MultiWindowUnitTests.cpp | 4 +- .../UnitConverterViewModelUnitTests.cpp | 47 ++++- .../UnitConverterViewModelUnitTests.h | 14 +- 13 files changed, 432 insertions(+), 43 deletions(-) diff --git a/src/CalcManager/UnitConverter.cpp b/src/CalcManager/UnitConverter.cpp index d760b61..3d4b186 100644 --- a/src/CalcManager/UnitConverter.cpp +++ b/src/CalcManager/UnitConverter.cpp @@ -24,7 +24,7 @@ static constexpr uint32_t OPTIMALDIGITSALLOWED = 7U; static constexpr wchar_t LEFTESCAPECHAR = 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)); unordered_map quoteConversions; @@ -149,6 +149,11 @@ void UnitConverter::SetCurrentUnitTypes(const Unit& fromType, const Unit& toType return; } + if (m_fromType != fromType) + { + m_switchedActive = true; + } + m_fromType = fromType; m_toType = toType; 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) { return Quote(std::to_wstring(c.id)) diff --git a/src/CalcManager/UnitConverter.h b/src/CalcManager/UnitConverter.h index 81a2629..fe085dc 100644 --- a/src/CalcManager/UnitConverter.h +++ b/src/CalcManager/UnitConverter.h @@ -223,6 +223,7 @@ namespace UnitConversionManager virtual Category GetCurrentCategory() = 0; virtual void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) = 0; virtual void SwitchActive(const std::wstring& newValue) = 0; + virtual bool IsSwitchedActive() const = 0; virtual std::wstring SaveUserPreferences() = 0; virtual void RestoreUserPreferences(_In_ std::wstring_view userPreferences) = 0; virtual void SendCommand(Command command) = 0; @@ -246,6 +247,7 @@ namespace UnitConversionManager Category GetCurrentCategory() override; void SetCurrentUnitTypes(const Unit& fromType, const Unit& toType) override; void SwitchActive(const std::wstring& newValue) override; + bool IsSwitchedActive() const override; std::wstring SaveUserPreferences() override; void RestoreUserPreferences(std::wstring_view userPreference) override; void SendCommand(Command command) override; diff --git a/src/CalcViewModel/UnitConverterViewModel.cpp b/src/CalcViewModel/UnitConverterViewModel.cpp index a159cf7..90960eb 100644 --- a/src/CalcViewModel/UnitConverterViewModel.cpp +++ b/src/CalcViewModel/UnitConverterViewModel.cpp @@ -134,7 +134,6 @@ UnitConverterViewModel::UnitConverterViewModel(const shared_ptrIsGrouped = true; m_currencyFormatter->Mode = CurrencyFormatterMode::UseCurrencyCode; m_currencyFormatter->ApplyRoundingForCurrency(RoundingAlgorithm::RoundHalfDown); - m_currencyMaxFractionDigits = m_currencyFormatter->FractionDigits; auto resourceLoader = AppResourceProvider::GetInstance(); m_localizedValueFromFormat = resourceLoader->GetResourceString(UnitConverterResourceKeys::ValueFromFormat); @@ -228,7 +227,9 @@ void UnitConverterViewModel::OnUnitChanged(Object ^ parameter) return; } + UpdateCurrencyFormatter(); m_model->SetCurrentUnitTypes(UnitFrom->GetModelUnit(), UnitTo->GetModelUnit()); + if (m_supplementaryResultsTimer != nullptr) { // End timer to show results immediately @@ -246,7 +247,7 @@ void UnitConverterViewModel::OnSwitchActive(Platform::Object ^ unused) if (m_relocalizeStringOnSwitch) { // 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(); @@ -269,9 +270,11 @@ void UnitConverterViewModel::OnSwitchActive(Platform::Object ^ unused) m_isInputBlocked = false; 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; @@ -280,10 +283,33 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st 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->FractionDigits = 0; - m_currencyFormatter->IsDecimalPointAlwaysDisplayed = false; - m_currencyFormatter->FractionDigits = 0; + currencyFormatter->IsDecimalPointAlwaysDisplayed = false; + currencyFormatter->FractionDigits = 0; wstring::size_type posOfE = stringToLocalize.find(L'e'); if (posOfE != wstring::npos) @@ -293,7 +319,8 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st std::wstring significandStr(stringToLocalize.substr(0, posOfE)); 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 { @@ -304,7 +331,7 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st if (hasDecimal) { - if (allowPartialStrings) + if (allowPartialStrings && lastCurrencyFractionDigits > 0) { // 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" @@ -312,18 +339,18 @@ String ^ UnitConverterViewModel::ConvertToLocalizedString(const std::wstring& st // typed a post-decimal digit. 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. m_decimalFormatter->FractionDigits = static_cast(stringToLocalize.length() - (posOfDecimal + 1)); - m_currencyFormatter->FractionDigits = m_currencyMaxFractionDigits; + currencyFormatter->FractionDigits = lastCurrencyFractionDigits; } if (IsCurrencyCurrentCategory) { - wstring currencyResult = m_currencyFormatter->Format(stod(stringToLocalize))->Data(); - wstring currencyCode = m_currencyFormatter->Currency->Data(); + wstring currencyResult = currencyFormatter->Format(stod(stringToLocalize))->Data(); + wstring currencyCode = currencyFormatter->Currency->Data(); // CurrencyFormatter always includes LangCode or Symbol. Make it include 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; } + + // restore the original fraction digits + currencyFormatter->FractionDigits = lastCurrencyFractionDigits; + return result; } @@ -394,9 +425,9 @@ void UnitConverterViewModel::DisplayPasteError() void UnitConverterViewModel::UpdateDisplay(const wstring& from, const wstring& to) { - String ^ fromStr = this->ConvertToLocalizedString(from, true); + String ^ fromStr = this->ConvertToLocalizedString(from, true, CurrencyFormatterParameterFrom); UpdateInputBlocked(from); - String ^ toStr = this->ConvertToLocalizedString(to, true); + String ^ toStr = this->ConvertToLocalizedString(to, true, CurrencyFormatterParameterTo); bool updatedValueFrom = ValueFrom != fromStr; 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, - UCM::Command::Five, UCM::Command::Six, UCM::Command::Seven, UCM::Command::Eight, UCM::Command::Nine }; - if (m_isInputBlocked && - command != UCM::Command::Clear && - command != UCM::Command::Backspace) - { - return; - } - m_model->SendCommand(command); + UCM::Command::Five, UCM::Command::Six, UCM::Command::Seven, UCM::Command::Eight, UCM::Command::Nine }; + + // input should be allowed if user just switches active, because we will clear values in such cases + if (m_isInputBlocked && !m_model->IsSwitchedActive() && command != UCM::Command::Clear && command != UCM::Command::Backspace) + { + return; + } + m_model->SendCommand(command); TraceLogger::GetInstance()->LogConverterInputReceived(Mode); } @@ -755,8 +786,8 @@ void UnitConverterViewModel::RefreshSupplementaryResults() for (tuple suggestedValue : m_cachedSuggestedValues) { - SupplementaryResult ^ result = - ref new SupplementaryResult(this->ConvertToLocalizedString(get<0>(suggestedValue), false), ref new Unit(get<1>(suggestedValue))); + SupplementaryResult ^ result = ref new SupplementaryResult( + this->ConvertToLocalizedString(get<0>(suggestedValue), false, CurrencyFormatterParameter::Default), ref new Unit(get<1>(suggestedValue))); if (result->IsWhimsical()) { whimsicals.push_back(result); @@ -803,10 +834,46 @@ void UnitConverterViewModel::UpdateInputBlocked(_In_ const wstring& currencyInpu m_isInputBlocked = false; if (posOfDecimal != wstring::npos && IsCurrencyCurrentCategory) { - m_isInputBlocked = (posOfDecimal + static_cast(m_currencyMaxFractionDigits) + 1 == currencyInput.length()); + m_isInputBlocked = (posOfDecimal + static_cast(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) { 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; if (displayvalue == ValueFrom && Utils::IsLastCharacterTarget(m_valueFromUnlocalized, m_decimalSeparator)) { // Need to compute a second localized value for the automation // name that does not include the decimal separator. - displayvalue = ConvertToLocalizedString(m_valueFromUnlocalized, false /*allowTrailingDecimal*/); + displayvalue = ConvertToLocalizedString(m_valueFromUnlocalized, false /*allowTrailingDecimal*/, cfp); format = m_localizedValueFromDecimalFormat; } @@ -962,7 +1034,7 @@ void UnitConverterViewModel::UpdateValue1AutomationName() { 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) { - Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2->AccessibleName, m_localizedValueToFormat); + Value2AutomationName = GetLocalizedAutomationName(Value2, Unit2->AccessibleName, m_localizedValueToFormat, CurrencyFormatterParameter::ForValue1); } } diff --git a/src/CalcViewModel/UnitConverterViewModel.h b/src/CalcViewModel/UnitConverterViewModel.h index a5287e4..b48c1c3 100644 --- a/src/CalcViewModel/UnitConverterViewModel.h +++ b/src/CalcViewModel/UnitConverterViewModel.h @@ -227,8 +227,19 @@ namespace CalculatorApp void OnCopyCommand(Platform::Object ^ parameter); void OnPasteCommand(Platform::Object ^ parameter); + enum class CurrencyFormatterParameter + { + Default, + ForValue1, + ForValue2, + }; + 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 ^ GetLocalizedConversionResultStringFormat( _In_ Platform::String ^ fromValue, @@ -276,11 +287,13 @@ namespace CalculatorApp void SupplementaryResultsTimerCancel(Windows::System::Threading::ThreadPoolTimer ^ timer); void RefreshSupplementaryResults(); void UpdateInputBlocked(_In_ const std::wstring& currencyInput); + void UpdateCurrencyFormatter(); + void UpdateIsDecimalEnabled(); bool UnitsAreValid(); void ResetCategory(); 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 m_model; wchar_t m_decimalSeparator; @@ -290,6 +303,34 @@ namespace CalculatorApp Source, Target } 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 { Platform::String^ get() { return m_value1cp == ConversionParameter::Source ? Value1 : Value2; } @@ -323,7 +364,8 @@ namespace CalculatorApp std::mutex m_cacheMutex; Windows::Globalization::NumberFormatting::DecimalFormatter ^ m_decimalFormatter; 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_valueToUnlocalized; bool m_relocalizeStringOnSwitch; diff --git a/src/CalculatorUITestFramework/CalculatorApp.cs b/src/CalculatorUITestFramework/CalculatorApp.cs index f1495a0..4112b1e 100644 --- a/src/CalculatorUITestFramework/CalculatorApp.cs +++ b/src/CalculatorUITestFramework/CalculatorApp.cs @@ -35,6 +35,14 @@ public static void EnsureCalculatorHasFocus() AppName.Click(); } + /// + /// Click the window (to lose focus on components) + /// + public static void ClickOnWindow() + { + Window.Click(); + } + /// /// 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 diff --git a/src/CalculatorUITestFramework/UnitConverterOperatorsPanel.cs b/src/CalculatorUITestFramework/UnitConverterOperatorsPanel.cs index f2be9bd..83d8322 100644 --- a/src/CalculatorUITestFramework/UnitConverterOperatorsPanel.cs +++ b/src/CalculatorUITestFramework/UnitConverterOperatorsPanel.cs @@ -13,5 +13,7 @@ public class UnitConverterOperatorsPanel public NumberPad NumberPad = new NumberPad(); public WindowsElement ClearButton => this.session.TryFindElementByAccessibilityId("ClearEntryButtonPos0"); public WindowsElement BackSpaceButton => this.session.TryFindElementByAccessibilityId("BackSpaceButtonSmall"); + public WindowsElement Units1 => this.session.TryFindElementByAccessibilityId("Units1"); + public WindowsElement Units2 => this.session.TryFindElementByAccessibilityId("Units2"); } } diff --git a/src/CalculatorUITestFramework/UnitConverterPage.cs b/src/CalculatorUITestFramework/UnitConverterPage.cs index e3984cf..516cf89 100644 --- a/src/CalculatorUITestFramework/UnitConverterPage.cs +++ b/src/CalculatorUITestFramework/UnitConverterPage.cs @@ -64,5 +64,44 @@ public void EnsureCalculatorIsCurrencyMode() } } + /// + /// Ensure Units1 and Units2 are the same + /// + 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(); + } + + /// + /// Select value in Units1 ComboBox + /// + /// Value in ComboBox Units1 + public void SelectUnits1(string value) + { + CalculatorApp.ClickOnWindow(); + UnitConverterOperators.Units1.SendKeys(value); + UnitConverterOperators.Units1.SendKeys(OpenQA.Selenium.Keys.Enter); + CalculatorApp.ClickOnWindow(); + } + + /// + /// Select value in Units2 ComboBox + /// + /// Value in ComboBox Units2 + public void SelectUnits2(string value) + { + CalculatorApp.ClickOnWindow(); + UnitConverterOperators.Units2.SendKeys(value); + UnitConverterOperators.Units2.SendKeys(OpenQA.Selenium.Keys.Enter); + CalculatorApp.ClickOnWindow(); + } } } diff --git a/src/CalculatorUITests/CalculatorUITests.ci.runsettings b/src/CalculatorUITests/CalculatorUITests.ci.runsettings index 8f2810d..bc6aa51 100644 --- a/src/CalculatorUITests/CalculatorUITests.ci.runsettings +++ b/src/CalculatorUITests/CalculatorUITests.ci.runsettings @@ -8,5 +8,7 @@ + + diff --git a/src/CalculatorUITests/CalculatorUITests.release.runsettings b/src/CalculatorUITests/CalculatorUITests.release.runsettings index 7ad3d4f..e13fca6 100644 --- a/src/CalculatorUITests/CalculatorUITests.release.runsettings +++ b/src/CalculatorUITests/CalculatorUITests.release.runsettings @@ -8,5 +8,7 @@ + + diff --git a/src/CalculatorUITests/CurrencyConverterFunctionalTests.cs b/src/CalculatorUITests/CurrencyConverterFunctionalTests.cs index fc8d669..e102c3c 100644 --- a/src/CalculatorUITests/CurrencyConverterFunctionalTests.cs +++ b/src/CalculatorUITests/CurrencyConverterFunctionalTests.cs @@ -12,6 +12,8 @@ public class CurrencyConverterFunctionalTests { private static UnitConverterPage page = new UnitConverterPage(); + public TestContext TestContext { get; set; } + /// /// Initializes the WinAppDriver web driver session. /// @@ -42,6 +44,7 @@ public void TestInit() CalculatorApp.EnsureCalculatorHasFocus(); page.EnsureCalculatorIsCurrencyMode(); page.EnsureCalculatorResultTextIsZero(); + page.EnsureSameUnitsAreSelected(); } [TestCleanup] @@ -50,6 +53,23 @@ public void TestCleanup() 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 /// /// 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 } + /// + /// These automated tests verify the currency has been formatted to 3 fractional digits + /// Via mouse input, all basic UI functionality is checked + /// + [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 + } + + /// + /// These automated tests verify the currency has been formatted to no fractional digit + /// Via mouse input, all basic UI functionality is checked + /// + [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 + } + + /// + /// These automated tests verify the currency format is updated after switching + /// Via mouse input, all basic UI functionality is checked + /// + [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()); + } + + /// + /// 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 + /// + [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()); + } + + /// + /// These automated tests verify the result consists after swiching currency + /// Via mouse input, all basic UI functionality is checked + /// + [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 } } diff --git a/src/CalculatorUnitTests/MultiWindowUnitTests.cpp b/src/CalculatorUnitTests/MultiWindowUnitTests.cpp index 38d2905..9b745a7 100644 --- a/src/CalculatorUnitTests/MultiWindowUnitTests.cpp +++ b/src/CalculatorUnitTests/MultiWindowUnitTests.cpp @@ -685,7 +685,7 @@ TEST_METHOD(InitializeMultipleConverterTest) viewModels[i] = ref new UnitConverterViewModel(unitConverterMocks[i]); IObservableVector ^ cats = viewModels[i]->Categories; 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_IS_TRUE(CAT2 == viewModels[i]->CurrentCategory->GetModelCategory()); } @@ -697,7 +697,7 @@ TEST_METHOD(InitializeMultipleConverterTest) // Verify that the instance properties were set independently 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); } diff --git a/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp b/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp index 11fc09f..c88e231 100644 --- a/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp +++ b/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.cpp @@ -140,6 +140,7 @@ vector UnitConverterMock::GetCategories() cats.push_back(CAT1); cats.push_back(CAT2); cats.push_back(CAT3); + cats.push_back(CAT_CURRENCY); m_curCategory = CAT2; @@ -174,6 +175,10 @@ UCM::CategorySelectionInitializer UnitConverterMock::SetCurrentCategory(const UC units.push_back(UNIT9); break; } + case CURRENCY_ID: + units.push_back(UNITJPY); + units.push_back(UNITJOD); + break; default: throw; } @@ -215,10 +220,15 @@ void UnitConverterMock::SwitchActive(const std::wstring& newValue) m_curValue = newValue; } - std::wstring UnitConverterMock::SaveUserPreferences() - { - return L"TEST"; - }; +bool UnitConverterMock::IsSwitchedActive() const +{ + return false; +} + +std::wstring UnitConverterMock::SaveUserPreferences() +{ + return L"TEST"; +}; void UnitConverterMock::RestoreUserPreferences(_In_ std::wstring_view /*userPreferences*/){}; @@ -341,7 +351,7 @@ TEST_METHOD(TestUnitConverterLoadSetsUpCategories) VM::UnitConverterViewModel vm(mock); IObservableVector ^ cats = vm.Categories; 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_IS_TRUE(CAT2 == vm.CurrentCategory->GetModelCategory()); } @@ -935,6 +945,33 @@ TEST_METHOD(TestDecimalFormattingLogic) VERIFY_IS_TRUE(vm.Value1 == L"3"); VERIFY_IS_TRUE(vm.Value2 == L"2.50"); } + +TEST_METHOD(TestCurrencyFormattingLogic) +{ + // verify that currency fraction digits is formatted per currency type + + shared_ptr mock = make_shared(); + 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 // updates, the correct automation names are are being updated. TEST_METHOD(TestValue1AndValue2AutomationNameChanges) diff --git a/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.h b/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.h index ffcd626..77436a6 100644 --- a/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.h +++ b/src/CalculatorUnitTests/UnitConverterViewModelUnitTests.h @@ -9,6 +9,8 @@ namespace UCM = UnitConversionManager; namespace CalculatorUnitTests { + static constexpr int CURRENCY_ID = 16; + 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 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 UNIT9 = { 9, L"UNIT9", L"U9", true, false, false }; 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 CAT2 = { 2, L"CAT2", false }; // contains Unit4 - Unit6 - static UCM::Category CAT3 = { 3, L"CAT3", false }; // contains Unit7 - Unit9 + 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 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 { @@ -33,7 +38,8 @@ namespace CalculatorUnitTests UCM::CategorySelectionInitializer SetCurrentCategory(const UCM::Category& input) override; UCM::Category GetCurrentCategory(); 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; void RestoreUserPreferences(_In_ std::wstring_view userPreferences) override; void SendCommand(UCM::Command command) override;