mirror of
https://github.com/discourse/discourse.git
synced 2026-03-03 23:54:20 +08:00
## Summary
- Improves scroll position stability when loading posts above the
viewport by using `scrollBy`-based compensation instead of `scrollTo` +
`offsetCalculator`
- Dynamically hides banners, notices, and footer content when the user
is in the middle of a long topic via a new `scrollState` service and
`{{hideScrollableContent}}` helper
- Triggers loading more posts when navigating to boundary posts via
keyboard (`j`/`k` keys)
- Prevents load-more sentinel from re-triggering during scroll
compensation with a `suppressLoadAbove` flag
## Details
### Scroll compensation
When new posts are prepended above the viewport, the component captures
the anchor post's position via `getBoundingClientRect()` before DOM
insertion (using a `beforePrepend` callback in `prependMore()`), then
compensates with `window.scrollBy()` after insertion. This keeps the
reading position visually stable.
### Content hiding
A `scrollState` service tracks ref-counted hide requests. The
`{{hideScrollableContent}}` class-based helper registers with the
service and automatically cleans up via `registerDestructor`. The
`application.gjs` and `topic.gjs` templates wrap scrollable content in
`{{#unless shouldHideScrollableContentAbove/Below}}` blocks.
The helper invocations live in `topic.gjs` outside the `{{#unless
loadingFilter}}` guard so content stays hidden during timeline jumps
that temporarily unmount `<PostStream>`.
### Keyboard navigation
`updateKeyboardSelectedPostNumber` now checks if the selected post is at
a loaded boundary (`firstAvailablePost` / `lastAvailablePost`) and
triggers `loadMoreAbove` / `loadMoreBelow`. Double-loading is prevented
by existing `canPrependMore` / `canAppendMore` guards.
179 lines
6.3 KiB
Ruby
179 lines
6.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
describe "Post stream", type: :system do
|
|
fab!(:user)
|
|
fab!(:admin)
|
|
|
|
context "when posting" do
|
|
let(:topic) { Fabricate(:topic, user: user) }
|
|
let(:post) { Fabricate(:post, topic: topic, user: user) }
|
|
|
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
|
let(:composer) { PageObjects::Components::Composer.new }
|
|
|
|
before do
|
|
SiteSetting.post_menu_hidden_items = ""
|
|
|
|
sign_in(admin)
|
|
end
|
|
|
|
it "can reply to a post and edit the reply" do
|
|
# Visit topic and initiate reply
|
|
topic_page.visit_topic(post.topic)
|
|
topic_page.click_post_action_button(post, :reply)
|
|
expect(composer).to be_opened
|
|
|
|
# Create initial reply
|
|
composer.type_content("This is a reply")
|
|
composer.submit
|
|
expect(composer).to be_closed
|
|
|
|
# Verify initial reply
|
|
reply = Post.last
|
|
expect(topic_page).to have_post_number(reply.post_number)
|
|
expect(topic_page).to have_post_content(
|
|
post_number: reply.post_number,
|
|
content: "This is a reply",
|
|
)
|
|
|
|
# Edit the reply
|
|
expect(topic_page).to have_post_action_button(reply, :edit)
|
|
topic_page.click_post_action_button(reply, :edit)
|
|
expect(composer).to be_opened
|
|
composer.type_content("This is an edited reply")
|
|
composer.submit
|
|
|
|
# Verify edited reply
|
|
expect(topic_page).to have_post_action_button(reply, :edit)
|
|
expect(topic_page).to have_post_content(
|
|
post_number: reply.post_number,
|
|
content: "This is an edited reply",
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when hiding scrollable content" do
|
|
let!(:topic) { Fabricate(:topic) }
|
|
let!(:posts) { Fabricate.times(60, :post, topic: topic) }
|
|
let(:topic_page) { PageObjects::Pages::Topic.new }
|
|
|
|
before { sign_in(user) }
|
|
|
|
it "hides and shows content above the post stream based on loaded posts" do
|
|
topic_page.visit_topic(topic, post_number: 30)
|
|
expect(page).to have_css("[data-post-number='30']")
|
|
expect(page).to have_no_css("#main-container")
|
|
|
|
topic_page.visit_topic(topic, post_number: 1)
|
|
expect(page).to have_css("[data-post-number='1']")
|
|
expect(page).to have_css("#main-container")
|
|
|
|
topic_page.visit_topic(topic, post_number: 30)
|
|
expect(page).to have_css("[data-post-number='30']")
|
|
expect(page).to have_no_css("#main-container")
|
|
|
|
visit "/"
|
|
expect(page).to have_css("#main-container")
|
|
end
|
|
end
|
|
|
|
context "when scrolling" do
|
|
let!(:topic) { Fabricate(:topic) }
|
|
let!(:posts) { Fabricate.times(20, :post, topic: topic) }
|
|
|
|
before { sign_in(user) }
|
|
|
|
it "cloaks posts that are far from the viewport" do
|
|
# Visit the topic page
|
|
visit "/t/#{topic.slug}/#{topic.id}/1"
|
|
|
|
# Ensure there are visible posts
|
|
expect(page).to have_css(".topic-post")
|
|
|
|
post_stream = find(".post-stream")
|
|
|
|
# Ensure the first post is uncloaked
|
|
expect(post_stream).to have_css("> [data-post-number='1']")
|
|
expect(post_stream).to have_no_css("> [data-post-number='1'].post-stream--cloaked")
|
|
|
|
# Check that some posts are cloaked (not all posts should be fully rendered at once)
|
|
# The posts that are far from the viewport should have the "post-stream--cloaked" class
|
|
expect(post_stream).to have_css(".post-stream--cloaked")
|
|
|
|
# Get the number of the last post
|
|
last_post_number = topic.highest_post_number
|
|
|
|
# Scroll to the bottom of the page to load more posts
|
|
# We use the :end key to scroll to the bottom
|
|
send_keys(:end)
|
|
|
|
# Verify that the last post is not cloaked
|
|
expect(post_stream).to have_no_css(
|
|
"> [data-post-number='#{last_post_number}'].post-stream--cloaked",
|
|
)
|
|
|
|
# Verify that some posts at the top are now cloaked
|
|
# we can check the first post to see if it's cloaked
|
|
expect(post_stream).to have_css("> [data-post-number='1'].post-stream--cloaked")
|
|
end
|
|
|
|
it "keeps the state of quoted posts after uncloaking them" do
|
|
first_post = posts[0]
|
|
second_post = posts[1]
|
|
second_post.raw =
|
|
"[quote=\"#{first_post.user.username}, post:1, topic:#{topic.id}\"]\nHello\n[/quote]\n#{first_post.raw}"
|
|
|
|
# Rebake the post to process the quote markup
|
|
second_post.rebake!
|
|
|
|
# Visit the topic page
|
|
visit "/t/#{topic.slug}/#{topic.id}/1"
|
|
|
|
post2_selector = "[data-post-number='2']"
|
|
|
|
# Find the post-stream container
|
|
post_stream = find(".post-stream")
|
|
|
|
# Verify the second post is initially visible (uncloaked)
|
|
expect(post_stream).to have_css(post2_selector)
|
|
expect(post_stream).to have_no_css("#{post2_selector}.post-stream--cloaked")
|
|
|
|
# Verify the quote starts collapsed
|
|
expect(post_stream).to have_css("#{post2_selector} aside.quote[data-expanded='false']")
|
|
|
|
# Find the blockquote and verify the initial state
|
|
blockquote = post_stream.find("#{post2_selector} aside.quote blockquote")
|
|
expect(blockquote).to have_content("Hello")
|
|
expect(blockquote).to have_no_content(first_post.raw)
|
|
|
|
# Expand the quote by clicking the toggle button
|
|
post_stream.find(
|
|
"#{post2_selector} aside.quote[data-expanded='false'] button.quote-toggle",
|
|
).click
|
|
expect(post_stream).to have_css("#{post2_selector} aside.quote[data-expanded='true']")
|
|
|
|
# Verify the expanded quote shows full content
|
|
blockquote = post_stream.find("#{post2_selector} aside.quote blockquote")
|
|
expect(blockquote).to have_content(first_post.raw)
|
|
|
|
# Scroll to bottom to trigger post cloaking
|
|
send_keys(:end)
|
|
|
|
# Verify that the second post gets cloaked and the quote is removed
|
|
expect(post_stream).to have_css("#{post2_selector}.post-stream--cloaked")
|
|
expect(post_stream).to have_no_css("#{post2_selector} aside.quote")
|
|
|
|
# Scroll back to top
|
|
send_keys(:home)
|
|
|
|
# Verify that the second post becomes visible again
|
|
expect(post_stream).to have_css(post2_selector)
|
|
expect(post_stream).to have_no_css("#{post2_selector}.post-stream--cloaked")
|
|
|
|
# Verify that the quote maintains an expanded state after uncloaking
|
|
expect(post_stream).to have_css("#{post2_selector} aside.quote[data-expanded='true']")
|
|
blockquote = post_stream.find("#{post2_selector} aside.quote blockquote")
|
|
expect(blockquote).to have_content(first_post.raw)
|
|
end
|
|
end
|
|
end
|