From ac998f0b14278ad2349fb39d6c8b75afef6d892f Mon Sep 17 00:00:00 2001 From: Sebastien GREGOIRE Date: Wed, 30 Jul 2025 09:49:50 +0200 Subject: [PATCH 1/2] add test highlighting problems --- source/nodejs/adaptivecards/package.json | 2 +- .../src/__tests__/text-formatters.spec.ts | 37 +++++++++ .../DateAndTimeUnitTest.cpp | 79 +++++++++++++++++-- 3 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 source/nodejs/adaptivecards/src/__tests__/text-formatters.spec.ts diff --git a/source/nodejs/adaptivecards/package.json b/source/nodejs/adaptivecards/package.json index f7ea83ddc1..dfd4650890 100644 --- a/source/nodejs/adaptivecards/package.json +++ b/source/nodejs/adaptivecards/package.json @@ -21,7 +21,7 @@ "src" ], "scripts": { - "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --forceExit --runInBand --detectOpenHandles", + "test": "TZ=UTC cross-env NODE_OPTIONS=--experimental-vm-modules jest --forceExit --runInBand --detectOpenHandles", "clean": "rimraf build lib dist", "generate-css": "concurrently \"sass -I ../node_modules src/scss/adaptivecards-default.scss lib/adaptivecards.css\" \"sass -I ../node_modules src/scss/adaptivecards-carousel.scss lib/adaptivecards-carousel.css\"", "build-source": "concurrently \"tsc\" \"npm:generate-css\"", diff --git a/source/nodejs/adaptivecards/src/__tests__/text-formatters.spec.ts b/source/nodejs/adaptivecards/src/__tests__/text-formatters.spec.ts new file mode 100644 index 0000000000..819592239e --- /dev/null +++ b/source/nodejs/adaptivecards/src/__tests__/text-formatters.spec.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { formatText } from "../text-formatters"; + + +function isoDate(tz: string = ""): string { + return `2023-10-01T12:00:00${tz}`; +} + +describe("Text Formatters", () => { + test("UTC formatter", () => { + const date = isoDate("Z"); + const res = formatText("en-GB", `{{DATE(${date}, SHORT)}} {{TIME(${date})}}`); + expect(res).toBe("Sun, 1 Oct 2023 12:00"); + }); + + describe("Timezone leading to date change", () => { + test("Negative date change", () => { + const date = isoDate("+13:00"); + const res = formatText("en-GB", `{{DATE(${date}, SHORT)}} {{TIME(${date})}}`); + expect(res).toBe("Sat, 30 Sept 2023 23:00"); + }); + + + test("Positive date change", () => { + const date = isoDate("-13:00"); + const res = formatText("en-GB", `{{DATE(${date}, SHORT)}} {{TIME(${date})}}`); + expect(res).toBe("Mon, 2 Oct 2023 1:00"); + }); + }); + + test("Date without timezone", () => { + const date = isoDate(); + const res = formatText("en-GB", `{{DATE(${date}, SHORT)}} {{TIME(${date})}}`); + expect(res).toBe("Sun, 1 Oct 2023 12:00"); + }); +}); \ No newline at end of file diff --git a/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/DateAndTimeUnitTest.cpp b/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/DateAndTimeUnitTest.cpp index d4f83ffe39..21a783fee9 100644 --- a/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/DateAndTimeUnitTest.cpp +++ b/source/shared/cpp/AdaptiveCardsSharedModel/AdaptiveCardsSharedModelUnitTest/DateAndTimeUnitTest.cpp @@ -74,9 +74,7 @@ namespace AdaptiveCardsSharedModelUnitTest SKIP_TEST_OUTSIDE_PACIFIC_TIME(); TextBlock blck; // seoul - std::string testString = "{{TIME(2017-10-28T11:25:00+09:00)}}"; - blck.SetText(testString); - Assert::AreEqual("{{TIME(2017-10-28T11:25:00+09:00)}}", blck.GetText()); + Assert::AreEqual("{{TIME(2017-10-28T11:25:00+09:00)}}", blck.GetText()); DateTimePreparser preparser = blck.GetTextForDateParsing(); Assert::AreEqual(preparser.GetTextTokens().front()->GetText(), "07:25 PM"); @@ -94,6 +92,19 @@ namespace AdaptiveCardsSharedModelUnitTest DateTimePreparser preparser = blck.GetTextForDateParsing(); Assert::AreEqual(preparser.GetTextTokens().front()->GetText(), "07:27 PM"); Assert::IsTrue(preparser.GetTextTokens().front()->GetFormat() == DateTimePreparsedTokenFormat::RegularString); + } + TEST_METHOD(TransformToTimeWithoutOffsetTest) + { + SKIP_TEST_OUTSIDE_PACIFIC_TIME(); + TextBlock blck; + // New York + std::string testString = "{{TIME(2017-10-27T22:27)}}"; + blck.SetText(testString); + Assert::AreEqual("{{TIME(2017-10-27T22:27:00)}}", blck.GetText()); + + DateTimePreparser preparser = blck.GetTextForDateParsing(); + Assert::AreEqual(preparser.GetTextTokens().front()->GetText(), "10:27 PM"); + Assert::IsTrue(preparser.GetTextTokens().front()->GetFormat() == DateTimePreparsedTokenFormat::RegularString); } }; TEST_CLASS(DateTest) @@ -132,15 +143,31 @@ namespace AdaptiveCardsSharedModelUnitTest { SKIP_TEST_OUTSIDE_PACIFIC_TIME(); TextBlock blck; - std::string testString = "{{DATE(2017-10-28T11:25:00+09:00, COMPACT)}}"; + std::string testString = "{{DATE(2023-10-01T12:00:00+13:00, COMPACT)}}"; // New York blck.SetText(testString); - Assert::AreEqual("{{DATE(2017-10-28T11:25:00+09:00, COMPACT)}}", blck.GetText()); + Assert::AreEqual("{{DATE(2023-10-01T12:00:00+13:00, COMPACT)}}", blck.GetText()); DateTimePreparser preparser = blck.GetTextForDateParsing(); std::shared_ptr token(preparser.GetTextTokens().front()); - Assert::AreEqual(token->GetText(), "{{DATE(2017-10-28T11:25:00+09:00, COMPACT)}}"); - Assert::IsTrue(token->GetDay() == 27 && token->GetMonth() == 9 && token->GetYear() == 2017); + Assert::AreEqual(token->GetText(), "{{DATE(2017-10-28T12:00:00+13:00, COMPACT)}}"); + Assert::IsTrue(token->GetDay() == 29 && token->GetMonth() == 8 && token->GetYear() == 2023); + Assert::IsTrue(token->GetFormat() == DateTimePreparsedTokenFormat::DateCompact); + } + + TEST_METHOD(TransformToDateWithLargeNegativeOffset) + { + SKIP_TEST_OUTSIDE_PACIFIC_TIME(); + TextBlock blck; + std::string testString = "{{DATE(2023-10-01T12:00:00-13:00, COMPACT)}}"; + // New York + blck.SetText(testString); + Assert::AreEqual("{{DATE(2023-10-01T12:00:00-13:00, COMPACT)}}", blck.GetText()); + + DateTimePreparser preparser = blck.GetTextForDateParsing(); + std::shared_ptr token(preparser.GetTextTokens().front()); + Assert::AreEqual(token->GetText(), "{{DATE(2023-10-01T12:00:00-13:00, COMPACT)}}"); + Assert::IsTrue(token->GetDay() == 2 && token->GetMonth() == 9 && token->GetYear() == 2023); Assert::IsTrue(token->GetFormat() == DateTimePreparsedTokenFormat::DateCompact); } @@ -183,6 +210,22 @@ namespace AdaptiveCardsSharedModelUnitTest Assert::AreEqual("{{DATE(2017-10-27T22:27:00-04:00, COMPACT)}}", blck.GetText()); } + TEST_METHOD(TransformToDateWithoutOffsetTest) + { + SKIP_TEST_OUTSIDE_PACIFIC_TIME(); + TextBlock blck; + // New York + std::string testString = "{{DATE(2017-10-27T22:27, COMPACT)}}"; + blck.SetText(testString); + Assert::AreEqual("{{DATE(2017-10-27T22:27:00, COMPACT)}}", blck.GetText()); + + DateTimePreparser preparser = blck.GetTextForDateParsing(); + std::shared_ptr token(preparser.GetTextTokens().front()); + Assert::AreEqual(token->GetText(), "{{DATE(2017-10-27T22:27:00,COMPACT)}}"); + Assert::IsTrue(token->GetDay() == 27 && token->GetMonth() == 9 && token->GetYear() == 2017); + Assert::IsTrue(token->GetFormat() == DateTimePreparsedTokenFormat::DateCompact); + } + }; TEST_CLASS(TimeAndDateInputTest) { @@ -423,6 +466,28 @@ namespace AdaptiveCardsSharedModelUnitTest std::string testString = "{{DATE(1994-02-29T06:08:00Z)}}"; blck.SetText(testString); Assert::AreEqual("{{DATE(1994-02-29T06:08:00Z)}}", blck.GetText()); + } + TEST_METHOD(PostiveOffsetLeadingToDateChange) + { + TextBlock blck; + std::string testString = "{{DATE(1994-02-29T12:00:00+13:00)}}"; + blck.SetText(testString); + Assert::AreEqual("{{DATE(1994-02-29T12:00:00+13:00)}}", blck.GetText()); + + DateTimePreparser preparser = blck.GetTextForDateParsing(); + Assert::AreEqual(preparser.GetTextTokens().front()->GetText(), "01:00 AM"); + Assert::IsTrue(preparser.GetTextTokens().front()->GetFormat() == DateTimePreparsedTokenFormat::RegularString); + } + TEST_METHOD(NegativeOffsetLeadingToTimeChange) + { + TextBlock blck; + std::string testString = "{{TIME(1994-02-29T12:00:00+13:00)}}"; + blck.SetText(testString); + Assert::AreEqual("{{TIME(1994-02-29T12:00:00+13:00)}}", blck.GetText()); + + DateTimePreparser preparser = blck.GetTextForDateParsing(); + Assert::AreEqual(preparser.GetTextTokens().front()->GetText(), "01:00 AM"); + Assert::IsTrue(preparser.GetTextTokens().front()->GetFormat() == DateTimePreparsedTokenFormat::RegularString); } }; } From 0b9c989f44a0947081e475da16ff97449f899f26 Mon Sep 17 00:00:00 2001 From: Sebastien GREGOIRE Date: Thu, 31 Jul 2025 09:24:30 +0200 Subject: [PATCH 2/2] add proposed solution for JS projects --- source/nodejs/adaptivecards/src/text-formatters.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/nodejs/adaptivecards/src/text-formatters.ts b/source/nodejs/adaptivecards/src/text-formatters.ts index a7cc49dcf0..e907e687bb 100644 --- a/source/nodejs/adaptivecards/src/text-formatters.ts +++ b/source/nodejs/adaptivecards/src/text-formatters.ts @@ -55,10 +55,10 @@ class TimeFormatter extends AbstractTextFormatter { export function formatText(lang: string | undefined, text: string | undefined): string | undefined { const formatters: AbstractTextFormatter[] = [ new DateFormatter( - /\{{2}DATE\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|(?:(?:-|\+)\d{2}:\d{2})))(?:, ?(COMPACT|LONG|SHORT))?\)\}{2}/g + /\{{2}DATE\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|(?:(?:-|\+)\d{2}:\d{2}))?)(?:, ?(COMPACT|LONG|SHORT))?\)\}{2}/g ), new TimeFormatter( - /\{{2}TIME\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|(?:(?:-|\+)\d{2}:\d{2})))\)\}{2}/g + /\{{2}TIME\((\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|(?:(?:-|\+)\d{2}:\d{2}))?)\)\}{2}/g ) ];