Coverage Report

Created: 2025-11-02 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/HTML/NavigateEvent.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <LibJS/Console.h>
8
#include <LibJS/Heap/Heap.h>
9
#include <LibJS/Runtime/ConsoleObject.h>
10
#include <LibJS/Runtime/Promise.h>
11
#include <LibJS/Runtime/Realm.h>
12
#include <LibWeb/Bindings/Intrinsics.h>
13
#include <LibWeb/Bindings/NavigateEventPrototype.h>
14
#include <LibWeb/DOM/AbortController.h>
15
#include <LibWeb/DOM/AbortSignal.h>
16
#include <LibWeb/DOM/Document.h>
17
#include <LibWeb/HTML/Focus.h>
18
#include <LibWeb/HTML/NavigateEvent.h>
19
#include <LibWeb/HTML/Navigation.h>
20
#include <LibWeb/HTML/NavigationDestination.h>
21
#include <LibWeb/HTML/Window.h>
22
#include <LibWeb/XHR/FormData.h>
23
24
namespace Web::HTML {
25
26
JS_DEFINE_ALLOCATOR(NavigateEvent);
27
28
JS::NonnullGCPtr<NavigateEvent> NavigateEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init)
29
0
{
30
0
    return realm.heap().allocate<NavigateEvent>(realm, realm, event_name, event_init);
31
0
}
32
33
NavigateEvent::NavigateEvent(JS::Realm& realm, FlyString const& event_name, NavigateEventInit const& event_init)
34
0
    : DOM::Event(realm, event_name, event_init)
35
0
    , m_navigation_type(event_init.navigation_type)
36
0
    , m_destination(*event_init.destination)
37
0
    , m_can_intercept(event_init.can_intercept)
38
0
    , m_user_initiated(event_init.user_initiated)
39
0
    , m_hash_change(event_init.hash_change)
40
0
    , m_signal(*event_init.signal)
41
0
    , m_form_data(event_init.form_data)
42
0
    , m_download_request(event_init.download_request)
43
0
    , m_info(event_init.info.value_or(JS::js_undefined()))
44
0
    , m_has_ua_visual_transition(event_init.has_ua_visual_transition)
45
0
{
46
0
}
47
48
0
NavigateEvent::~NavigateEvent() = default;
49
50
void NavigateEvent::initialize(JS::Realm& realm)
51
0
{
52
0
    Base::initialize(realm);
53
0
    WEB_SET_PROTOTYPE_FOR_INTERFACE(NavigateEvent);
54
0
}
55
56
void NavigateEvent::visit_edges(JS::Cell::Visitor& visitor)
57
0
{
58
0
    Base::visit_edges(visitor);
59
0
    visitor.visit(m_navigation_handler_list);
60
0
    visitor.visit(m_abort_controller);
61
0
    visitor.visit(m_destination);
62
0
    visitor.visit(m_signal);
63
0
    visitor.visit(m_form_data);
64
0
    visitor.visit(m_info);
65
0
}
66
67
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-intercept
68
WebIDL::ExceptionOr<void> NavigateEvent::intercept(NavigationInterceptOptions const& options)
69
0
{
70
0
    auto& realm = this->realm();
71
0
    auto& vm = this->vm();
72
    // The intercept(options) method steps are:
73
74
    // 1. Perform shared checks given this.
75
0
    TRY(perform_shared_checks());
76
77
    // 2. If this's canIntercept attribute was initialized to false, then throw a "SecurityError" DOMException.
78
0
    if (!m_can_intercept)
79
0
        return WebIDL::SecurityError::create(realm, "NavigateEvent cannot be intercepted"_string);
80
81
    // 3. If this's dispatch flag is unset, then throw an "InvalidStateError" DOMException.
82
0
    if (!this->dispatched())
83
0
        return WebIDL::InvalidStateError::create(realm, "NavigationEvent is not dispatched yet"_string);
84
85
    // 4. Assert: this's interception state is either "none" or "intercepted".
86
0
    VERIFY(m_interception_state == InterceptionState::None || m_interception_state == InterceptionState::Intercepted);
87
88
    // 5. Set this's interception state to "intercepted".
89
0
    m_interception_state = InterceptionState::Intercepted;
90
91
    // 6. If options["handler"] exists, then append it to this's navigation handler list.
92
0
    if (options.handler != nullptr)
93
0
        TRY_OR_THROW_OOM(vm, m_navigation_handler_list.try_append(*options.handler));
94
95
    // 7. If options["focusReset"] exists, then:
96
0
    if (options.focus_reset.has_value()) {
97
        // 1.  If this's focus reset behavior is not null, and it is not equal to options["focusReset"],
98
        //     then the user agent may report a warning to the console indicating that the focusReset option
99
        //     for a previous call to intercept() was overridden by this new value, and the previous value
100
        //     will be ignored.
101
0
        if (m_focus_reset_behavior.has_value() && *m_focus_reset_behavior != *options.focus_reset) {
102
0
            auto& console = realm.intrinsics().console_object()->console();
103
0
            console.output_debug_message(JS::Console::LogLevel::Warn,
104
0
                TRY_OR_THROW_OOM(vm, String::formatted("focusReset behavior on NavigationEvent overriden (was: {}, now: {})", *m_focus_reset_behavior, *options.focus_reset)));
105
0
        }
106
107
        // 2. Set this's focus reset behavior to options["focusReset"].
108
0
        m_focus_reset_behavior = options.focus_reset;
109
0
    }
110
111
    // 8. If options["scroll"] exists, then:
112
0
    if (options.scroll.has_value()) {
113
        // 1. If this's scroll behavior is not null, and it is not equal to options["scroll"], then the user
114
        //    agent may report a warning to the console indicating that the scroll option for a previous call
115
        //    to intercept() was overridden by this new value, and the previous value will be ignored.
116
0
        if (m_scroll_behavior.has_value() && *m_scroll_behavior != *options.scroll) {
117
0
            auto& console = realm.intrinsics().console_object()->console();
118
0
            console.output_debug_message(JS::Console::LogLevel::Warn,
119
0
                TRY_OR_THROW_OOM(vm, String::formatted("scroll option on NavigationEvent overriden (was: {}, now: {})", *m_scroll_behavior, *options.scroll)));
120
0
        }
121
122
        // 2. Set this's scroll behavior to options["scroll"].
123
0
        m_scroll_behavior = options.scroll;
124
0
    }
125
0
    return {};
126
0
}
127
128
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigateevent-scroll
129
WebIDL::ExceptionOr<void> NavigateEvent::scroll()
130
0
{
131
    // The scroll() method steps are:
132
    // 1. Perform shared checks given this.
133
0
    TRY(perform_shared_checks());
134
135
    // 2. If this's interception state is not "committed", then throw an "InvalidStateError" DOMException.
136
0
    if (m_interception_state != InterceptionState::Committed)
137
0
        return WebIDL::InvalidStateError::create(realm(), "Cannot scroll NavigationEvent that is not committed"_string);
138
139
    // 3. Process scroll behavior given this.
140
0
    process_scroll_behavior();
141
142
0
    return {};
143
0
}
144
145
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-perform-shared-checks
146
WebIDL::ExceptionOr<void> NavigateEvent::perform_shared_checks()
147
0
{
148
    // To perform shared checks for a NavigateEvent event:
149
150
    // 1. If event's relevant global object's associated Document is not fully active,
151
    //    then throw an "InvalidStateError" DOMException.
152
0
    auto& associated_document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document();
153
0
    if (!associated_document.is_fully_active())
154
0
        return WebIDL::InvalidStateError::create(realm(), "Document is not fully active"_string);
155
156
    // 2. If event's isTrusted attribute was initialized to false, then throw a "SecurityError" DOMException.
157
0
    if (!this->is_trusted())
158
0
        return WebIDL::SecurityError::create(realm(), "NavigateEvent is not trusted"_string);
159
160
    // 3. If event's canceled flag is set, then throw an "InvalidStateError" DOMException.
161
0
    if (this->cancelled())
162
0
        return WebIDL::InvalidStateError::create(realm(), "NavigateEvent already cancelled"_string);
163
164
0
    return {};
165
0
}
166
167
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#process-scroll-behavior
168
void NavigateEvent::process_scroll_behavior()
169
0
{
170
    // To process scroll behavior given a NavigateEvent event:
171
172
    // 1. Assert: event's interception state is "committed".
173
0
    VERIFY(m_interception_state == InterceptionState::Committed);
174
175
    // 2. Set event's interception state to "scrolled".
176
0
    m_interception_state = InterceptionState::Scrolled;
177
178
    // FIXME: 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data
179
    //           given event's relevant global object's navigable's active session history entry.
180
0
    if (m_navigation_type == Bindings::NavigationType::Traverse || m_navigation_type == Bindings::NavigationType::Reload) {
181
0
        dbgln("FIXME: restore scroll position data after traversal or reload navigation");
182
0
    }
183
184
    // 4. Otherwise:
185
0
    else {
186
        // 1. Let document be event's relevant global object's associated Document.
187
0
        auto& document = verify_cast<HTML::Window>(relevant_global_object(*this)).associated_document();
188
189
        // 2. If document's indicated part is null, then scroll to the beginning of the document given document. [CSSOMVIEW]
190
0
        auto indicated_part = document.determine_the_indicated_part();
191
0
        if (indicated_part.has<DOM::Element*>() && indicated_part.get<DOM::Element*>() == nullptr) {
192
0
            document.scroll_to_the_beginning_of_the_document();
193
0
        }
194
195
        // 3. Otherwise, scroll to the fragment given document.
196
0
        else {
197
            // FIXME: This will re-determine the indicated part. Can we avoid this extra work?
198
0
            document.scroll_to_the_fragment();
199
0
        }
200
0
    }
201
0
}
202
203
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-process-scroll-behavior
204
void NavigateEvent::potentially_process_scroll_behavior()
205
0
{
206
    // 1. Assert: event's interception state is "committed" or "scrolled".
207
0
    VERIFY(m_interception_state != InterceptionState::Committed && m_interception_state != InterceptionState::Scrolled);
208
209
    // 2. If event's interception state is "scrolled", then return.
210
0
    if (m_interception_state == InterceptionState::Scrolled)
211
0
        return;
212
213
    // 3. If event's scroll behavior is "manual", then return.
214
    // NOTE: If it was left as null, then we treat that as "after-transition", and continue onward.
215
0
    if (m_scroll_behavior == Bindings::NavigationScrollBehavior::Manual)
216
0
        return;
217
218
    // 4. Process scroll behavior given event.
219
0
    process_scroll_behavior();
220
0
}
221
222
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#potentially-reset-the-focus
223
void NavigateEvent::potentially_reset_the_focus()
224
0
{
225
    // 1. Assert: event's interception state is "committed" or "scrolled".
226
0
    VERIFY(m_interception_state == InterceptionState::Committed || m_interception_state == InterceptionState::Scrolled);
227
228
    // 2. Let navigation be event's relevant global object's navigation API.
229
0
    auto& relevant_global_object = verify_cast<Window>(HTML::relevant_global_object(*this));
230
0
    auto navigation = relevant_global_object.navigation();
231
232
    // 3. Let focusChanged be navigation's focus changed during ongoing navigation.
233
0
    auto focus_changed = navigation->focus_changed_during_ongoing_navigation();
234
235
    // 4. Set navigation's focus changed during ongoing navigation to false.
236
0
    navigation->set_focus_changed_during_ongoing_navigation(false);
237
238
    // 5. If focusChanged is true, then return.
239
0
    if (focus_changed)
240
0
        return;
241
242
    // 6. If event's focus reset behavior is "manual", then return.
243
    // NOTE: If it was left as null, then we treat that as "after-transition", and continue onward.
244
0
    if (m_focus_reset_behavior == Bindings::NavigationFocusReset::Manual)
245
0
        return;
246
247
    // 7. Let document be event's relevant global object's associated Document.
248
0
    auto& document = relevant_global_object.associated_document();
249
250
    // 8. FIXME: Let focusTarget be the autofocus delegate for document.
251
0
    JS::GCPtr<DOM::Node> focus_target = nullptr;
252
253
    // 9. If focusTarget is null, then set focusTarget to document's body element.
254
0
    if (focus_target == nullptr)
255
0
        focus_target = document.body();
256
257
    // 10. If focusTarget is null, then set focusTarget to document's document element.
258
0
    if (focus_target == nullptr)
259
0
        focus_target = document.document_element();
260
261
    // FIXME: 11. Run the focusing steps for focusTarget, with document's viewport as the fallback target.
262
0
    run_focusing_steps(focus_target, nullptr);
263
264
    // FIXME: 12. Move the sequential focus navigation starting point to focusTarget.
265
0
}
266
267
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigateevent-finish
268
void NavigateEvent::finish(bool did_fulfill)
269
0
{
270
    // 1. Assert: event's interception state is not "intercepted" or "finished".
271
0
    VERIFY(m_interception_state != InterceptionState::Intercepted && m_interception_state != InterceptionState::Finished);
272
273
    // 2. If event's interception state is "none", then return.
274
0
    if (m_interception_state == InterceptionState::None)
275
0
        return;
276
277
    // 3. Potentially reset the focus given event.
278
0
    potentially_reset_the_focus();
279
280
    // 4. If didFulfill is true, then potentially process scroll behavior given event.
281
0
    if (did_fulfill)
282
0
        potentially_process_scroll_behavior();
283
284
    // 5. Set event's interception state to "finished".
285
0
    m_interception_state = InterceptionState::Finished;
286
0
}
287
288
}