discourse/spec/support/message_bus_test_sync.rb
Jarek Radosz ca8b84c052
DEV: Consistently wait for message_bus in system specs (#40076)
Flakes can happen when a spec publishes via message_bus and then asserts
on the UI. Capybara waits for DOM changes but doesn't know if a message
is still in flight.

This records every server-side publish, then waits for
`MessageBus.callbacks` on the client to catch up.
2026-06-02 22:06:20 +02:00

77 lines
1.9 KiB
Ruby
Vendored

# frozen_string_literal: true
# Records MessageBus publishes in system tests and waits for the client
# to catch up, to prevent flakes in "publish then assert" scenarios.
module MessageBusTestSync
MUTEX = Mutex.new
CATCH_UP_SCRIPT = <<~JS
const [pending, timeoutMs, done] = arguments;
const deadline = Date.now() + timeoutMs;
function poll() {
const callbacks = window.MessageBus?.callbacks ?? [];
const behind = [];
for (const [channel, expectedId] of Object.entries(pending)) {
const lastId = callbacks.find((c) => c.channel === channel)?.last_id;
if (Number.isInteger(lastId) && lastId < expectedId) {
behind.push({ channel, lastId, expectedId });
}
}
if (behind.length === 0 || Date.now() >= deadline) {
done(behind);
} else {
setTimeout(poll, 10);
}
}
poll();
JS
@pending = nil
def self.start
MUTEX.synchronize { @pending = {} }
end
def self.stop
MUTEX.synchronize { @pending = nil }
end
def self.pending?
@pending&.any?
end
def self.record(channel, id)
return if id.nil? || @pending.nil?
MUTEX.synchronize do
next if @pending.nil?
@pending[channel] = id if id > (@pending[channel] || -1)
end
end
# Waits until each recorded id is observed in the browser, or
# `timeout` elapses. Channels with no `last_id` are skipped.
def self.flush!(session, timeout:)
snapshot =
MUTEX.synchronize do
return if @pending.nil? || @pending.empty?
taken = @pending
@pending = {}
taken
end
session.evaluate_async_script(CATCH_UP_SCRIPT, snapshot, (timeout * 1000).to_i)
end
module PublishHook
def publish(channel, data, opts = nil)
id = super
MessageBusTestSync.record(channel, id)
id
end
end
MessageBus::Implementation.prepend(PublishHook)
end