LibWeb: Use a queue to process fullscreen request completions

Instead of immediately firing fullscreenchange, defer that until
WebContent's client has confirmed that it is in fullscreen for the
content. The fullscreenchange is fired by the viewport change, so in
cases where the fullscreen transition is instantaneous (i.e. the
fullscreen state is entered at the exact moment the viewport expands),
the resize event should precede the fullscreenchange event, as the spec
requires.

This fixes the WPT element-request-fullscreen-timing.html test, which
was previously succeeding by accident because we were immediately
fullscreenchange upon requestFullscreen() being called, instead of
following spec and doing the viewport (window) resize in parallel. The
WPT test was actually initially intended to assert that the
fullscreenchange event follows the resize event, but the WPT runner
didn't actually have a different resolution for normal vs fullscreen
viewports, so the resize event doesn't actually fire in their setup. In
our headless mode, the default viewport is 800x600, and the fullscreen
viewport is 1920x1080, so we do fire a resize event when entering
fullscreen. Therefore, that imported test is reverted to assert that
the resize precedes the fullscreenchange.
This commit is contained in:
Zaggy1024
2026-03-16 22:01:36 -05:00
committed by Gregory Bertilson
parent ae9537a53c
commit 2e54c18fb3
Notes: github-actions[bot] 2026-03-17 23:59:34 +00:00
10 changed files with 295 additions and 140 deletions

View File

@@ -6994,60 +6994,7 @@ GC::Ref<WebIDL::Promise> Document::exit_fullscreen()
}
// 8. Return promise, and run the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [&realm, doc, promise, resize] {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// FIXME: 9. Run the fully unlock the screen orientation steps with doc.
// 10. If resize is true, resize docs viewport to its "normal" dimensions.
// NB: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
if (resize)
doc->page().client().page_did_request_exit_fullscreen();
// 11. If docs fullscreen element is null, then resolve promise with undefined and terminate these steps.
if (!doc->fullscreen_element()) {
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
return;
}
// 12. Let exitDocs be the result of collecting documents to unfullscreen given doc.
auto exit_docs = doc->collect_documents_to_unfullscreen();
// 13. Let descendantDocs be an ordered set consisting of docs descendant navigables' active documents whose
// fullscreen element is non-null, if any, in tree order.
auto descendant_docs = realm.heap().allocate<GC::HeapVector<GC::Ref<Document>>>();
for (auto& descendant : doc->descendant_navigables()) {
if (descendant->active_document()->fullscreen_element())
descendant_docs->elements().append(*descendant->active_document());
}
// 14. For each exitDoc in exitDocs:
for (auto& exit_doc : exit_docs->elements()) {
// 1. Append (fullscreenchange, exitDocs fullscreen element) to exitDocs list of pending fullscreen events.
exit_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *exit_doc->fullscreen_element());
// 2. If resize is true, unfullscreen exitDoc.
if (resize)
exit_doc->unfullscreen();
// 3. Otherwise, unfullscreen exitDocs fullscreen element.
else
exit_doc->unfullscreen_element(*exit_doc->fullscreen_element());
}
// 15. For each descendantDoc in descendantDocs:
for (auto& descendant_doc : descendant_docs->elements()) {
// 1. Append (fullscreenchange, descendantDocs fullscreen element) to descendantDocs list of pending fullscreen events.
descendant_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, *descendant_doc->fullscreen_element());
// 2. Unfullscreen descendantDoc.
descendant_doc->unfullscreen();
}
// Note: The order in which documents are unfullscreened is not observable, because run the fullscreen steps is
// invoked in tree order.
// 16. Resolve promise with undefined.
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
}));
page().enqueue_fullscreen_exit(doc, resize, promise);
return promise;
}

View File

@@ -1007,6 +1007,9 @@ public:
GC::Ref<WebIDL::Promise> exit_fullscreen();
void unfullscreen_element(GC::Ref<Element> element);
void unfullscreen();
bool is_simple_fullscreen_document() const;
GC::Ref<GC::HeapVector<GC::Ref<Document>>> collect_documents_to_unfullscreen();
auto& script_blocking_style_sheet_set() { return m_script_blocking_style_sheet_set; }
auto const& script_blocking_style_sheet_set() const { return m_script_blocking_style_sheet_set; }
@@ -1057,9 +1060,6 @@ private:
void evaluate_media_rules();
bool is_simple_fullscreen_document() const;
GC::Ref<GC::HeapVector<GC::Ref<Document>>> collect_documents_to_unfullscreen();
enum class AddLineFeed {
Yes,
No,
@@ -1096,8 +1096,6 @@ private:
void ensure_cookie_version_index(URL::URL const& new_url, URL::URL const& old_url = {});
void unfullscreen();
GC::Ref<Page> m_page;
GC::Ptr<CSS::StyleComputer> m_style_computer;
GC::Ptr<CSS::FontComputer> m_font_computer;

View File

@@ -2462,81 +2462,7 @@ GC::Ref<WebIDL::Promise> Element::request_fullscreen(FullscreenRequester fullscr
}
// 7. Return promise, and run the remaining steps in parallel.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [&realm, error, pending_doc, requesting_element = GC::Ref { *this }, promise]() mutable {
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// NB: Fullscreen API is affected by site-isolation and will require additional work once site-isolation is implemented.
// 8. If error is false, then resize pendingDocs node navigables top-level traversables active documents
// viewports dimensions FIXME: optionally taking into account options["navigationUI"]:
if (error == RequestFullscreenError::False)
pending_doc->page().client().page_did_request_fullscreen_window();
// 9. If any of the following conditions are false, then set error to true:
// * Thiss node document is pendingDoc.
// * The fullscreen element ready check for this returns true.
if (pending_doc != requesting_element->owner_document())
error = RequestFullscreenError::ElementNodeDocIsNotPendingDoc;
if (!requesting_element->is_element_ready_for_fullscreen())
error = RequestFullscreenError::ElementReadyCheckFailed;
// 10. If error is true:
if (error != RequestFullscreenError::False) {
// 1. Append (fullscreenerror, this) to pendingDocs list of pending fullscreen events.
pending_doc->append_pending_fullscreen_change(PendingFullscreenEvent::Type::Error, requesting_element);
// 2. Reject promise with a TypeError exception and terminate these steps.
WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, request_fullscreen_error_to_string(error)));
return;
}
// 11. Let fullscreenElements be an ordered set initially consisting of this.
auto fullscreen_elements = realm.heap().allocate<GC::HeapVector<GC::Ref<Element>>>();
fullscreen_elements->elements().append(requesting_element);
// 12. While true:
while (true) {
// 1. Let last be the last item of fullscreenElements.
auto last = fullscreen_elements->elements().last();
// 2. Let container be lasts node navigables container.
auto container = last->navigable()->container();
// 3. If container is null, then break.
if (!container)
break;
// 4. Append container to fullscreenElements.
fullscreen_elements->elements().append(*container);
}
// 13. For each element in fullscreenElements:
for (auto& element : fullscreen_elements->elements()) {
// 1. Let doc be elements node document.
auto& doc = element->document();
// 2. If element is docs fullscreen element, continue.
if (doc.fullscreen_element() == element) {
// Note: No need to notify observers when nothing has changed.
continue;
}
// 3. If element is this and this is an iframe element, then set elements iframe fullscreen flag.
if (element == requesting_element && requesting_element->is_html_iframe_element()) {
auto& iframe_element = static_cast<HTML::HTMLIFrameElement&>(*element);
iframe_element.set_iframe_fullscreen_flag(true);
}
// 4. Fullscreen element within doc.
doc.fullscreen_element_within_doc(element);
// 5. Append (fullscreenchange, element) to docs list of pending fullscreen events.
doc.append_pending_fullscreen_change(PendingFullscreenEvent::Type::Change, element);
}
// 14. Resolve promise with undefined
WebIDL::resolve_promise(realm, promise, JS::js_undefined());
}));
pending_doc->page().enqueue_fullscreen_enter(*this, *pending_doc, error, promise);
return promise;
}

View File

@@ -272,6 +272,9 @@ public:
};
GC::Ref<WebIDL::Promise> request_fullscreen(FullscreenRequester = FullscreenRequester::Bindings);
RequestFullscreenError is_element_allowed_to_enter_fullscreen(FullscreenRequester) const;
bool is_element_ready_for_fullscreen() const;
void set_fullscreen_flag(bool is_fullscreen) { m_fullscreen_flag = is_fullscreen; }
bool is_fullscreen_element() const { return m_fullscreen_flag; }
@@ -606,8 +609,6 @@ private:
void invalidate_style_after_attribute_change(FlyString const& attribute_name, Optional<String> const& old_value, Optional<String> const& new_value);
void exit_fullscreen_on_element_removal();
RequestFullscreenError is_element_allowed_to_enter_fullscreen(FullscreenRequester) const;
bool is_element_ready_for_fullscreen() const;
WebIDL::ExceptionOr<GC::Ptr<Node>> insert_adjacent(StringView where, GC::Ref<Node> node);

View File

@@ -9,12 +9,15 @@
#include <AK/SourceLocation.h>
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/CSS/StyleComputer.h>
#include <LibWeb/Clipboard/SystemClipboard.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/HTMLSelectElement.h>
@@ -51,6 +54,16 @@ void Page::visit_edges(JS::Cell::Visitor& visitor)
visitor.visit(m_window_rect_observer);
visitor.visit(m_on_pending_dialog_closed);
visitor.visit(m_pending_clipboard_requests);
m_pending_fullscreen_operations.for_each([&](auto const& operation) {
operation.visit([&](PendingFullscreenEnter const& enter_operation) {
visitor.visit(enter_operation.element);
visitor.visit(enter_operation.pending_doc);
visitor.visit(enter_operation.promise); },
[&](PendingFullscreenExit const& exit_operation) {
visitor.visit(exit_operation.doc);
visitor.visit(exit_operation.promise);
});
});
}
HTML::Navigable& Page::focused_navigable()
@@ -851,6 +864,209 @@ void Page::update_find_in_page_selection(Vector<GC::Root<DOM::Range>> matches)
}
}
void Page::enqueue_fullscreen_enter(GC::Ref<DOM::Element> element, GC::Ref<DOM::Document> pending_doc, DOM::RequestFullscreenError error, GC::Ref<WebIDL::Promise> promise)
{
m_pending_fullscreen_operations.enqueue(PendingFullscreenEnter { element, pending_doc, error, promise });
// NOTE: Processing is deferred because the spec says "run the remaining steps in parallel",
// meaning the caller's synchronous JS should complete before we process the operation.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this]() {
process_pending_fullscreen_operations();
}));
}
void Page::enqueue_fullscreen_exit(GC::Ref<DOM::Document> doc, bool resize, GC::Ref<WebIDL::Promise> promise)
{
m_pending_fullscreen_operations.enqueue(PendingFullscreenExit { doc, resize, promise });
// NOTE: Processing is deferred because the spec says "run the remaining steps in parallel",
// meaning the caller's synchronous JS should complete before we process the operation.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this]() {
process_pending_fullscreen_operations();
}));
}
void Page::process_pending_fullscreen_operations()
{
// FIXME: The Fullscreen API interacts with the top-level traversable's viewport. With site-isolation,
// an iframe's content process won't have direct access to this Page, so fullscreen operations
// will need to be routed through IPC to the top-level process.
// NOTE: Resolving/rejecting promises during processing may trigger JS microtasks that re-enter
// this function (e.g., JS calls exitFullscreen() after a requestFullscreen() promise resolves).
// The outer call's while loop will pick up newly enqueued items.
if (m_processing_fullscreen_operations)
return;
m_processing_fullscreen_operations = true;
ScopeGuard guard = [this] { m_processing_fullscreen_operations = false; };
while (!m_pending_fullscreen_operations.is_empty()) {
auto& front = m_pending_fullscreen_operations.head();
auto processed = front.visit(
[&](PendingFullscreenEnter& enter) -> bool {
// https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen
// 8. If error is false, then resize pendingDoc's node navigable's top-level traversable's
// active document's viewport's dimensions, optionally taking into account
// options["navigationUI"]:
if (enter.error == DOM::RequestFullscreenError::False) {
if (m_viewport_is_fullscreen == ViewportIsFullscreen::No) {
if (!m_fullscreen_ipc_sent_to_ui) {
m_client->page_did_request_fullscreen_window();
m_fullscreen_ipc_sent_to_ui = true;
}
// NB: Stop processing here and wait for a change in the fullscreen state if we aren't
// in the desired state yet.
return false;
}
// 9. If any of the following conditions are false, then set error to true:
// * This's node document is pendingDoc.
// * The fullscreen element ready check for this returns true.
if (enter.element->owner_document() != enter.pending_doc.ptr())
enter.error = DOM::RequestFullscreenError::ElementNodeDocIsNotPendingDoc;
else if (!enter.element->is_element_ready_for_fullscreen())
enter.error = DOM::RequestFullscreenError::ElementReadyCheckFailed;
}
auto& realm = enter.element->realm();
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// 10. If error is true:
if (enter.error != DOM::RequestFullscreenError::False) {
// 1. Append (fullscreenerror, this) to pendingDoc's list of pending fullscreen events.
enter.pending_doc->append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Error, enter.element);
// 2. Reject promise with a TypeError exception and terminate these steps.
WebIDL::reject_promise(realm, enter.promise, JS::TypeError::create(realm, DOM::request_fullscreen_error_to_string(enter.error)));
return true;
}
// 11. Let fullscreenElements be an ordered set initially consisting of this.
auto fullscreen_elements = realm.heap().allocate<GC::HeapVector<GC::Ref<DOM::Element>>>();
fullscreen_elements->elements().append(enter.element);
// 12. While true:
while (true) {
// 1. Let last be the last item of fullscreenElements.
auto last = fullscreen_elements->elements().last();
// 2. Let container be last's node navigable's container.
auto container = last->navigable()->container();
// 3. If container is null, then break.
if (!container)
break;
// 4. Append container to fullscreenElements.
fullscreen_elements->elements().append(*container);
}
// 13. For each element in fullscreenElements:
for (auto& element : fullscreen_elements->elements()) {
// 1. Let doc be element's node document.
auto& doc = element->document();
// 2. If element is doc's fullscreen element, continue.
if (doc.fullscreen_element() == element)
continue;
// 3. If element is this and this is an iframe element, then set element's iframe fullscreen flag.
if (element == enter.element && is<HTML::HTMLIFrameElement>(*enter.element))
as<HTML::HTMLIFrameElement>(*element).set_iframe_fullscreen_flag(true);
// 4. Fullscreen element within doc.
doc.fullscreen_element_within_doc(element);
// 5. Append (fullscreenchange, element) to doc's list of pending fullscreen events.
doc.append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Change, element);
}
// 14. Resolve promise with undefined
WebIDL::resolve_promise(realm, enter.promise, JS::js_undefined());
return true;
},
[&](PendingFullscreenExit& exit) -> bool {
auto& realm = exit.doc->realm();
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
// https://fullscreen.spec.whatwg.org/#exit-fullscreen
// FIXME: 9. Run the fully unlock the screen orientation steps with doc.
// 10. If resize is true, resize doc's viewport to its "normal" dimensions.
if (exit.resize && m_viewport_is_fullscreen == ViewportIsFullscreen::Yes) {
if (!m_fullscreen_ipc_sent_to_ui) {
m_client->page_did_request_exit_fullscreen();
m_fullscreen_ipc_sent_to_ui = true;
}
// NB: Stop processing here and wait for a change in the fullscreen state if we aren't
// in the desired state yet.
return false;
}
// 11. If doc's fullscreen element is null, then resolve promise with undefined and terminate these
// steps.
if (!exit.doc->fullscreen_element()) {
WebIDL::resolve_promise(realm, exit.promise, JS::js_undefined());
return true;
}
// 12. Let exitDocs be the result of collecting documents to unfullscreen given doc.
auto exit_docs = exit.doc->collect_documents_to_unfullscreen();
// 13. Let descendantDocs be an ordered set consisting of doc's descendant navigables' active documents
// whose fullscreen element is non-null, if any, in tree order.
auto descendant_docs = realm.heap().allocate<GC::HeapVector<GC::Ref<DOM::Document>>>();
for (auto& descendant : exit.doc->descendant_navigables()) {
if (descendant->active_document()->fullscreen_element())
descendant_docs->elements().append(*descendant->active_document());
}
// 14. For each exitDoc in exitDocs:
for (auto& exit_doc : exit_docs->elements()) {
// 1. Append (fullscreenchange, exitDoc's fullscreen element) to exitDoc's list of pending
// fullscreen events.
exit_doc->append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Change, *exit_doc->fullscreen_element());
// 2. If resize is true, unfullscreen exitDoc.
if (exit.resize)
exit_doc->unfullscreen();
// 3. Otherwise, unfullscreen exitDoc's fullscreen element.
else
exit_doc->unfullscreen_element(*exit_doc->fullscreen_element());
}
// 15. For each descendantDoc in descendantDocs:
for (auto& descendant_doc : descendant_docs->elements()) {
// 1. Append (fullscreenchange, descendantDoc's fullscreen element) to descendantDoc's list of
// pending fullscreen events.
descendant_doc->append_pending_fullscreen_change(DOM::PendingFullscreenEvent::Type::Change, *descendant_doc->fullscreen_element());
// 2. Unfullscreen descendantDoc.
descendant_doc->unfullscreen();
}
// 16. Resolve promise with undefined.
WebIDL::resolve_promise(realm, exit.promise, JS::js_undefined());
return true;
});
if (!processed)
break;
m_pending_fullscreen_operations.dequeue();
}
}
void Page::set_viewport_is_fullscreen(ViewportIsFullscreen is_fullscreen)
{
if (m_viewport_is_fullscreen == is_fullscreen)
return;
m_viewport_is_fullscreen = is_fullscreen;
m_fullscreen_ipc_sent_to_ui = false;
process_pending_fullscreen_operations();
}
}
template<>

View File

@@ -10,6 +10,8 @@
#pragma once
#include <AK/JsonValue.h>
#include <AK/Queue.h>
#include <AK/Variant.h>
#include <LibGC/Root.h>
#include <LibGC/Weak.h>
#include <LibGfx/Cursor.h>
@@ -31,6 +33,7 @@
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/CSS/PreferredContrast.h>
#include <LibWeb/CSS/PreferredMotion.h>
#include <LibWeb/DOM/RequestFullscreenError.h>
#include <LibWeb/Export.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
@@ -43,6 +46,7 @@
#include <LibWeb/Loader/FileRequest.h>
#include <LibWeb/Page/EventResult.h>
#include <LibWeb/Page/InputEvent.h>
#include <LibWeb/Page/ViewportIsFullscreen.h>
#include <LibWeb/Painting/ChromeMetrics.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/StorageAPI/StorageEndpoint.h>
@@ -244,6 +248,13 @@ public:
bool listen_for_dom_mutations() const { return m_listen_for_dom_mutations; }
void set_listen_for_dom_mutations(bool listen_for_dom_mutations) { m_listen_for_dom_mutations = listen_for_dom_mutations; }
void enqueue_fullscreen_enter(GC::Ref<DOM::Element>, GC::Ref<DOM::Document>, DOM::RequestFullscreenError, GC::Ref<WebIDL::Promise>);
void enqueue_fullscreen_exit(GC::Ref<DOM::Document> doc, bool resize, GC::Ref<WebIDL::Promise>);
void process_pending_fullscreen_operations();
ViewportIsFullscreen viewport_is_fullscreen() const { return m_viewport_is_fullscreen; }
void set_viewport_is_fullscreen(ViewportIsFullscreen);
private:
explicit Page(GC::Ref<PageClient>);
virtual void visit_edges(Visitor&) override;
@@ -322,6 +333,26 @@ private:
URL::URL m_last_find_in_page_url;
bool m_listen_for_dom_mutations { false };
struct PendingFullscreenEnter {
GC::Ref<DOM::Element> element;
GC::Ref<DOM::Document> pending_doc;
DOM::RequestFullscreenError error;
GC::Ref<WebIDL::Promise> promise;
};
struct PendingFullscreenExit {
GC::Ref<DOM::Document> doc;
bool resize;
GC::Ref<WebIDL::Promise> promise;
};
using PendingFullscreenOperation = Variant<PendingFullscreenEnter, PendingFullscreenExit>;
Queue<PendingFullscreenOperation> m_pending_fullscreen_operations;
ViewportIsFullscreen m_viewport_is_fullscreen { ViewportIsFullscreen::No };
bool m_fullscreen_ipc_sent_to_ui { false };
bool m_processing_fullscreen_operations { false };
};
enum class DisplayListPlayerType {

View File

@@ -196,10 +196,12 @@ void ConnectionFromClient::traverse_the_history_by_delta(u64 page_id, i32 delta)
page->page().traverse_the_history_by_delta(delta);
}
void ConnectionFromClient::set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen)
void ConnectionFromClient::set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen)
{
if (auto page = this->page(page_id); page.has_value())
if (auto page = this->page(page_id); page.has_value()) {
page->set_viewport(size, device_pixel_ratio);
page->page().set_viewport_is_fullscreen(is_fullscreen);
}
}
void ConnectionFromClient::ready_to_paint(u64 page_id)

View File

@@ -0,0 +1,2 @@
resize 1920x1080
fullscreenchange

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<style>
body { margin: 0; }
#target { width: 100px; height: 100px; }
</style>
<div id="target"></div>
<script src="../include.js"></script>
<script>
asyncTest(done => {
const target = document.getElementById("target");
window.addEventListener("resize", () => {
println(`resize ${window.innerWidth}x${window.innerHeight}`);
});
document.addEventListener("fullscreenchange", () => {
println("fullscreenchange");
});
target.addEventListener("click", () => {
target.requestFullscreen().then(() => {
requestAnimationFrame(() => {
document.exitFullscreen().then(() => done());
});
}, (err) => {
println(`promise rejected: ${err}`);
done();
});
});
internals.click(5, 5);
});
</script>

View File

@@ -22,8 +22,7 @@ promise_test(async t => {
if (event.type == 'fullscreenchange') {
step_timeout(t.unreached_func('timer callback'));
requestAnimationFrame(t.step_func_done(() => {
// Removed 'resize' expectation for now, see https://crbug.com/381127087.
assert_array_equals(events, ['fullscreenchange'], 'event order');
assert_array_equals(events, ['resize', 'fullscreenchange'], 'event order');
resolve();
}));
}