0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-08 15:55:38 +08:00
discourse/spec/jobs/check_new_features_spec.rb
Kris 992f0a6188
UX: suppress new feature notifications on brand new sites (#40135)
Similar to c6d8fcf, this suppresses the "new feature" notifications for
admins on brand new sites <1hr old.

This change sets `last_viewed_feature_date` to the newest item on new
sites, suppressing the initial changes while keeping notifications for
new features that come later.
2026-05-19 09:38:57 +10:00

358 lines
11 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe Jobs::CheckNewFeatures do
def build_feature_hash(id:, created_at:, discourse_version: "2.9.0.beta10")
{
id: id,
user_id: 89_432,
emoji: "👤",
title: "New fancy feature!",
description: "",
link: "https://meta.discourse.org/t/-/238821",
created_at: created_at.iso8601,
updated_at: (created_at + 1.minute).iso8601,
discourse_version: discourse_version,
}
end
def stub_new_features_endpoint(*features)
stub_request(:get, DiscourseUpdates.new_features_full_endpoint_url).to_return(
status: 200,
body: JSON.dump(features),
headers: {
"Content-Type" => "application/json",
},
)
end
let(:permanent_upcoming_changes) do
[
{
setting: :enable_upload_debug_mode,
humanized_name: SiteSetting.humanized_names(:enable_upload_debug_mode),
description: SiteSetting.description(:enable_upload_debug_mode),
upcoming_change: {
learn_more_url: "https://meta.discourse.org/t/-/1234",
image: {
url: "#{Discourse.base_url}/images/upcoming_changes/enable_upload_debug_mode.png",
},
},
},
]
end
def stub_permanent_upcoming_changes!(changes)
UpcomingChanges.stubs(:permanent_upcoming_changes).returns(changes)
end
fab!(:admin1, :admin)
fab!(:admin2, :admin)
let(:feature1) do
build_feature_hash(id: 35, created_at: 3.days.ago, discourse_version: "2.8.1.beta12")
end
let(:feature2) do
build_feature_hash(id: 34, created_at: 2.days.ago, discourse_version: "2.8.1.beta13")
end
let(:pending_feature) do
build_feature_hash(id: 37, created_at: 1.day.ago, discourse_version: "2.8.1.beta14")
end
before do
Migration::Helpers.stubs(:new_site?).returns(false)
DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta13")
freeze_time
stub_new_features_endpoint(feature1, feature2, pending_feature)
UpcomingChanges.stubs(:permanent_upcoming_changes).returns([])
end
after { DiscourseUpdates.clean_state }
it "backfills last viewed feature for admins who don't have last viewed feature" do
DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta12")
DiscourseUpdates.update_new_features([feature1].to_json)
DiscourseUpdates.bump_last_viewed_feature_date(admin1.id, Time.zone.now.iso8601)
described_class.new.execute({})
expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id).iso8601).to eq(
feature1[:created_at],
)
expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id).iso8601).to eq(
Time.zone.now.iso8601,
)
end
it "notifies admins about new features that are available in the site's version" do
Notification.destroy_all
described_class.new.execute({})
expect(
admin1
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
expect(
admin2
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
end
context "when the site is brand new (< 1 hour old)" do
before { Migration::Helpers.stubs(:new_site?).returns(true) }
it "suppresses the back-catalog notification and seeds last_viewed_feature_date" do
Notification.destroy_all
described_class.new.execute({})
expect(
admin1.notifications.where(notification_type: Notification.types[:new_features]),
).to be_empty
expect(
admin2.notifications.where(notification_type: Notification.types[:new_features]),
).to be_empty
# pending_feature is filtered out by version (beta14 > current beta13), so
# feature2 is the newest visible item.
newest_at = Time.zone.parse(feature2[:created_at])
expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id)).to be_within_one_second_of(
newest_at,
)
expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id)).to be_within_one_second_of(
newest_at,
)
end
it "still notifies once a newer feature lands after the site is established" do
Notification.destroy_all
described_class.new.execute({})
Migration::Helpers.stubs(:new_site?).returns(false)
newer_feature =
build_feature_hash(id: 99, created_at: 1.hour.from_now, discourse_version: "2.8.1.beta13")
stub_new_features_endpoint(newer_feature, feature1, feature2, pending_feature)
described_class.new.execute({})
expect(
admin1
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
expect(
admin2
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
end
end
context "when a permanent upcoming change is merged into an empty new-features feed" do
before do
stub_permanent_upcoming_changes!(permanent_upcoming_changes)
UpcomingChanges.stubs(:image_exists?).returns(true)
UpcomingChanges.stubs(:image_data).returns(
{
url: "#{Discourse.base_url}/images/upcoming_changes/enable_upload_debug_mode.png",
width: 244,
height: 66,
file_path: file_from_fixtures("logo.png", "images").path,
},
)
stub_new_features_endpoint(feature1)
end
it "notifies admins and bumps last_viewed_feature_date from the status_changed time" do
Notification.destroy_all
status_changed_at = 1.day.ago
event =
UpcomingChangeEvent.create!(
event_type: :status_changed,
upcoming_change_name: "enable_upload_debug_mode",
event_data: {
"previous_value" => "stable",
"new_value" => "permanent",
},
created_at: status_changed_at,
)
Discourse.cache.delete(UpcomingChanges.current_statuses_cache_key)
described_class.new.execute({})
expect(
admin1
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
expect(
admin2
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
status_changed_at_db = event.reload.created_at
expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id)).to be_within_one_second_of(
status_changed_at_db,
)
expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id)).to be_within_one_second_of(
status_changed_at_db,
)
end
end
context "when persisted feed is older than a permanent upcoming change" do
let(:feature_stale) do
build_feature_hash(id: 99, created_at: 3.days.ago, discourse_version: "2.8.1.beta12")
end
let(:feature_newer_than_uc) do
build_feature_hash(id: 100, created_at: 1.day.ago, discourse_version: "2.8.1.beta13")
end
before do
stub_permanent_upcoming_changes!(permanent_upcoming_changes)
UpcomingChanges.stubs(:image_exists?).returns(true)
UpcomingChanges.stubs(:image_data).returns(
{
url: "#{Discourse.base_url}/images/upcoming_changes/enable_upload_debug_mode.png",
width: 244,
height: 66,
file_path: file_from_fixtures("logo.png", "images").path,
},
)
stub_new_features_endpoint(feature_stale)
end
it "seeds last_viewed to the UC when the fetch adds nothing newer, without notifying" do
Notification.destroy_all
uc_became_permanent_at = 2.days.ago
UpcomingChangeEvent.create!(
event_type: :status_changed,
upcoming_change_name: "enable_upload_debug_mode",
event_data: {
"previous_value" => "stable",
"new_value" => "permanent",
},
created_at: uc_became_permanent_at,
)
Discourse.cache.delete(UpcomingChanges.current_statuses_cache_key)
DiscourseUpdates.update_new_features([feature_stale].to_json)
described_class.new.execute({})
expect(
admin1.notifications.where(
notification_type: Notification.types[:new_features],
read: false,
),
).to be_empty
expect(
admin2.notifications.where(
notification_type: Notification.types[:new_features],
read: false,
),
).to be_empty
expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id)).to be_within_one_second_of(
uc_became_permanent_at,
)
expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id)).to be_within_one_second_of(
uc_became_permanent_at,
)
end
it "notifies and bumps last_viewed to a new feed item newer than the UC" do
Notification.destroy_all
uc_became_permanent_at = 2.days.ago
UpcomingChangeEvent.create!(
event_type: :status_changed,
upcoming_change_name: "enable_upload_debug_mode",
event_data: {
"previous_value" => "stable",
"new_value" => "permanent",
},
created_at: uc_became_permanent_at,
)
Discourse.cache.delete(UpcomingChanges.current_statuses_cache_key)
DiscourseUpdates.update_new_features([feature_stale].to_json)
stub_new_features_endpoint(feature_newer_than_uc, feature_stale)
described_class.new.execute({})
expect(
admin1
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
expect(
admin2
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.count,
).to eq(1)
newer_time = Time.zone.parse(feature_newer_than_uc[:created_at])
expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id)).to be_within_one_second_of(
newer_time,
)
expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id)).to be_within_one_second_of(
newer_time,
)
end
end
it "consolidates new features notifications" do
Notification.destroy_all
described_class.new.execute({})
notification =
admin1
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.first
expect(notification).to be_present
DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta14")
described_class.new.execute({})
# old notification is destroyed
expect(Notification.find_by(id: notification.id)).to eq(nil)
notification =
admin1
.notifications
.where(notification_type: Notification.types[:new_features], read: false)
.first
# new notification is created
expect(notification).to be_present
end
it "doesn't notify admins about features they've already seen" do
Notification.destroy_all
DiscourseUpdates.bump_last_viewed_feature_date(admin1.id, feature2[:created_at])
described_class.new.execute({})
expect(admin1.notifications.count).to eq(0)
expect(
admin2.notifications.where(notification_type: Notification.types[:new_features]).count,
).to eq(1)
end
end