-
{{i18n
- "discourse_rewind.reports.top_words.title"
- }}
-
- {{#each this.topWords as |entry index|}}
-
- {{/each}}
-
+const WordCards =
+
+
+
{{i18n
+ "discourse_rewind.reports.top_words.title"
+ }}
+
+ {{#each @report.data as |entry index|}}
+
+ {{/each}}
-
-}
+
+;
+
+export default WordCards;
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 975ba8c..f13c8d1 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -13,6 +13,12 @@ en:
reports:
activity_calendar:
title: Activity Calendar
+ cell_title:
+ visited_no_posts: "You visited and lurked on %{date}, but made no posts."
+ visited_with_posts:
+ one: "You made %{count} post on %{date}."
+ other: "You made %{count} posts on %{date}."
+ no_activity: "You weren't around on %{date}."
top_words:
title: Word Usage
reading_time:
diff --git a/spec/actions/activity_calendar_spec.rb b/spec/actions/activity_calendar_spec.rb
new file mode 100644
index 0000000..779514b
--- /dev/null
+++ b/spec/actions/activity_calendar_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::ActivityCalendar do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:other_user, :user)
+
+ fab!(:post_1) { Fabricate(:post, user: user, created_at: Date.new(2021, 1, 15)) }
+ fab!(:post_2) { Fabricate(:post, user: user, created_at: Date.new(2021, 6, 27)) }
+ fab!(:post_3) { Fabricate(:post, user: user, created_at: Date.new(2021, 6, 27)) }
+ fab!(:post_4) { Fabricate(:post, user: user, created_at: Date.new(2021, 11, 27)) }
+ fab!(:post_5) { Fabricate(:post, user: other_user, created_at: Date.new(2021, 11, 27)) }
+ fab!(:post_6) { Fabricate(:post, user: user, created_at: Date.new(2022, 02, 27)) }
+
+ fab!(:user_visit_1) do
+ UserVisit.create!(
+ user_id: user.id,
+ visited_at: Date.new(2021, 3, 10),
+ posts_read: 5,
+ time_read: 120,
+ )
+ end
+ fab!(:user_visit_2) do
+ UserVisit.create!(
+ user_id: user.id,
+ visited_at: Date.new(2021, 4, 18),
+ posts_read: 12,
+ time_read: 1200,
+ )
+ end
+ fab!(:user_visit_3) do
+ UserVisit.create!(
+ user_id: other_user.id,
+ visited_at: Date.new(2021, 7, 24),
+ posts_read: 12,
+ time_read: 1200,
+ )
+ end
+
+ it "returns an entry for all days of the last year" do
+ result = call_report
+ expect(result[:data].map { |d| d[:date] }.count).to eq(365)
+ end
+
+ it "counts up posts for the user on days they were made in the year" do
+ result = call_report
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 1, 15) }[:post_count]).to eq(1)
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 6, 27) }[:post_count]).to eq(2)
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 11, 27) }[:post_count]).to eq(1)
+ expect(result[:data].find { |d| d[:date] == Date.new(2022, 2, 27) }).to be_nil
+ end
+
+ it "marks dates as visited for the user in the year" do
+ result = call_report
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 3, 10) }[:visited]).to eq(true)
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 4, 18) }[:visited]).to eq(true)
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 5, 1) }[:visited]).to eq(false)
+ end
+
+ context "when a post is deleted" do
+ before { post_1.trash! }
+
+ it "does not count" do
+ result = call_report
+ expect(result[:data].find { |d| d[:date] == Date.new(2021, 1, 15) }[:post_count]).to eq(0)
+ end
+ end
+end
diff --git a/spec/actions/best_posts_spec.rb b/spec/actions/best_posts_spec.rb
new file mode 100644
index 0000000..9c9d4e9
--- /dev/null
+++ b/spec/actions/best_posts_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::BestPosts do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:post_1) { Fabricate(:post, created_at: random_datetime, user: user, post_number: 3) }
+ fab!(:post_2) { Fabricate(:post, created_at: random_datetime, user: user, post_number: 2) }
+ fab!(:post_3) { Fabricate(:post, created_at: random_datetime, user: user, post_number: 10) }
+ fab!(:post_4) { Fabricate(:post, created_at: random_datetime, user: user, post_number: 6) }
+ fab!(:post_5) { Fabricate(:post, created_at: random_datetime, user: user, post_number: 1) }
+
+ describe ".call" do
+ it "returns top 3 posts ordered by like count" do
+ post_4.update!(like_count: 15)
+ post_3.update!(like_count: 13)
+ post_1.update!(like_count: 11)
+ post_2.update!(like_count: 9)
+ post_5.update!(like_count: 7)
+
+ expect(call_report[:data]).to eq(
+ [
+ {
+ post_number: post_4.post_number,
+ topic_id: post_4.topic_id,
+ like_count: post_4.like_count,
+ reply_count: post_4.reply_count,
+ excerpt:
+ post_4.excerpt(200, { strip_links: true, remap_emoji: true, keep_images: true }),
+ },
+ {
+ post_number: post_3.post_number,
+ topic_id: post_3.topic_id,
+ like_count: post_3.like_count,
+ reply_count: post_3.reply_count,
+ excerpt:
+ post_3.excerpt(200, { strip_links: true, remap_emoji: true, keep_images: true }),
+ },
+ {
+ post_number: post_1.post_number,
+ topic_id: post_1.topic_id,
+ like_count: post_1.like_count,
+ reply_count: post_1.reply_count,
+ excerpt:
+ post_1.excerpt(200, { strip_links: true, remap_emoji: true, keep_images: true }),
+ },
+ ],
+ )
+ end
+
+ context "when a post is deleted" do
+ before { post_1.trash!(Discourse.system_user) }
+
+ it "is not included" do
+ expect(call_report[:data].map { |d| d[:post_number] }).not_to include(post_1.post_number)
+ end
+ end
+
+ context "when a post is made by another user" do
+ before { post_1.update!(user: Fabricate(:user)) }
+
+ it "is not included" do
+ expect(call_report[:data].map { |d| d[:post_number] }).not_to include(post_1.post_number)
+ end
+ end
+ end
+end
diff --git a/spec/actions/best_topics_spec.rb b/spec/actions/best_topics_spec.rb
new file mode 100644
index 0000000..1aaa1ba
--- /dev/null
+++ b/spec/actions/best_topics_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::BestTopics do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:topic_1) { Fabricate(:topic, user: user, created_at: random_datetime) }
+ fab!(:topic_2) { Fabricate(:topic, user: user, created_at: random_datetime) }
+ fab!(:topic_3) { Fabricate(:topic, user: user, created_at: random_datetime) }
+ fab!(:topic_4) { Fabricate(:topic, user: user, created_at: random_datetime) }
+ fab!(:topic_5) { Fabricate(:topic, user: user, created_at: random_datetime) }
+
+ before { TopTopic.refresh! }
+
+ describe ".call" do
+ it "returns top 3 topics ordered by yearly_score" do
+ TopTopic.find_by(topic_id: topic_1.id).update!(yearly_score: 15)
+ TopTopic.find_by(topic_id: topic_2.id).update!(yearly_score: 10)
+ TopTopic.find_by(topic_id: topic_3.id).update!(yearly_score: 6)
+ TopTopic.find_by(topic_id: topic_4.id).update!(yearly_score: 11)
+ TopTopic.find_by(topic_id: topic_5.id).update!(yearly_score: 13)
+ expect(call_report[:data]).to eq(
+ [
+ {
+ topic_id: topic_1.id,
+ title: topic_1.title,
+ excerpt: topic_1.excerpt,
+ yearly_score: 15,
+ },
+ {
+ topic_id: topic_5.id,
+ title: topic_5.title,
+ excerpt: topic_5.excerpt,
+ yearly_score: 13,
+ },
+ {
+ topic_id: topic_4.id,
+ title: topic_4.title,
+ excerpt: topic_4.excerpt,
+ yearly_score: 11,
+ },
+ ],
+ )
+ end
+
+ context "when a topic is deleted" do
+ before { topic_1.trash!(Discourse.system_user) }
+
+ it "is not included" do
+ expect(call_report[:data].map { |d| d[:topic_id] }).not_to include(topic_1.id)
+ end
+ end
+
+ context "when a topic" do
+ before { topic_1.update!(user: Fabricate(:user)) }
+
+ it "is not included" do
+ expect(call_report[:data].map { |d| d[:topic_id] }).not_to include(topic_1.id)
+ end
+ end
+ end
+end
diff --git a/spec/actions/most_viewed_categories_spec.rb b/spec/actions/most_viewed_categories_spec.rb
new file mode 100644
index 0000000..bbefeb6
--- /dev/null
+++ b/spec/actions/most_viewed_categories_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::MostViewedCategories do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:other_user, :user)
+
+ fab!(:category_1) { Fabricate(:category, name: "Technology") }
+ fab!(:category_2) { Fabricate(:category, name: "Science") }
+ fab!(:category_3) { Fabricate(:category, name: "Philosophy") }
+ fab!(:category_4) { Fabricate(:category, name: "Literature") }
+ fab!(:category_5) { Fabricate(:category, name: "History") }
+
+ fab!(:topic_1) { Fabricate(:topic, category: category_1) }
+ fab!(:topic_2) { Fabricate(:topic, category: category_1) }
+ fab!(:topic_3) { Fabricate(:topic, category: category_2) }
+ fab!(:topic_4) { Fabricate(:topic, category: category_3) }
+ fab!(:topic_5) { Fabricate(:topic, category: category_4) }
+ fab!(:topic_6) { Fabricate(:topic, category: category_5) }
+
+ describe ".call" do
+ it "returns top 4 most viewed categories ordered by view count" do
+ # Category 1: 2 views
+ TopicViewItem.add(topic_1.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(topic_2.id, "127.0.0.2", user.id, Date.new(2021, 4, 20))
+
+ # Category 2: 1 view
+ TopicViewItem.add(topic_3.id, "127.0.0.3", user.id, Date.new(2021, 5, 10))
+
+ # Category 3: 1 view
+ TopicViewItem.add(topic_4.id, "127.0.0.4", user.id, Date.new(2021, 6, 5))
+
+ # Category 4: 3 views (same topic, multiple views)
+ TopicViewItem.add(topic_5.id, "127.0.0.5", user.id, Date.new(2021, 7, 1))
+ TopicViewItem.add(topic_5.id, "127.0.0.6", user.id, Date.new(2021, 8, 15))
+ TopicViewItem.add(topic_5.id, "127.0.0.7", user.id, Date.new(2021, 9, 20))
+
+ # Category 5: 0 views
+
+ result = call_report
+
+ expect(result[:identifier]).to eq("most-viewed-categories")
+ expect(result[:data].length).to eq(4)
+ expect(result[:data]).to eq(
+ [
+ { category_id: category_1.id, name: "Technology" },
+ { category_id: category_2.id, name: "Science" },
+ { category_id: category_3.id, name: "Philosophy" },
+ { category_id: category_4.id, name: "Literature" },
+ ],
+ )
+ end
+
+ it "only includes categories the user can see (no read-restricted/private categories)" do
+ group = Fabricate(:group)
+ private_category = Fabricate(:private_category, group: group)
+ private_topic = Fabricate(:topic, category: private_category)
+
+ TopicViewItem.add(private_topic.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+
+ result = call_report
+ expect(result[:data].map { |c| c[:category_id] }).not_to include(private_category.id)
+ end
+
+ it "filters by date range" do
+ TopicViewItem.add(topic_1.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(topic_2.id, "127.0.0.2", user.id, Date.new(2020, 12, 31))
+
+ result = call_report
+
+ expect(result[:data].length).to eq(1)
+ expect(result[:data].first[:category_id]).to eq(category_1.id)
+ end
+
+ it "only counts views for the specific user" do
+ TopicViewItem.add(topic_1.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(topic_2.id, "127.0.0.2", other_user.id, Date.new(2021, 4, 20))
+
+ result = call_report
+
+ expect(result[:data].length).to eq(1)
+ expect(result[:data].first[:category_id]).to eq(category_1.id)
+ end
+
+ it "returns empty array when no views" do
+ result = call_report
+
+ expect(result[:identifier]).to eq("most-viewed-categories")
+ expect(result[:data]).to eq([])
+ end
+ end
+end
diff --git a/spec/actions/most_viewed_tags_spec.rb b/spec/actions/most_viewed_tags_spec.rb
new file mode 100644
index 0000000..9d03745
--- /dev/null
+++ b/spec/actions/most_viewed_tags_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::MostViewedTags do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:other_user, :user)
+
+ fab!(:tag_1) { Fabricate(:tag, name: "ruby") }
+ fab!(:tag_2) { Fabricate(:tag, name: "javascript") }
+ fab!(:tag_3) { Fabricate(:tag, name: "python") }
+ fab!(:tag_4) { Fabricate(:tag, name: "golang") }
+ fab!(:tag_5) { Fabricate(:tag, name: "rust") }
+
+ fab!(:topic_1, :topic)
+ fab!(:topic_2, :topic)
+ fab!(:topic_3, :topic)
+ fab!(:topic_4, :topic)
+ fab!(:topic_5, :topic)
+
+ before do
+ SiteSetting.tagging_enabled = true
+
+ topic_1.tags = [tag_1]
+ topic_2.tags = [tag_1]
+ topic_3.tags = [tag_2]
+ topic_4.tags = [tag_3]
+ topic_5.tags = [tag_4]
+ end
+
+ describe ".call" do
+ it "returns top 4 most viewed tags ordered by view count" do
+ # Tag 1 (ruby): 2 views (2 different topics)
+ TopicViewItem.add(topic_1.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(topic_2.id, "127.0.0.2", user.id, Date.new(2021, 4, 20))
+
+ # Tag 2 (javascript): 1 view
+ TopicViewItem.add(topic_3.id, "127.0.0.3", user.id, Date.new(2021, 5, 10))
+
+ # Tag 3 (python): 1 view
+ TopicViewItem.add(topic_4.id, "127.0.0.4", user.id, Date.new(2021, 6, 5))
+
+ # Tag 4 (golang): 3 views (same topic, multiple views)
+ TopicViewItem.add(topic_5.id, "127.0.0.5", user.id, Date.new(2021, 7, 1))
+ TopicViewItem.add(topic_5.id, "127.0.0.6", user.id, Date.new(2021, 8, 15))
+ TopicViewItem.add(topic_5.id, "127.0.0.7", user.id, Date.new(2021, 9, 20))
+
+ # Tag 5 (rust): 0 views
+
+ result = call_report
+
+ expect(result[:data]).to eq(
+ [
+ { tag_id: tag_1.id, name: "ruby" },
+ { tag_id: tag_2.id, name: "javascript" },
+ { tag_id: tag_3.id, name: "python" },
+ { tag_id: tag_4.id, name: "golang" },
+ ],
+ )
+ end
+
+ it "only includes tags the user can see (no restricted tags)" do
+ group = Fabricate(:group)
+ tag_group = Fabricate(:tag_group, tags: [tag_5])
+ tag_group.permissions = { group.name => TagGroupPermission.permission_types[:full] }
+ tag_group.save!
+
+ restricted_topic = Fabricate(:topic)
+ restricted_topic.tags = [tag_5]
+
+ TopicViewItem.add(restricted_topic.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+
+ result = call_report
+ expect(result[:data].map { |t| t[:tag_id] }).not_to include(tag_5.id)
+ end
+
+ it "filters by date range" do
+ TopicViewItem.add(topic_1.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(topic_2.id, "127.0.0.2", user.id, Date.new(2020, 12, 31))
+
+ result = call_report
+
+ expect(result[:data].length).to eq(1)
+ expect(result[:data].first[:tag_id]).to eq(tag_1.id)
+ end
+
+ it "only counts views for the specific user" do
+ TopicViewItem.add(topic_1.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(topic_2.id, "127.0.0.2", other_user.id, Date.new(2021, 4, 20))
+
+ result = call_report
+
+ expect(result[:data].length).to eq(1)
+ expect(result[:data].first[:tag_id]).to eq(tag_1.id)
+ end
+
+ it "counts distinct topics per tag" do
+ multi_tag_topic = Fabricate(:topic)
+ multi_tag_topic.tags = [tag_1, tag_2]
+
+ TopicViewItem.add(multi_tag_topic.id, "127.0.0.1", user.id, Date.new(2021, 3, 15))
+ TopicViewItem.add(multi_tag_topic.id, "127.0.0.2", user.id, Date.new(2021, 4, 20))
+
+ result = call_report
+
+ tag_1_data = result[:data].find { |t| t[:tag_id] == tag_1.id }
+ tag_2_data = result[:data].find { |t| t[:tag_id] == tag_2.id }
+ expect(tag_1_data).not_to be_nil
+ expect(tag_2_data).not_to be_nil
+ end
+
+ it "returns empty array when no views" do
+ result = call_report
+
+ expect(result[:data]).to eq([])
+ end
+ end
+end
diff --git a/spec/actions/reading_time_spec.rb b/spec/actions/reading_time_spec.rb
new file mode 100644
index 0000000..7b9429e
--- /dev/null
+++ b/spec/actions/reading_time_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::ReadingTime do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:other_user, :user)
+
+ fab!(:user_visit_1) do
+ UserVisit.create!(
+ user_id: user.id,
+ visited_at: Date.new(2021, 3, 10),
+ posts_read: 5,
+ time_read: 100,
+ )
+ end
+ fab!(:user_visit_2) do
+ UserVisit.create!(
+ user_id: user.id,
+ visited_at: Date.new(2021, 4, 18),
+ posts_read: 12,
+ time_read: 1000,
+ )
+ end
+ fab!(:user_visit_3) do
+ UserVisit.create!(
+ user_id: other_user.id,
+ visited_at: Date.new(2021, 7, 24),
+ posts_read: 8,
+ time_read: 1200,
+ )
+ end
+
+ def new_target_time_read(value)
+ value - 1000
+ end
+
+ it "calculates reading time for the year correctly" do
+ result = call_report
+ expect(result[:data][:reading_time]).to eq(1100)
+ end
+
+ it "matches the correct book based on reading time" do
+ result = call_report
+ expect(result[:data][:book]).to eq("The Metamorphosis")
+
+ user_visit_1.update!(time_read: new_target_time_read(5300))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Little Prince")
+
+ user_visit_1.update!(time_read: new_target_time_read(7100))
+ result = call_report
+ expect(result[:data][:book]).to eq("Animal Farm")
+
+ user_visit_1.update!(time_read: new_target_time_read(10_700))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Alchemist")
+
+ user_visit_1.update!(time_read: new_target_time_read(12_500))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Great Gatsby")
+
+ user_visit_1.update!(time_read: new_target_time_read(14_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("Fahrenheit 451")
+
+ user_visit_1.update!(time_read: new_target_time_read(16_100))
+ result = call_report
+ expect(result[:data][:book]).to eq("And Then There Were None")
+
+ user_visit_1.update!(time_read: new_target_time_read(16_700))
+ result = call_report
+ expect(result[:data][:book]).to eq("1984")
+
+ user_visit_1.update!(time_read: new_target_time_read(17_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Catcher in the Rye")
+
+ user_visit_1.update!(time_read: new_target_time_read(19_640))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Hunger Games")
+
+ user_visit_1.update!(time_read: new_target_time_read(22_700))
+ result = call_report
+ expect(result[:data][:book]).to eq("To Kill a Mockingbird")
+
+ user_visit_1.update!(time_read: new_target_time_read(24_500))
+ result = call_report
+ expect(result[:data][:book]).to eq("Harry Potter and the Sorcerer's Stone")
+
+ user_visit_1.update!(time_read: new_target_time_read(25_100))
+ result = call_report
+ expect(result[:data][:book]).to eq("Pride and Prejudice")
+
+ user_visit_1.update!(time_read: new_target_time_read(26_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Hobbit")
+
+ user_visit_1.update!(time_read: new_target_time_read(29_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("Little Women")
+
+ user_visit_1.update!(time_read: new_target_time_read(34_100))
+ result = call_report
+ expect(result[:data][:book]).to eq("Jane Eyre")
+
+ user_visit_1.update!(time_read: new_target_time_read(37_700))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Da Vinci Code")
+
+ user_visit_1.update!(time_read: new_target_time_read(46_700))
+ result = call_report
+ expect(result[:data][:book]).to eq("One Hundred Years of Solitude")
+
+ user_visit_1.update!(time_read: new_target_time_read(107_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Lord of the Rings")
+
+ user_visit_1.update!(time_read: new_target_time_read(179_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Complete works of Shakespeare")
+
+ user_visit_1.update!(time_read: new_target_time_read(359_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Game of Thrones Series")
+
+ user_visit_1.update!(time_read: new_target_time_read(719_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("Malazan Book of the Fallen")
+
+ user_visit_1.update!(time_read: new_target_time_read(1_439_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("Terry Pratchett's Discworld series")
+
+ user_visit_1.update!(time_read: new_target_time_read(2_159_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Wandering Inn web series")
+
+ user_visit_1.update!(time_read: new_target_time_read(2_879_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Combined Cosmere works + Wheel of Time")
+
+ user_visit_1.update!(time_read: new_target_time_read(3_599_900))
+ result = call_report
+ expect(result[:data][:book]).to eq("The Star Trek novels")
+ end
+end
diff --git a/spec/actions/top_words_spec.rb b/spec/actions/top_words_spec.rb
new file mode 100644
index 0000000..4f8c5c3
--- /dev/null
+++ b/spec/actions/top_words_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+RSpec.describe DiscourseRewind::Action::TopWords do
+ fab!(:date) { Date.new(2021).all_year }
+ fab!(:user)
+ fab!(:other_user, :user)
+
+ fab!(:post1) do
+ Fabricate(
+ :post,
+ user: user,
+ raw: "apple orange banana apple apple orange",
+ created_at: random_datetime,
+ )
+ end
+ fab!(:post2) do
+ Fabricate(:post, user: user, raw: "cucumber tomato banana orange", created_at: random_datetime)
+ end
+ fab!(:post3) do
+ Fabricate(:post, user: user, raw: "grape watermelon mango", created_at: random_datetime)
+ end
+ fab!(:post4) do
+ Fabricate(:post, user: user, raw: "apple banana grape apple", created_at: random_datetime)
+ end
+ fab!(:post5) do
+ Fabricate(:post, user: user, raw: "apple orange apple apple", created_at: random_datetime)
+ end
+ fab!(:other_user_post) do
+ Fabricate(:post, user: other_user, raw: "apple apple apple", created_at: random_datetime)
+ end
+
+ before do
+ SearchIndexer.enable
+ [post1, post2, post3, post4, post5, other_user_post].each do |post|
+ SearchIndexer.index(post, force: true)
+ end
+ end
+
+ describe ".call" do
+ it "limits top words to 5" do
+ result = call_report
+
+ expect(result[:data].length).to eq(5)
+ end
+
+ it "returns top words ordered by frequency" do
+ result = call_report
+
+ expect(result[:identifier]).to eq("top-words")
+
+ words = result[:data]
+
+ expect(words.first[:word]).to eq("apple")
+ expect(words.second[:word]).to eq("orange")
+ expect(words.third[:word]).to eq("banana")
+
+ expect(words.map { |w| w[:word] }).to include("apple", "orange", "banana", "grape")
+ expect(words.map { |w| w[:score] }).to eq(words.map { |w| w[:score] }.sort.reverse)
+ end
+
+ context "when a post is deleted" do
+ before do
+ post1.trash!(Discourse.system_user)
+ post1.post_search_data.destroy!
+ end
+
+ it "does not include words from deleted posts" do
+ result = call_report
+
+ words = result[:data]
+
+ apple = words.find { |w| w[:word] == "apple" }
+ expect(apple[:score]).to be < 9
+ end
+ end
+
+ context "when posts are from another user" do
+ it "does not include words from other users' posts" do
+ result = call_report
+
+ words = result[:data]
+ apple_score = words.find { |w| w[:word] == "apple" }[:score]
+
+ expect(apple_score).to be < 12
+ end
+ end
+
+ context "with a large number of posts and words" do
+ before do
+ # Create posts with different frequencies of non-stop words
+ 10.times do |i|
+ post =
+ Fabricate(
+ :post,
+ user: user,
+ raw: "#{frequent_word} #{frequent_word} #{frequent_word} #{infrequent_word}",
+ created_at: random_datetime,
+ )
+ SearchIndexer.index(post, force: true)
+ end
+ end
+
+ let(:frequent_word) { "zucchini" }
+ let(:infrequent_word) { "xylophone" }
+
+ it "ranks high frequency words higher than low frequency words" do
+ result = call_report
+
+ words = result[:data]
+ frequent_word_entry = words.find { |w| w[:word] == frequent_word }
+ infrequent_word_entry = words.find { |w| w[:word] == infrequent_word }
+
+ expect(frequent_word_entry).to be_present
+ expect(infrequent_word_entry).to be_present
+
+ expect(frequent_word_entry[:score]).to be > infrequent_word_entry[:score]
+ end
+ end
+ end
+
+ context "when in rails development mode" do
+ before { Rails.env.stubs(:development?).returns(true) }
+
+ it "returns fake data" do
+ result = call_report
+
+ expect(result[:identifier]).to eq("top-words")
+ expect(result[:data].length).to eq(5)
+ expect(result[:data].first[:word]).to eq("seven")
+ expect(result[:data].first[:score]).to eq(100)
+ expect(result[:data].second[:word]).to eq("longest")
+ expect(result[:data].second[:score]).to eq(90)
+ end
+ end
+end
diff --git a/spec/plugin_helper.rb b/spec/plugin_helper.rb
new file mode 100644
index 0000000..8eb5298
--- /dev/null
+++ b/spec/plugin_helper.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module DiscourseRewindSpecHelper
+ def call_report
+ # user + date should be defined via fab! in the spec
+ described_class.call(user:, date:, guardian: user.guardian)
+ end
+
+ def random_datetime
+ # date should be defined via fab! in the spec
+ date.to_a.sample.to_datetime + rand(0..23).hours + rand(0..59).minutes + rand(0..59).seconds
+ end
+end
+
+RSpec.configure { |config| config.include DiscourseRewindSpecHelper }
diff --git a/spec/services/fetch_reports_spec.rb b/spec/services/fetch_reports_spec.rb
index edaeba0..beeb2dc 100644
--- a/spec/services/fetch_reports_spec.rb
+++ b/spec/services/fetch_reports_spec.rb
@@ -31,6 +31,17 @@ RSpec.describe(DiscourseRewind::FetchReports) do
it { is_expected.to fail_to_find_a_model(:year) }
end
+ context "in development mode" do
+ before do
+ Rails.env.stubs(:development?).returns(true)
+ freeze_time DateTime.parse("2021-06-22")
+ end
+
+ it "finds the year no matter what month" do
+ expect(result.year).to eq(2021)
+ end
+ end
+
context "when reports is cached" do
before { freeze_time DateTime.parse("2021-12-22") }