From 6facaf0b024810991ece4af6da56cc4e8eb2ae11 Mon Sep 17 00:00:00 2001 From: Matt Lichtenstein Date: Tue, 21 Nov 2023 11:42:10 -0500 Subject: [PATCH] VPN-3362: Add plan upgrade message for monthly users (#8570) * annual upgrade plan message * update documentation * separate into 2 messages * re-evaluate message conditions on a timer * mistype * linter * Unify 14 and 87 day upgrade messages into one * code review updates * linter * use single qtimer object for callbacks * disconnect timeout signal before creating new one * rename message * initial test * functional test * fix inspector command * unit test * linter * use production addon for testing * fix timer unit test * linter * move comment --- .../conditions.js | 48 +++++++++ .../message_upgrade_to_annual_plan/getHelp.js | 25 +++++ .../manifest.json | 56 +++++++++++ .../openLink.js | 4 + docs/messages.md | 3 +- scripts/ci/jsonSchemas/message.json | 4 + src/addons/addonapi.cpp | 22 ++++- src/addons/addonapi.h | 4 + src/addons/addonmessage.cpp | 18 ++++ src/addons/addonmessage.h | 14 ++- src/constants.h | 4 +- src/inspector/inspectorhandler.cpp | 28 +++++- src/models/subscriptiondata.cpp | 10 +- src/models/subscriptiondata.h | 1 + src/notificationhandler.cpp | 2 +- src/translations/strings.yaml | 3 + src/ui/screens/messaging/ViewMessage.qml | 6 ++ tests/functional/addons/CMakeLists.txt | 2 +- tests/functional/testAddons.js | 99 +++++++++++++++++++ tests/unit_tests/addons/addons.qrc | 1 + .../unit_tests/addons/api_settimedcallback.js | 3 + tests/unit_tests/testaddonapi.cpp | 40 ++++++++ tests/unit_tests/testaddonapi.h | 1 + 23 files changed, 388 insertions(+), 10 deletions(-) create mode 100644 addons/message_upgrade_to_annual_plan/conditions.js create mode 100644 addons/message_upgrade_to_annual_plan/getHelp.js create mode 100644 addons/message_upgrade_to_annual_plan/manifest.json create mode 100644 addons/message_upgrade_to_annual_plan/openLink.js create mode 100644 tests/unit_tests/addons/api_settimedcallback.js diff --git a/addons/message_upgrade_to_annual_plan/conditions.js b/addons/message_upgrade_to_annual_plan/conditions.js new file mode 100644 index 0000000000..bea6c2404e --- /dev/null +++ b/addons/message_upgrade_to_annual_plan/conditions.js @@ -0,0 +1,48 @@ +(function(api, condition) { + //Show this message after 14 days, and resurface after 87 days + + //This will run when TaskaGetSubscriptionDetails returns something different than what is loaded from setting + //(often from signing in/out but can be any change in the subscription) + api.connectSignal(api.subscriptionData, 'changed', () => computeCondition()); + + //This will run on app launch for signed in users when there subscription data is loaded into memory from settings + api.connectSignal(api.subscriptionData, 'initialized', () => computeCondition()); + + //This is for already running clients that receive this addon while the client is launched and signed in + //(Because these users may not see a change in subscriptionData for a long time) + computeCondition() + + function isMonthlyWebPlan() { + return api.subscriptionData.type === api.subscriptionData.SubscriptionWeb && + api.subscriptionData.planBillingInterval === api.subscriptionData.BillingIntervalMonthly + } + + function computeCondition() { + //subscriptionData not initalized - return + if (api.subscriptionData.createdAt <= 0) return + + let now = Date.now() + let fourteenDaysAfterSubscriptionStarted = api.subscriptionData.createdAt + 1000 * 60 * 60 * 24 * 14 + let eightySevenDaysAfterSubscriptionStarted = api.subscriptionData.createdAt + 1000 * 60 * 60 * 24 * 87 + + if (isMonthlyWebPlan) { + //Less than 14 days into the subscription, don't show message + if (now < fourteenDaysAfterSubscriptionStarted) { + api.setTimedCallback(fourteenDaysAfterSubscriptionStarted - now, () => computeCondition()); + condition.disable() + } + //Between 14 and 87 days into the subscription, show message + else if (now >= fourteenDaysAfterSubscriptionStarted && now < eightySevenDaysAfterSubscriptionStarted) { + api.setTimedCallback(eightySevenDaysAfterSubscriptionStarted - now, () => computeCondition()) + api.addon.date = fourteenDaysAfterSubscriptionStarted / 1000 + condition.enable() + } + // After 87 days into the subscription, re-surface the message (undimiss, unread, updating timestamp) + else if (now >= eightySevenDaysAfterSubscriptionStarted) { + api.addon.date = eightySevenDaysAfterSubscriptionStarted / 1000 + api.addon.resetMessage() + condition.enable() + } + } + } +}); diff --git a/addons/message_upgrade_to_annual_plan/getHelp.js b/addons/message_upgrade_to_annual_plan/getHelp.js new file mode 100644 index 0000000000..bd72ec5eda --- /dev/null +++ b/addons/message_upgrade_to_annual_plan/getHelp.js @@ -0,0 +1,25 @@ +(function(api) { + function versionCompare(a, b) { + for (let i = 0; i < 3; ++i) { + if (a[i] != b[i]) { + return a[i] > b[i] ? -1 : 1; + } + } + return 0; + } + + const parts = api.env.versionString.split('.'); + + const version = parts.map(a => parseInt(a, 10)); + + //Post 2.16 API + if (versionCompare([2, 16, 0], version) >= 0) { + api.navigator.requestScreen(api.vpn.ScreenGetHelp); + return; + } + //Pre 2.16 API + else { + api.navigator.requestScreen(api.navigator.ScreenGetHelp); + return; + } +}) diff --git a/addons/message_upgrade_to_annual_plan/manifest.json b/addons/message_upgrade_to_annual_plan/manifest.json new file mode 100644 index 0000000000..00a0a2d849 --- /dev/null +++ b/addons/message_upgrade_to_annual_plan/manifest.json @@ -0,0 +1,56 @@ +{ + "api_version": "0.1", + "id": "message_upgrade_to_annual_plan", + "name": "Upgrade to annual plan", + "type": "message", + "conditions": { + "javascript": "conditions.js" + }, + "message": { + "id": "message_upgrade_to_annual_plan", + "title": "Save 50% when you switch to an annual plan", + "subtitle": "Get the same protection, for half the price.", + "badge": "subscription", + "notify": false, + "blocks": [ + { + "id": "c_1", + "type": "text", + "content": "Switch to an annual plan, and get all the same benefits at 50% less than the monthly plan:" + }, + { "id": "c_2", + "type": "ulist", + "content": [ + { "id": "l_1", + "content": "Protection for up to 5 devices" + }, + { "id": "l_2", + "content": "500+ servers in 30+ countries" + }, + { "id": "l_3", + "content": "Device-level encryption" + }, + { "id": "l_4", + "content": "No bandwidth restrictions" + }, + { "id": "l_5", + "content": "No logging of your network activity" + } + ] + }, + { "id": "c_3", + "type": "button", + "style": "primary", + "content": "Switch to an annual plan", + "javascript": "openLink.js" + }, + { + "id": "c_4", + "type": "button", + "style": "link", + "content": "Get help", + "javascript": "getHelp.js" + } + ] + } +} diff --git a/addons/message_upgrade_to_annual_plan/openLink.js b/addons/message_upgrade_to_annual_plan/openLink.js new file mode 100644 index 0000000000..44c22ea8f6 --- /dev/null +++ b/addons/message_upgrade_to_annual_plan/openLink.js @@ -0,0 +1,4 @@ +((api) => { + return api.urlOpener.openUrl( + `https://www.mozilla.org/products/vpn/?utm_medium=mozillavpn&utm_source=messages#pricing`); +}); diff --git a/docs/messages.md b/docs/messages.md index 6b87118ba7..e74602fce1 100644 --- a/docs/messages.md +++ b/docs/messages.md @@ -22,5 +22,6 @@ property object with the following properties: | title_comment | An optional comment to describe the meaning of the title | String | No | | subtitle_comment | An optional comment to describe the meaning of the subtitle | String | No | | date | The date the message was received (using seconds since epoch time) | Number | No | -| badge | A label used to tag a message (options: `warning`, `critical`, `new_update`, `whats_new`, `survey` ) | String | No | +| badge | A label used to tag a message (options: `warning`, `critical`, `new_update`, `whats_new`, `survey`, `subscription` ) | String | No | +| notify | Should we notify the user about this message via system notifications (Default: true) | Boolean | No | | blocks | An array of graphical blocks that compose the user interface of the message's contents (see more info [here](https://github.com/mozilla-mobile/mozilla-vpn-client/wiki/guides#block-object)) | Array of Block objects | Yes | \ No newline at end of file diff --git a/scripts/ci/jsonSchemas/message.json b/scripts/ci/jsonSchemas/message.json index 4c7723fd28..57aff18240 100644 --- a/scripts/ci/jsonSchemas/message.json +++ b/scripts/ci/jsonSchemas/message.json @@ -32,6 +32,10 @@ "type": "string", "description": "The type of this message" }, + "notify": { + "type": "boolean", + "description": "Determines whether we trigger a system notification. Default: true" + }, "blocks": { "type": "array", "description": "The list of text blocks", diff --git a/src/addons/addonapi.cpp b/src/addons/addonapi.cpp index 2797b4d84d..c0fa5a47d2 100644 --- a/src/addons/addonapi.cpp +++ b/src/addons/addonapi.cpp @@ -39,7 +39,7 @@ void AddonApi::initialize() { QQmlEngine::setObjectOwnership(m_addon, QQmlEngine::CppOwnership); QJSValue value = engine->newQObject(m_addon); - value.setPrototype(engine->newQMetaObject(&Addon::staticMetaObject)); + value.setPrototype(engine->newQMetaObject(m_addon->metaObject())); insert("addon", QVariant::fromValue(value)); } @@ -90,6 +90,26 @@ void AddonApi::initialize() { if (s_constructorCallback) { s_constructorCallback(this); } + + m_timer.setSingleShot(true); +} + +void AddonApi::setTimedCallback(int interval, const QJSValue& callback) { + if (!callback.isCallable()) { + logger.debug() << "No callback received"; + return; + } + + // disconnect a potential previous timer + if (m_timer.isActive()) { + logger.warning() << "Disconnecting timer for addon: " << m_addon->id(); + + QObject::disconnect(&m_timer, nullptr, nullptr, nullptr); + } + + connect(&m_timer, &QTimer::timeout, this, [callback]() { callback.call(); }); + + m_timer.start(interval); } void AddonApi::log(const QString& message) { logger.debug() << message; } diff --git a/src/addons/addonapi.h b/src/addons/addonapi.h index 2cff47d1a6..763732144b 100644 --- a/src/addons/addonapi.h +++ b/src/addons/addonapi.h @@ -7,6 +7,7 @@ #include #include +#include class Addon; @@ -27,6 +28,8 @@ class AddonApi final : public QQmlPropertyMap { const QJSValue& callback); Q_INVOKABLE void log(const QString& message); + Q_INVOKABLE void setTimedCallback(int interval, const QJSValue& callback); + /** * @brief callback executed when a new AddonApi is created. Use it to add * your custom APIs. @@ -39,6 +42,7 @@ class AddonApi final : public QQmlPropertyMap { private: Addon* m_addon = nullptr; + QTimer m_timer; }; class AddonApiCallbackWrapper final : public QObject { diff --git a/src/addons/addonmessage.cpp b/src/addons/addonmessage.cpp index e766cb1080..c8df0d34a5 100644 --- a/src/addons/addonmessage.cpp +++ b/src/addons/addonmessage.cpp @@ -61,6 +61,9 @@ Addon* AddonMessage::create(QObject* parent, const QString& manifestFileName, message->m_date = messageObj["date"].toInteger(); message->planDateRetranslation(); + // if "notify" is not specified in the manifest, default to true + message->m_shouldNotify = messageObj["notify"].toBool(true); + message->setBadge(messageObj["badge"].toString()); guard.dismiss(); @@ -104,6 +107,12 @@ void AddonMessage::updateMessageStatus(MessageStatus newStatus) { QMetaEnum statusMetaEnum = QMetaEnum::fromType(); QString newStatusSetting = statusMetaEnum.valueToKey(newStatus); + + // We are going from dismissed, to some other status, so re-enable the message + if (m_status == MessageStatus::Dismissed) { + enable(); + } + m_status = newStatus; emit statusChanged(m_status); @@ -126,6 +135,11 @@ void AddonMessage::dismiss() { void AddonMessage::markAsRead() { updateMessageStatus(MessageStatus::Read); } +// Marks messaged as un-read and un-dimissed +void AddonMessage::resetMessage() { + updateMessageStatus(MessageStatus::Received); +} + bool AddonMessage::containsSearchString(const QString& query) const { if (query.isEmpty()) { return true; @@ -220,6 +234,8 @@ void AddonMessage::setBadge(const QString& badge) { m_badge = WhatsNew; } else if (badge == "survey") { m_badge = Survey; + } else if (badge == "subscription") { + m_badge = Subscription; } else { logger.error() << "Unsupported badge type" << badge; } @@ -233,4 +249,6 @@ void AddonMessage::setBadge(Badge badge) { void AddonMessage::setDate(qint64 date) { m_date = date; emit dateChanged(); + // Notifies formattedDate that the date has been changed + emit retranslationCompleted(); } diff --git a/src/addons/addonmessage.h b/src/addons/addonmessage.h index 116adecb66..fd87bbb1a3 100644 --- a/src/addons/addonmessage.h +++ b/src/addons/addonmessage.h @@ -38,7 +38,15 @@ class AddonMessage final : public Addon { QString formattedDate READ formattedDate NOTIFY retranslationCompleted) Q_PROPERTY(Badge badge MEMBER m_badge WRITE setBadge NOTIFY badgeChanged) - enum Badge { None, Warning, Critical, NewUpdate, WhatsNew, Survey }; + enum Badge { + None, + Warning, + Critical, + NewUpdate, + WhatsNew, + Survey, + Subscription + }; Q_ENUM(Badge) static Addon* create(QObject* parent, const QString& manifestFileName, @@ -61,6 +69,7 @@ class AddonMessage final : public Addon { Q_INVOKABLE void dismiss(); Q_INVOKABLE void markAsRead(); + Q_INVOKABLE void resetMessage(); Q_INVOKABLE bool containsSearchString(const QString& query) const; void setBadge(Badge badge); @@ -70,6 +79,8 @@ class AddonMessage final : public Addon { bool isReceived() const { return m_status == MessageStatus::Received; } + bool shouldNotify() const { return m_shouldNotify; } + QString formattedDate() const; bool enabled() const override; @@ -109,6 +120,7 @@ class AddonMessage final : public Addon { Composer* m_composer = nullptr; qint64 m_date = 0; + bool m_shouldNotify = true; MessageStatus m_status = MessageStatus::Received; diff --git a/src/constants.h b/src/constants.h index 912ad5097c..f8cf22ef63 100644 --- a/src/constants.h +++ b/src/constants.h @@ -171,8 +171,8 @@ constexpr uint32_t BENCHMARK_THRESHOLD_SPEED_MEDIUM = 10000000; // 10 Megabit CONSTEXPR(uint32_t, releaseMonitorMsec, 21600000, 4000, 0) // in milliseconds, how often we should fetch the server list, the account and -// so on. -CONSTEXPR(uint32_t, schedulePeriodicTaskTimerMsec, 3600000, 30000, 0) +// so on. 60 minuts on prod, 5 minutes on stage +CONSTEXPR(uint32_t, schedulePeriodicTaskTimerMsec, 3600000, 300000, 0) // how often we check the captive portal when the VPN is on. CONSTEXPR(uint32_t, captivePortalRequestTimeoutMsec, 10000, 4000, 0) diff --git a/src/inspector/inspectorhandler.cpp b/src/inspector/inspectorhandler.cpp index 99ef97daae..d19fd0aa29 100644 --- a/src/inspector/inspectorhandler.cpp +++ b/src/inspector/inspectorhandler.cpp @@ -33,6 +33,8 @@ #include "logger.h" #include "loghandler.h" #include "models/featuremodel.h" +#include "models/subscriptiondata.h" +#include "mozillavpn.h" #include "mzglean.h" #include "networkmanager.h" #include "qmlengineholder.h" @@ -636,7 +638,31 @@ static QList s_commands{ [](InspectorHandler*, const QList&) { MZGlean::initialize(); return QJsonObject(); - }}}; + }}, + + InspectorCommand{ + "set_subscription_start_date", + "Changes the start date of the subscription", 1, + [](InspectorHandler*, const QList& arguments) { + qint64 newCreatedAtTimestamp = arguments[1].toLongLong(); + + // get sub data json from settings + QByteArray subscriptionData = + SettingsHolder::instance()->subscriptionData(); + QJsonDocument doc = QJsonDocument::fromJson(subscriptionData); + QJsonObject obj = doc.object(); + QJsonObject subObj = obj["subscription"].toObject(); + + // modify createdAt date + qlonglong createdAt = newCreatedAtTimestamp; + subObj["created"] = createdAt; + obj["subscription"] = subObj; + doc.setObject(obj); + subscriptionData = doc.toJson(); + SettingsHolder::instance()->setSubscriptionData(subscriptionData); + + return QJsonObject(); + }}}; // static void InspectorHandler::initialize() { diff --git a/src/models/subscriptiondata.cpp b/src/models/subscriptiondata.cpp index f46db943cc..bebc7ac16d 100644 --- a/src/models/subscriptiondata.cpp +++ b/src/models/subscriptiondata.cpp @@ -23,7 +23,12 @@ namespace { Logger logger("SubscriptionData"); } // namespace -SubscriptionData::SubscriptionData() { MZ_COUNT_CTOR(SubscriptionData); } +SubscriptionData::SubscriptionData() { + MZ_COUNT_CTOR(SubscriptionData); + + connect(SettingsHolder::instance(), &SettingsHolder::subscriptionDataChanged, + this, &SubscriptionData::fromSettings); +} SubscriptionData::~SubscriptionData() { MZ_COUNT_DTOR(SubscriptionData); } @@ -50,12 +55,13 @@ bool SubscriptionData::fromSettings() { logger.debug() << "Reading the subscription data from settings"; - const QByteArray& json = settingsHolder->devices(); + const QByteArray& json = settingsHolder->subscriptionData(); if (json.isEmpty() || !fromJsonInternal(json)) { return false; } m_rawJson = json; + emit initialized(); return true; } diff --git a/src/models/subscriptiondata.h b/src/models/subscriptiondata.h index 00c2b585aa..7a35e02787 100644 --- a/src/models/subscriptiondata.h +++ b/src/models/subscriptiondata.h @@ -68,6 +68,7 @@ class SubscriptionData final : public QObject { signals: void changed(); + void initialized(); private: bool fromJsonInternal(const QByteArray& json); diff --git a/src/notificationhandler.cpp b/src/notificationhandler.cpp index fa7b0b7f34..99e2c83d84 100644 --- a/src/notificationhandler.cpp +++ b/src/notificationhandler.cpp @@ -370,7 +370,7 @@ void NotificationHandler::addonCreated(Addon* addon) { return; } - if (addon->enabled()) { + if (addon->enabled() && qobject_cast(addon)->shouldNotify()) { maybeAddonNotification(addon); } diff --git a/src/translations/strings.yaml b/src/translations/strings.yaml index a483f8b110..12666de5b1 100644 --- a/src/translations/strings.yaml +++ b/src/translations/strings.yaml @@ -883,6 +883,9 @@ inAppMessaging: surveyBadge: value: Survey comment: A badge shown in a message signifying that this message contains a survey + subscriptionBadge: + value: Subscription + comment: A badge shown in a message signifying that this message contains information about the user's subscription devices: countLabel: diff --git a/src/ui/screens/messaging/ViewMessage.qml b/src/ui/screens/messaging/ViewMessage.qml index 1ff38ff546..0bb70d6b7f 100644 --- a/src/ui/screens/messaging/ViewMessage.qml +++ b/src/ui/screens/messaging/ViewMessage.qml @@ -101,6 +101,8 @@ MZViewBase { return badgeInfo.whatsNewBadge case MZAddonMessage.Survey: return badgeInfo.surveyBadge + case MZAddonMessage.Subscription: + return badgeInfo.subscriptionBadge } } @@ -127,6 +129,10 @@ MZViewBase { 'badgeText': MZI18n.InAppMessagingSurveyBadge, 'badgeTheme': MZTheme.theme.blueBadge }; + property var subscriptionBadge: { + 'badgeText': MZI18n.InAppMessagingSubscriptionBadge, + 'badgeTheme': MZTheme.theme.blueBadge + }; } } } diff --git a/tests/functional/addons/CMakeLists.txt b/tests/functional/addons/CMakeLists.txt index 15cea15ff3..65cc5901d0 100644 --- a/tests/functional/addons/CMakeLists.txt +++ b/tests/functional/addons/CMakeLists.txt @@ -52,4 +52,4 @@ add_addon_target(test_08_message_disabled OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/08_message_disabled/ SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/08_message_disabled/ ) -set_target_properties(test_08_message_disabled PROPERTIES EXCLUDE_FROM_ALL FALSE) +set_target_properties(test_08_message_disabled PROPERTIES EXCLUDE_FROM_ALL FALSE) \ No newline at end of file diff --git a/tests/functional/testAddons.js b/tests/functional/testAddons.js index c79fc7aa2e..0207497731 100644 --- a/tests/functional/testAddons.js +++ b/tests/functional/testAddons.js @@ -305,4 +305,103 @@ describe('Addons', function() { await vpn.waitForQueryAndClick( queries.screenMessaging.messageItem('message_disabled').visible()); }); + + describe('test message_upgrade_to_annual_plan addon condition', async () => { + + const testCases = [ + [() => Date.now() - 1000 * 60 * 60 * 24 * 13, + "", + false, + '13 days after subscription created'], + [() => Date.now() - 1000 * 60 * 60 * 24 * 14, + "time", + true, + '14 days after subscription created'], + [() => Date.now() - 1000 * 60 * 60 * 24 * 15, + "yesterday", + true, + '15 days after subscription created'], + [() => Date.now() - 1000 * 60 * 60 * 24 * 86, + "date", + true, + '86 days after subscription created'], + //Fails by an hour (due to daylight savings time?) + // [() => Date.now() - 1000 * 60 * 60 * 24 * 87, + // "time", + // true, + // '87 days after subscription created'], + [() => Date.now() - 1000 * 60 * 60 * 24 * 88, + "yesterday", + true, + '88 days after subscription created'], + ]; + + const getNextTestCase = testCases[Symbol.iterator](); + function setNextSubscriptionStarted(ctx) { + const mockDetails = {...SubscriptionDetails}; + const nextTestCase = getNextTestCase.next().value; + + if (nextTestCase) { + const [createdAt] = nextTestCase; + // We are faking a Stripe subscription, so this value is expected to be + // in seconds. + mockDetails.subscription.created = createdAt() / 1000; + mockDetails.subscription._subscription_type = "web"; + mockDetails.plan.interval = "month"; + + ctx.guardianSubscriptionDetailsCallback = () => { + ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] + .status = 200; + ctx.guardianOverrideEndpoints.GETs['/api/v1/vpn/subscriptionDetails'] + .body = mockDetails; + }; + } + } + + // We call this once before all tests to set up the first test, + // we can't use beforeEach because that is executed after the guardian + // endpoints are overriden. + // + // We need to setup for the next test before it even starts for the + // overrides to apply. + setNextSubscriptionStarted(this.ctx); + afterEach(() => setNextSubscriptionStarted(this.ctx)); + + testCases.forEach(([createdAtTimestamp, expectedTimeFormat, shouldBeAvailable, testCase]) => { + it.only(`message display is correct when subscription started at ${testCase}`, async () => { + await vpn.resetAddons('prod'); + + //Load messages + const loadedMessages = await vpn.messages(); + + //If the message is supposed to be enabled, lets check the timestamp + if (shouldBeAvailable) { + await vpn.waitForCondition(async () => (parseInt(await vpn.getMozillaProperty('Mozilla.Shared', 'MZAddonManager', 'count'), 10) > 0)); + + await vpn.waitForQueryAndClick(queries.navBar.MESSAGES.visible()); + await vpn.waitForQuery(queries.screenMessaging.SCREEN.visible()); + + //Check timestamp + let expectedTimestamp + let actualTimestamp = await vpn.getQueryProperty(queries.screenMessaging.messageItem('message_upgrade_to_annual_plan'), 'formattedDate'); + //Maybe add 14 days to account for the timestamp that starts 14 days into the subscription + const addedTime = Date.now() > Date.now() - (14 * 24 * 60 * 60 * 1000) ? 1000 * 60 * 60 * 24 * 14 : 0; + + if (expectedTimeFormat === "time") { + expectedTimestamp = new Date(createdAtTimestamp() + addedTime).toLocaleTimeString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }); + } + else if (expectedTimeFormat === "date") { + expectedTimestamp = new Date(createdAtTimestamp() + addedTime).toLocaleDateString('en-US', { month: 'numeric', day: 'numeric', year: '2-digit' }); + } + else if (expectedTimeFormat === "yesterday") { + expectedTimestamp = "Yesterday"; + } + + assert.equal(actualTimestamp, expectedTimestamp); + } + assert.equal(shouldBeAvailable, loadedMessages.includes('message_upgrade_to_annual_plan')); + + }); + }); + }); }); diff --git a/tests/unit_tests/addons/addons.qrc b/tests/unit_tests/addons/addons.qrc index ca9bc5eb4f..5ca4537119 100644 --- a/tests/unit_tests/addons/addons.qrc +++ b/tests/unit_tests/addons/addons.qrc @@ -13,6 +13,7 @@ api_featurelist.js api_foobar.js api_navigator.js + api_settimedcallback.js api_settings.js api_urlopener.js diff --git a/tests/unit_tests/addons/api_settimedcallback.js b/tests/unit_tests/addons/api_settimedcallback.js new file mode 100644 index 0000000000..e550f93576 --- /dev/null +++ b/tests/unit_tests/addons/api_settimedcallback.js @@ -0,0 +1,3 @@ +(function(api, condition) { + api.setTimedCallback(1000, () => condition.enable()); +}); diff --git a/tests/unit_tests/testaddonapi.cpp b/tests/unit_tests/testaddonapi.cpp index 0ad0c5e68c..5150563a7e 100644 --- a/tests/unit_tests/testaddonapi.cpp +++ b/tests/unit_tests/testaddonapi.cpp @@ -186,4 +186,44 @@ void TestAddonApi::foobar() { AddonApi::setConstructorCallback(nullptr); } +void TestAddonApi::settimedcallback() { + SettingsHolder settingsHolder; + Localizer l; + + QQmlApplicationEngine engine; + QmlEngineHolder qml(&engine); + + QJsonObject content; + content["id"] = "foo"; + content["blocks"] = QJsonArray(); + + QJsonObject obj; + obj["message"] = content; + + QObject parent; + Addon* message = AddonMessage::create(&parent, "foo", "bar", "name", obj); + QVERIFY(!!message); + + AddonConditionWatcher* a = AddonConditionWatcherJavascript::maybeCreate( + message, ":/addons_test/api_settimedcallback.js"); + QVERIFY(!!a); + + QTimer timer; + + int timeoutPeriodMsec = 1000; + + timer.setSingleShot(true); + timer.start(timeoutPeriodMsec); + + QSignalSpy spy(&timer, &QTimer::timeout); + + // Give the slot time to execute + QTest::qWait(timeoutPeriodMsec + 1000); + + QObject::connect(&timer, &QTimer::timeout, + [&]() { QVERIFY(a->conditionApplied()); }); + + QCOMPARE(spy.count(), 1); +} + static TestAddonApi s_testAddonApi; diff --git a/tests/unit_tests/testaddonapi.h b/tests/unit_tests/testaddonapi.h index 9ac10be0f8..9a5828960f 100644 --- a/tests/unit_tests/testaddonapi.h +++ b/tests/unit_tests/testaddonapi.h @@ -14,4 +14,5 @@ class TestAddonApi final : public TestHelper { void settings(); void urlopener(); void foobar(); + void settimedcallback(); };