mirror of
https://github.com/discourse/discourse.git
synced 2025-06-07 18:54:44 +08:00

A system test in `system/search_spec.rb` was failing with the following error frequently on CI: ``` Failure/Error: expect(search_page).to have_heading_text("Search") expected `#<PageObjects::Pages::Search:0x00007fb9fcd3f028>.has_heading_text?("Search")` to be truthy, got false [Screenshot Image]: /__w/discourse/discourse/tmp/capybara/failures_r_spec_example_groups_search_when_using_full_page_search_on_mobile_works_and_clears_search_page_state_912.png ~~~~~~~ JS LOGS ~~~~~~~ (no logs) ~~~~~ END JS LOGS ~~~~~ ./spec/system/search_spec.rb:42:in `block (3 levels) in <main>' ./spec/rails_helper.rb:619:in `block (3 levels) in <top (required)>' /var/www/discourse/vendor/bundle/ruby/3.3.0/gems/benchmark-0.4.0/lib/benchmark.rb:304:in `measure' ./spec/rails_helper.rb:619:in `block (2 levels) in <top (required)>' ./spec/rails_helper.rb:580:in `block (3 levels) in <top (required)>' /var/www/discourse/vendor/bundle/ruby/3.3.0/gems/timeout-0.4.3/lib/timeout.rb:185:in `block in timeout' /var/www/discourse/vendor/bundle/ruby/3.3.0/gems/timeout-0.4.3/lib/timeout.rb:192:in `timeout' ./spec/rails_helper.rb:570:in `block (2 levels) in <top (required)>' ./spec/rails_helper.rb:527:in `block (2 levels) in <top (required)>' /var/www/discourse/vendor/bundle/ruby/3.3.0/gems/webmock-3.25.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>' ``` The failure screenshot shows that the "user" is on the homepage even though we have already clicked the search icon and ensured that the user can see the search container. I suspect there is some sort of race condition here since Capybara executes clicks in quick sucession where we clicked on both the homepage logo and the search icon. It may be possible that Ember redirected the user to the search page first before the browser was able to finish navigating the user to the `/` href. ### Reviewer notes Test flaked in https://github.com/discourse/discourse/actions/runs/14085443789/job/39448197089 with the following failure screenshot: 
224 lines
7.0 KiB
Ruby
224 lines
7.0 KiB
Ruby
# frozen_string_literal: true
|
|
require "highline/import"
|
|
|
|
module SystemHelpers
|
|
PLATFORM_KEY_MODIFIER = RUBY_PLATFORM =~ /darwin/i ? :meta : :control
|
|
|
|
def pause_test
|
|
msg = "Test paused. Press enter to resume, or `d` + enter to start debugger.\n\n"
|
|
msg += "Browser inspection URLs:\n"
|
|
|
|
base_url = page.driver.browser.send(:devtools_address)
|
|
uri = URI(base_url)
|
|
response = Net::HTTP.get(uri.hostname, "/json/list", uri.port)
|
|
|
|
socat_pid = nil
|
|
|
|
if exposed_port = ENV["SELENIUM_FORWARD_DEVTOOLS_TO_PORT"]
|
|
socat_pid =
|
|
fork do
|
|
chrome_port = uri.port
|
|
exec "socat tcp-listen:#{exposed_port},reuseaddr,fork tcp:localhost:#{chrome_port}"
|
|
end
|
|
end
|
|
|
|
# Fetch devtools urls
|
|
base_url = page.driver.browser.send(:devtools_address)
|
|
uri = URI(base_url)
|
|
response = Net::HTTP.get(uri.hostname, "/json/list", uri.port)
|
|
JSON
|
|
.parse(response)
|
|
.each do |result|
|
|
devtools_url = "#{base_url}#{result["devtoolsFrontendUrl"]}"
|
|
|
|
devtools_url = devtools_url.gsub(":#{uri.port}", ":#{exposed_port}") if exposed_port
|
|
|
|
if ENV["CODESPACE_NAME"]
|
|
devtools_url =
|
|
devtools_url
|
|
.gsub(
|
|
"localhost:#{exposed_port}",
|
|
"#{ENV["CODESPACE_NAME"]}-#{exposed_port}.#{ENV["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"]}",
|
|
)
|
|
.gsub("http://", "https://")
|
|
.gsub("ws=", "wss=")
|
|
end
|
|
|
|
msg += " - (#{result["type"]}) #{devtools_url} (#{URI(result["url"]).path})\n"
|
|
end
|
|
|
|
result = ask("\n\e[33m#{msg}\e[0m")
|
|
binding.pry if result == "d" # rubocop:disable Lint/Debugger
|
|
puts "\e[33mResuming...\e[0m"
|
|
Process.kill("TERM", socat_pid) if socat_pid
|
|
self
|
|
end
|
|
|
|
def sign_in(user)
|
|
visit File.join(
|
|
GlobalSetting.relative_url_root || "",
|
|
"/session/#{user.encoded_username}/become.json?redirect=false",
|
|
)
|
|
|
|
expect(page).to have_content("Signed in to #{user.encoded_username} successfully")
|
|
end
|
|
|
|
def setup_system_test
|
|
SiteSetting.login_required = false
|
|
SiteSetting.has_login_hint = false
|
|
SiteSetting.force_hostname = Capybara.server_host
|
|
SiteSetting.port = Capybara.server_port
|
|
SiteSetting.external_system_avatars_enabled = false
|
|
SiteSetting.disable_avatar_education_message = true
|
|
SiteSetting.enable_user_tips = false
|
|
SiteSetting.splash_screen = false
|
|
SiteSetting.allowed_internal_hosts =
|
|
(
|
|
SiteSetting.allowed_internal_hosts.to_s.split("|") +
|
|
MinioRunner.config.minio_urls.map { |url| URI.parse(url).host }
|
|
).join("|")
|
|
end
|
|
|
|
def try_until_success(timeout: Capybara.default_max_wait_time, frequency: 0.01)
|
|
start ||= Time.zone.now
|
|
backoff ||= frequency
|
|
yield
|
|
rescue RSpec::Expectations::ExpectationNotMetError,
|
|
Capybara::ExpectationNotMet,
|
|
Capybara::ElementNotFound
|
|
raise if Time.zone.now >= start + timeout.seconds
|
|
sleep backoff
|
|
backoff += frequency
|
|
retry
|
|
end
|
|
|
|
def wait_for_attribute(
|
|
element,
|
|
attribute,
|
|
value,
|
|
timeout: Capybara.default_max_wait_time,
|
|
frequency: 0.01
|
|
)
|
|
try_until_success(timeout: timeout, frequency: frequency) do
|
|
expect(element[attribute.to_sym]).to eq(value)
|
|
end
|
|
end
|
|
|
|
# Waits for an element to stop animating up to timeout seconds,
|
|
# then raises a Capybara error if it does not stop.
|
|
#
|
|
# This is based on getBoundingClientRect, where Y is the distance
|
|
# from the top of the element to the top of the viewport, and X
|
|
# is the distance from the leftmost edge of the element to the
|
|
# left of the viewport. The viewpoint origin (0, 0) is at the
|
|
# top left of the page.
|
|
#
|
|
# Once X and Y stop changing based on the current vs previous position,
|
|
# then we know the animation has stopped and the element is stabilised,
|
|
# at which point we can click on it without fear of Capybara mis-clicking.
|
|
#
|
|
# c.f. https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
|
def wait_for_animation(element, timeout: Capybara.default_max_wait_time)
|
|
old_element_x = nil
|
|
old_element_y = nil
|
|
|
|
try_until_success(timeout: timeout) do
|
|
current_element_x = element.rect.x
|
|
current_element_y = element.rect.y
|
|
|
|
stopped_moving = current_element_x == old_element_x && current_element_y == old_element_y
|
|
|
|
old_element_x = current_element_x
|
|
old_element_y = current_element_y
|
|
|
|
raise Capybara::ExpectationNotMet if !stopped_moving
|
|
end
|
|
end
|
|
|
|
def resize_window(width: nil, height: nil)
|
|
original_size = page.driver.browser.manage.window.size
|
|
page.driver.browser.manage.window.resize_to(
|
|
width || original_size.width,
|
|
height || original_size.height,
|
|
)
|
|
yield
|
|
ensure
|
|
page.driver.browser.manage.window.resize_to(original_size.width, original_size.height)
|
|
end
|
|
|
|
def using_browser_timezone(timezone, &example)
|
|
using_session(timezone) do
|
|
page.driver.browser.devtools.emulation.set_timezone_override(timezone_id: timezone)
|
|
freeze_time(&example)
|
|
end
|
|
end
|
|
|
|
def select_text_range(selector, start = 0, offset = 5)
|
|
js = <<-JS
|
|
const node = document.querySelector(arguments[0]).childNodes[0];
|
|
const selection = window.getSelection();
|
|
const range = document.createRange();
|
|
range.selectNodeContents(node);
|
|
range.setStart(node, arguments[1]);
|
|
range.setEnd(node, arguments[1] + arguments[2]);
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
JS
|
|
|
|
page.execute_script(js, selector, start, offset)
|
|
end
|
|
|
|
def current_active_element
|
|
{
|
|
classes: page.evaluate_script("document.activeElement.className"),
|
|
id: page.evaluate_script("document.activeElement.id"),
|
|
}
|
|
end
|
|
|
|
def fake_scroll_down_long(selector_to_make_tall = "#main-outlet")
|
|
# Trick to give a huge vertical space to scroll
|
|
page.execute_script(
|
|
"document.querySelector('#{selector_to_make_tall}').style.height = '10000px'",
|
|
)
|
|
page.scroll_to(0, 1000)
|
|
end
|
|
|
|
def setup_or_skip_s3_system_test(enable_secure_uploads: false, enable_direct_s3_uploads: true)
|
|
skip_unless_s3_system_specs_enabled!
|
|
|
|
SiteSetting.enable_s3_uploads = true
|
|
|
|
SiteSetting.s3_upload_bucket = "discoursetest"
|
|
SiteSetting.enable_upload_debug_mode = true
|
|
|
|
SiteSetting.s3_access_key_id = MinioRunner.config.minio_root_user
|
|
SiteSetting.s3_secret_access_key = MinioRunner.config.minio_root_password
|
|
SiteSetting.s3_endpoint = MinioRunner.config.minio_server_url
|
|
|
|
SiteSetting.enable_direct_s3_uploads = enable_direct_s3_uploads
|
|
SiteSetting.secure_uploads = enable_secure_uploads
|
|
|
|
MinioRunner.start
|
|
end
|
|
|
|
def skip_unless_s3_system_specs_enabled!
|
|
if !ENV["CI"] && !ENV["RUN_S3_SYSTEM_SPECS"]
|
|
skip(
|
|
"S3 system specs are disabled in this environment, set CI=1 or RUN_S3_SYSTEM_SPECS=1 to enable them.",
|
|
)
|
|
end
|
|
end
|
|
|
|
def skip_on_ci!(message = "Flaky on CI")
|
|
skip(message) if ENV["CI"]
|
|
end
|
|
|
|
def click_logo
|
|
PageObjects::Components::Logo.new.click
|
|
end
|
|
|
|
def is_mobile?
|
|
!!RSpec.current_example.metadata[:mobile]
|
|
end
|
|
end
|