srctree

Robin Linden parent 76aba39b 92db6dc1
engine: Replace callbacks w/ returning the page state

inlinesplit
browser/gui/BUILD added: 179, removed: 196, total 0
@@ -26,6 +26,7 @@ cc_binary(
"//type:sfml",
"//uri",
"//util:history",
"@expected",
"@fmt",
"@imgui",
"@imgui-sfml",
 
browser/gui/app.cpp added: 179, removed: 196, total 0
@@ -35,11 +35,11 @@
 
#include <algorithm>
#include <array>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <functional>
#include <memory>
#include <optional>
#include <span>
@@ -251,9 +251,6 @@ App::App(std::string browser_title, std::string start_page_hint, bool load_start
canvas_->set_viewport_size(window_.getSize().x, window_.getSize().y);
 
engine_.set_layout_width(window_.getSize().x / scale_);
engine_.set_on_navigation_failure(std::bind_front(&App::on_navigation_failure, this));
engine_.set_on_page_loaded(std::bind_front(&App::on_page_loaded, this));
engine_.set_on_layout_updated(std::bind_front(&App::on_layout_updated, this));
 
if (load_start_page) {
ensure_has_scheme(url_buf_);
@@ -297,6 +294,10 @@ void App::step() {
case sf::Event::Resized: {
canvas_->set_viewport_size(event.size.width, event.size.height);
engine_.set_layout_width(event.size.width / scale_);
if (maybe_page_) {
engine_.relayout(**maybe_page_, event.size.width / scale_);
on_layout_updated();
}
break;
}
case sf::Event::KeyPressed: {
@@ -364,7 +365,7 @@ void App::step() {
break;
}
case sf::Event::MouseMoved: {
if (!page_loaded_) {
if (!maybe_page_) {
break;
}
 
@@ -437,7 +438,7 @@ void App::step() {
run_layout_widget();
}
 
if (!page_loaded_ || engine_.layout() == nullptr) {
if (!maybe_page_ || (**maybe_page_).layout == std::nullopt) {
canvas_->clear(gfx::Color{255, 255, 255});
} else {
render_layout();
@@ -458,13 +459,25 @@ int App::run() {
void App::navigate() {
spdlog::info("Navigating to '{}'", url_buf_);
window_.setIcon(16, 16, kBrowserIcon.data());
page_loaded_ = false;
auto uri = uri::Uri::parse(url_buf_, engine_.uri());
auto uri = [this] {
if (maybe_page_) {
return uri::Uri::parse(url_buf_, (**maybe_page_).uri);
}
 
return uri::Uri::parse(url_buf_);
}();
 
browse_history_.push(uri);
engine_.navigate(std::move(uri));
maybe_page_ = engine_.navigate(std::move(uri));
 
// Make sure the displayed url is still correct if we followed any redirects.
url_buf_ = engine_.uri().uri;
if (maybe_page_) {
url_buf_ = (**maybe_page_).uri.uri;
on_page_loaded();
} else {
url_buf_ = maybe_page_.error().uri.uri;
on_navigation_failure(maybe_page_.error().response.err);
}
}
 
void App::navigate_back() {
@@ -490,7 +503,7 @@ void App::navigate_forward() {
 
void App::on_navigation_failure(protocol::Error err) {
update_status_line();
response_headers_str_ = engine_.response().headers.to_string();
response_headers_str_ = maybe_page_.error().response.headers.to_string();
dom_str_.clear();
stylesheet_str_.clear();
layout_str_.clear();
@@ -524,8 +537,7 @@ void App::on_navigation_failure(protocol::Error err) {
}
 
void App::on_page_loaded() {
page_loaded_ = true;
if (auto page_title = try_get_text_content(engine_.dom(), "/html/head/title"sv)) {
if (auto page_title = try_get_text_content(page().dom, "/html/head/title"sv)) {
window_.setTitle(fmt::format("{} - {}", *page_title, browser_title_));
} else {
window_.setTitle(browser_title_);
@@ -538,14 +550,14 @@ void App::on_page_loaded() {
return rel != end(v->attributes) && rel->second == "icon" && v->attributes.contains("href");
};
 
auto links = dom::nodes_by_xpath(engine_.dom().html(), "/html/head/link");
auto links = dom::nodes_by_xpath(page().dom.html(), "/html/head/link");
std::ranges::reverse(links);
for (auto const &link : links) {
if (!is_favicon_link(link)) {
continue;
}
 
auto uri = uri::Uri::parse(link->attributes.at("href"), engine_.uri());
auto uri = uri::Uri::parse(link->attributes.at("href"), page().uri);
auto icon = engine_.load(uri).response;
sf::Image favicon;
if (icon.err != protocol::Error::Ok || !favicon.loadFromMemory(icon.body.data(), icon.body.size())) {
@@ -558,26 +570,29 @@ void App::on_page_loaded() {
}
 
update_status_line();
response_headers_str_ = engine_.response().headers.to_string();
dom_str_ = dom::to_string(engine_.dom());
stylesheet_str_ = stylesheet_to_string(engine_.stylesheet());
response_headers_str_ = page().response.headers.to_string();
dom_str_ = dom::to_string(page().dom);
stylesheet_str_ = stylesheet_to_string(page().stylesheet);
on_layout_updated();
}
 
void App::on_layout_updated() {
reset_scroll();
nav_widget_extra_info_.clear();
auto const *layout = engine_.layout();
layout_str_ = layout != nullptr ? layout::to_string(*layout) : "";
if (!maybe_page_) {
return;
}
 
auto const &layout = page().layout;
layout_str_ = layout.has_value() ? layout::to_string(*layout) : "";
}
 
layout::LayoutBox const *App::get_hovered_node(geom::Position document_position) const {
auto const *layout = engine_.layout();
if (!page_loaded_ || layout == nullptr) {
if (!maybe_page_.has_value() || !page().layout.has_value()) {
return nullptr;
}
 
return layout::box_at_position(*layout, document_position);
return layout::box_at_position(*page().layout, document_position);
}
 
geom::Position App::to_document_position(geom::Position window_position) const {
@@ -591,13 +606,13 @@ void App::reset_scroll() {
}
 
void App::scroll(int pixels) {
auto const *layout = engine_.layout();
if (!page_loaded_ || layout == nullptr) {
if (!maybe_page_ || !page().layout.has_value()) {
return;
}
 
auto const &layout = *page().layout;
// Don't allow scrolling if the entire page fits on the screen.
if (static_cast<int>(window_.getSize().y) > layout->dimensions.margin_box().height) {
if (static_cast<int>(window_.getSize().y) > layout.dimensions.margin_box().height) {
return;
}
 
@@ -608,8 +623,8 @@ void App::scroll(int pixels) {
 
int current_bottom_visible_y = static_cast<int>(window_.getSize().y) - scroll_offset_y_;
int scrolled_bottom_visible_y = current_bottom_visible_y - pixels;
if (scrolled_bottom_visible_y > layout->dimensions.margin_box().height) {
pixels -= layout->dimensions.margin_box().height - scrolled_bottom_visible_y;
if (scrolled_bottom_visible_y > layout.dimensions.margin_box().height) {
pixels -= layout.dimensions.margin_box().height - scrolled_bottom_visible_y;
}
 
canvas_->add_translation(0, pixels);
@@ -617,7 +632,14 @@ void App::scroll(int pixels) {
}
 
void App::update_status_line() {
auto const &r = engine_.response();
auto const &r = [this] {
if (maybe_page_) {
return page().response;
}
 
return maybe_page_.error().response;
}();
 
status_line_str_ = fmt::format("{} {} {}", r.status_line.version, r.status_line.status_code, r.status_line.reason);
}
 
@@ -648,7 +670,7 @@ void App::run_http_response_widget() const {
ImGui::TextUnformatted(response_headers_str_.c_str());
}
if (ImGui::CollapsingHeader("Body")) {
ImGui::TextUnformatted(engine_.response().body.c_str());
ImGui::TextUnformatted(page().response.body.c_str());
}
});
}
@@ -674,16 +696,17 @@ void App::run_layout_widget() const {
}
 
void App::render_layout() {
auto const *layout = engine_.layout();
if (layout == nullptr) {
assert(maybe_page_);
 
if (page().layout == std::nullopt) {
return;
}
 
if (render_debug_) {
render::debug::render_layout_depth(*canvas_, *layout);
render::debug::render_layout_depth(*canvas_, *page().layout);
} else {
render::render_layout(*canvas_,
*layout,
*page().layout,
culling_enabled_ ? std::optional{geom::Rect{0,
-scroll_offset_y_,
static_cast<int>(window_.getSize().x),
 
browser/gui/app.h added: 179, removed: 196, total 0
@@ -16,6 +16,7 @@
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/System/Clock.hpp>
#include <SFML/Window/Cursor.hpp>
#include <tl/expected.hpp>
 
#include <memory>
#include <string>
@@ -33,7 +34,8 @@ public:
 
private:
engine::Engine engine_;
bool page_loaded_{};
tl::expected<std::unique_ptr<engine::PageState>, engine::NavigationError> maybe_page_{
tl::unexpected<engine::NavigationError>{{}}};
 
std::string browser_title_{};
sf::Cursor cursor_{};
@@ -70,6 +72,9 @@ private:
 
util::History<uri::Uri> browse_history_;
 
engine::PageState &page() { return *maybe_page_.value(); }
engine::PageState const &page() const { return *maybe_page_.value(); }
 
void on_navigation_failure(protocol::Error);
void on_page_loaded();
void on_layout_updated();
 
browser/tui/tui.cpp added: 179, removed: 196, total 0
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021-2023 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021-2024 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
@@ -48,20 +48,22 @@ int main(int argc, char **argv) {
// Latest Firefox ESR user agent (on Windows). This matches what the Tor browser does.
auto user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0"s;
engine::Engine engine{protocol::HandlerFactory::create(std::move(user_agent))};
if (auto err = engine.navigate(uri); err != protocol::Error::Ok) {
spdlog::error(R"(Error loading "{}": {})", uri.uri, to_string(err));
auto maybe_page = engine.navigate(uri);
if (!maybe_page) {
spdlog::error(R"(Error loading "{}": {})", uri.uri, to_string(maybe_page.error().response.err));
return 1;
}
 
std::cout << dom::to_string(engine.dom());
auto page = std::move(*maybe_page);
 
std::cout << dom::to_string(page->dom);
spdlog::info("Building TUI");
 
auto const *layout = engine.layout();
if (layout == nullptr) {
if (!page->layout.has_value()) {
spdlog::error("Unable to create a layout of {}", uri.uri);
return 1;
}
 
std::cout << tui::render(*layout) << '\n';
std::cout << tui::render(*page->layout) << '\n';
spdlog::info("Done");
}
 
engine/BUILD added: 179, removed: 196, total 0
@@ -18,6 +18,7 @@ cc_library(
"//type",
"//type:naive",
"//uri",
"@expected",
"@spdlog",
],
)
 
engine/engine.cpp added: 179, removed: 196, total 0
@@ -16,8 +16,10 @@
#include "uri/uri.h"
 
#include <spdlog/spdlog.h>
#include <tl/expected.hpp>
 
#include <future>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
@@ -27,49 +29,33 @@ using namespace std::literals;
 
namespace engine {
 
protocol::Error Engine::navigate(uri::Uri uri) {
tl::expected<std::unique_ptr<PageState>, NavigationError> Engine::navigate(uri::Uri uri) {
auto result = load(std::move(uri));
state_.response = std::move(result.response);
state_.uri = std::move(result.uri_after_redirects);
 
switch (state_.response.err) {
case protocol::Error::Ok:
on_navigation_success();
break;
default:
on_navigation_failure_(state_.response.err);
break;
if (result.response.err != protocol::Error::Ok) {
return tl::unexpected{NavigationError{
.uri = std::move(result.uri_after_redirects),
.response = std::move(result.response),
}};
}
 
return state_.response.err;
}
auto state = std::make_unique<PageState>();
state->uri = std::move(result.uri_after_redirects);
state->response = std::move(result.response);
state->dom = html::parse(state->response.body);
state->stylesheet = css::default_style();
 
void Engine::set_layout_width(int width) {
state_.layout_width = width;
if (!state_.styled) {
return;
}
 
state_.styled = style::style_tree(state_.dom.html_node, state_.stylesheet, {.window_width = state_.layout_width});
state_.layout = layout::create_layout(*state_.styled, state_.layout_width, *type_);
on_layout_update_();
}
 
void Engine::on_navigation_success() {
state_.dom = html::parse(state_.response.body);
state_.stylesheet = css::default_style();
 
for (auto const &style : dom::nodes_by_xpath(state_.dom.html(), "/html/head/style"sv)) {
for (auto const &style : dom::nodes_by_xpath(state->dom.html(), "/html/head/style"sv)) {
if (style->children.empty()) {
continue;
}
 
// Style can only contain text, and we enforce this in our HTML parser.
auto const &style_content = std::get<dom::Text>(style->children[0]);
state_.stylesheet.splice(css::parse(style_content.text));
state->stylesheet.splice(css::parse(style_content.text));
}
 
auto head_links = dom::nodes_by_xpath(state_.dom.html(), "/html/head/link");
auto head_links = dom::nodes_by_xpath(state->dom.html(), "/html/head/link");
std::erase_if(head_links, [](auto const *link) {
return !link->attributes.contains("rel")
|| (link->attributes.contains("rel") && link->attributes.at("rel") != "stylesheet")
@@ -81,9 +67,9 @@ void Engine::on_navigation_success() {
std::vector<std::future<css::StyleSheet>> future_new_rules;
future_new_rules.reserve(head_links.size());
for (auto const *link : head_links) {
future_new_rules.push_back(std::async(std::launch::async, [=, this]() -> css::StyleSheet {
future_new_rules.push_back(std::async(std::launch::async, [=, this, &state]() -> css::StyleSheet {
auto const &href = link->attributes.at("href");
auto stylesheet_url = uri::Uri::parse(href, state_.uri);
auto stylesheet_url = uri::Uri::parse(href, state->uri);
 
spdlog::info("Downloading stylesheet from {}", stylesheet_url.uri);
auto res = load(stylesheet_url);
@@ -131,13 +117,21 @@ void Engine::on_navigation_success() {
 
// In order, wait for the download to finish and merge with the big stylesheet.
for (auto &future_rules : future_new_rules) {
state_.stylesheet.splice(future_rules.get());
state->stylesheet.splice(future_rules.get());
}
 
spdlog::info("Styling dom w/ {} rules", state_.stylesheet.rules.size());
state_.styled = style::style_tree(state_.dom.html_node, state_.stylesheet, {.window_width = state_.layout_width});
state_.layout = layout::create_layout(*state_.styled, state_.layout_width, *type_);
on_page_loaded_();
spdlog::info("Styling dom w/ {} rules", state->stylesheet.rules.size());
state->layout_width = layout_width_;
state->styled = style::style_tree(state->dom.html_node, state->stylesheet, {.window_width = state->layout_width});
state->layout = layout::create_layout(*state->styled, state->layout_width, *type_);
 
return state;
}
 
void Engine::relayout(PageState &state, int width) {
state.layout_width = width;
state.styled = style::style_tree(state.dom.html_node, state.stylesheet, {.window_width = state.layout_width});
state.layout = layout::create_layout(*state.styled, state.layout_width, *type_);
}
 
Engine::LoadResult Engine::load(uri::Uri uri) {
 
engine/engine.h added: 179, removed: 196, total 0
@@ -16,7 +16,8 @@
#include "type/type.h"
#include "uri/uri.h"
 
#include <functional>
#include <tl/expected.hpp>
 
#include <memory>
#include <optional>
#include <utility>
@@ -33,25 +34,22 @@ struct PageState {
int layout_width{};
};
 
struct NavigationError {
uri::Uri uri{};
protocol::Response response{};
};
 
class Engine {
public:
explicit Engine(std::unique_ptr<protocol::IProtocolHandler> protocol_handler,
std::unique_ptr<type::IType> type = std::make_unique<type::NaiveType>())
: protocol_handler_{std::move(protocol_handler)}, type_{std::move(type)} {}
 
protocol::Error navigate(uri::Uri uri);
void set_layout_width(int layout_width) { layout_width_ = layout_width; }
 
void set_layout_width(int width);
[[nodiscard]] tl::expected<std::unique_ptr<PageState>, NavigationError> navigate(uri::Uri);
 
void set_on_navigation_failure(std::function<void(protocol::Error)> cb) { on_navigation_failure_ = std::move(cb); }
void set_on_page_loaded(std::function<void()> cb) { on_page_loaded_ = std::move(cb); }
void set_on_layout_updated(std::function<void()> cb) { on_layout_update_ = std::move(cb); }
 
uri::Uri const &uri() const { return state_.uri; }
protocol::Response const &response() const { return state_.response; }
dom::Document const &dom() const { return state_.dom; }
css::StyleSheet const &stylesheet() const { return state_.stylesheet; }
layout::LayoutBox const *layout() const { return state_.layout.has_value() ? &*state_.layout : nullptr; }
void relayout(PageState &, int layout_width);
 
struct [[nodiscard]] LoadResult {
protocol::Response response;
@@ -62,19 +60,9 @@ public:
type::IType &font_system() { return *type_; }
 
private:
std::function<void(protocol::Error)> on_navigation_failure_{[](protocol::Error) {
}};
std::function<void()> on_page_loaded_{[] {
}};
std::function<void()> on_layout_update_{[] {
}};
 
std::unique_ptr<protocol::IProtocolHandler> protocol_handler_{};
std::unique_ptr<type::IType> type_{};
 
PageState state_{};
 
void on_navigation_success();
int layout_width_{600}; // Default chosen by rolling 1d600.
};
 
} // namespace engine
 
engine/engine_test.cpp added: 179, removed: 196, total 0
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2021-2023 Robin Lindén <dev@robinlinden.eu>
// SPDX-FileCopyrightText: 2021-2024 Robin Lindén <dev@robinlinden.eu>
//
// SPDX-License-Identifier: BSD-2-Clause
 
@@ -49,19 +49,6 @@ bool contains(std::vector<css::Rule> const &stylesheet, css::Rule const &rule) {
} // namespace
 
int main() {
etest::test("no handlers set", [] {
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = Error::Unresolved}},
})};
e.navigate(uri::Uri::parse("hax://example.com"));
 
e = engine::Engine{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = Error::Ok}},
})};
e.navigate(uri::Uri::parse("hax://example.com"));
e.set_layout_width(10);
});
 
etest::test("css in <head><style>", [] {
std::map<std::string, Response> responses{{
"hax://example.com"s,
@@ -72,8 +59,8 @@ int main() {
},
}};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
expect_eq(e.stylesheet().rules.back(),
auto page = e.navigate(uri::Uri::parse("hax://example.com"));
expect_eq(page.value()->stylesheet.rules.back(),
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
@@ -81,54 +68,30 @@ int main() {
});
 
etest::test("navigation failure", [] {
bool success{false};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = Error::Unresolved}},
})};
e.set_on_navigation_failure([&](Error err) { success = err != Error::Ok; });
e.set_on_page_loaded([] { require(false); });
e.set_on_layout_updated([] { require(false); });
 
e.navigate(uri::Uri::parse("hax://example.com"));
expect(success);
auto page = e.navigate(uri::Uri::parse("hax://example.com"));
expect_eq(page.has_value(), false);
});
 
etest::test("page load", [] {
bool success{false};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = Error::Ok}},
})};
e.set_on_navigation_failure([&](Error) { require(false); });
e.set_on_page_loaded([&] { success = true; });
e.set_on_layout_updated([] { require(false); });
 
e.navigate(uri::Uri::parse("hax://example.com"));
expect(success);
auto page = e.navigate(uri::Uri::parse("hax://example.com"));
expect(page.has_value());
});
 
etest::test("layout update", [] {
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::map{
std::pair{"hax://example.com"s, Response{.err = Error::Ok}},
})};
e.set_on_navigation_failure([&](Error) { require(false); });
e.set_on_page_loaded([] { require(false); });
e.set_on_layout_updated([] { require(false); });
 
e.set_layout_width(10);
 
bool success{false};
e.set_on_page_loaded([&] { success = true; });
 
e.navigate(uri::Uri::parse("hax://example.com"));
 
expect(success);
 
e.set_on_page_loaded([&] { require(false); });
success = false;
e.set_on_layout_updated([&] { success = true; });
 
e.set_layout_width(100);
expect(success);
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
e.relayout(*page, 100);
});
 
etest::test("css in <head><style> takes priority over browser built-in css", [] {
@@ -141,10 +104,10 @@ int main() {
},
}};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
// Our default CSS gives <html> the property display: block.
require(e.layout() != nullptr);
expect_eq(e.layout()->get_property<css::PropertyId::Display>(), style::DisplayValue::Block);
require(page->layout.has_value());
expect_eq(page->layout->get_property<css::PropertyId::Display>(), style::DisplayValue::Block);
 
responses = std::map<std::string, Response>{{
"hax://example.com"s,
@@ -156,12 +119,12 @@ int main() {
}};
 
e = engine::Engine{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
page = e.navigate(uri::Uri::parse("hax://example.com")).value();
 
// The CSS declared in the page should have a higher priority and give
// <html> the property display: inline.
require(e.layout() != nullptr);
expect_eq(e.layout()->get_property<css::PropertyId::Display>(), style::DisplayValue::Inline);
require(page->layout.has_value());
expect_eq(page->layout->get_property<css::PropertyId::Display>(), style::DisplayValue::Inline);
});
 
etest::test("multiple inline <head><style> elements are allowed", [] {
@@ -180,11 +143,11 @@ int main() {
},
}};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
require(e.layout() != nullptr);
auto const *a = dom::nodes_by_xpath(*e.layout(), "//a"sv).at(0);
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
require(page->layout.has_value());
auto const *a = dom::nodes_by_xpath(*page->layout, "//a"sv).at(0);
expect_eq(a->get_property<css::PropertyId::Color>(), gfx::Color::from_css_name("red"));
auto const *p = dom::nodes_by_xpath(*e.layout(), "//p"sv).at(0);
auto const *p = dom::nodes_by_xpath(*page->layout, "//p"sv).at(0);
expect_eq(p->get_property<css::PropertyId::Color>(), gfx::Color::from_css_name("cyan"));
});
 
@@ -209,9 +172,10 @@ int main() {
.body{"p { color: green; }"},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(contains(e.stylesheet().rules, {.selectors{"p"}, .declarations{{css::PropertyId::FontSize, "123em"}}}));
expect(contains(e.stylesheet().rules, {.selectors{"p"}, .declarations{{css::PropertyId::Color, "green"}}}));
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(contains(
page->stylesheet.rules, {.selectors{"p"}, .declarations{{css::PropertyId::FontSize, "123em"}}}));
expect(contains(page->stylesheet.rules, {.selectors{"p"}, .declarations{{css::PropertyId::Color, "green"}}}));
});
 
etest::test("stylesheet link, unsupported Content-Encoding", [] {
@@ -228,8 +192,9 @@ int main() {
.body{"p { font-size: 123em; }"},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(!contains(e.stylesheet().rules, {.selectors{"p"}, .declarations{{css::PropertyId::FontSize, "123em"}}}));
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(!contains(
page->stylesheet.rules, {.selectors{"p"}, .declarations{{css::PropertyId::FontSize, "123em"}}}));
});
 
// p { font-size: 123em; }, gzipped.
@@ -253,13 +218,13 @@ int main() {
.body{gzipped_css},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(responses)};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(std::ranges::find(e.stylesheet().rules,
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(std::ranges::find(page->stylesheet.rules,
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
})
!= end(e.stylesheet().rules));
!= end(page->stylesheet.rules));
 
// And again, but with x-gzip instead.
responses["hax://example.com/lol.css"s] = Response{
@@ -269,13 +234,13 @@ int main() {
.body{std::move(gzipped_css)},
};
e = engine::Engine{std::make_unique<FakeProtocolHandler>(responses)};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(std::ranges::find(e.stylesheet().rules,
page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(std::ranges::find(page->stylesheet.rules,
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
})
!= end(e.stylesheet().rules));
!= end(page->stylesheet.rules));
});
 
etest::test("stylesheet link, gzip Content-Encoding, bad header", [gzipped_css]() mutable {
@@ -294,13 +259,13 @@ int main() {
.body{std::move(gzipped_css)},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(std::ranges::find(e.stylesheet().rules,
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(std::ranges::find(page->stylesheet.rules,
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
})
== end(e.stylesheet().rules));
== end(page->stylesheet.rules));
});
 
etest::test("stylesheet link, gzip Content-Encoding, crc32 mismatch", [gzipped_css]() mutable {
@@ -319,13 +284,13 @@ int main() {
.body{std::move(gzipped_css)},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(std::ranges::find(e.stylesheet().rules,
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(std::ranges::find(page->stylesheet.rules,
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
})
== end(e.stylesheet().rules));
== end(page->stylesheet.rules));
});
 
etest::test("stylesheet link, gzip Content-Encoding, served zlib", [zlibbed_css] {
@@ -342,13 +307,13 @@ int main() {
.body{zlibbed_css},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(responses)};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(std::ranges::find(e.stylesheet().rules,
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(std::ranges::find(page->stylesheet.rules,
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
})
== end(e.stylesheet().rules));
== end(page->stylesheet.rules));
});
 
etest::test("stylesheet link, deflate Content-Encoding", [zlibbed_css] {
@@ -365,13 +330,13 @@ int main() {
.body{zlibbed_css},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(responses)};
e.navigate(uri::Uri::parse("hax://example.com"));
expect(std::ranges::find(e.stylesheet().rules,
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect(std::ranges::find(page->stylesheet.rules,
css::Rule{
.selectors{"p"},
.declarations{{css::PropertyId::FontSize, "123em"}},
})
!= end(e.stylesheet().rules));
!= end(page->stylesheet.rules));
});
 
etest::test("redirect", [] {
@@ -387,10 +352,11 @@ int main() {
.body{"<html><body>hello!</body></html>"},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
expect_eq(e.navigate(uri::Uri::parse("hax://example.com")), protocol::Error::Ok);
expect_eq(e.uri().uri, "hax://example.com/redirected");
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect_eq(page->response.err, protocol::Error::Ok);
expect_eq(page->uri.uri, "hax://example.com/redirected");
 
auto const &body = std::get<dom::Element>(e.dom().html().children.at(1));
auto const &body = std::get<dom::Element>(page->dom.html().children.at(1));
expect_eq(std::get<dom::Text>(body.children.at(0)).text, "hello!"sv);
});
 
@@ -401,7 +367,8 @@ int main() {
.status_line = {.status_code = 301},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
expect_eq(e.navigate(uri::Uri::parse("hax://example.com")), protocol::Error::InvalidResponse);
expect_eq(e.navigate(uri::Uri::parse("hax://example.com")).error().response.err,
protocol::Error::InvalidResponse);
});
 
etest::test("redirect, style", [] {
@@ -424,8 +391,9 @@ int main() {
.body{"p { color: green; }"},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
expect_eq(e.navigate(uri::Uri::parse("hax://example.com")), protocol::Error::Ok);
expect(contains(e.stylesheet().rules, {.selectors{"p"}, .declarations{{css::PropertyId::Color, "green"}}}));
auto page = e.navigate(uri::Uri::parse("hax://example.com")).value();
expect_eq(page->response.err, protocol::Error::Ok);
expect(contains(page->stylesheet.rules, {.selectors{"p"}, .declarations{{css::PropertyId::Color, "green"}}}));
});
 
etest::test("redirect loop", [] {
@@ -436,7 +404,8 @@ int main() {
.headers = {{"Location", "hax://example.com"}},
};
engine::Engine e{std::make_unique<FakeProtocolHandler>(std::move(responses))};
expect_eq(e.navigate(uri::Uri::parse("hax://example.com")), protocol::Error::RedirectLimit);
expect_eq(e.navigate(uri::Uri::parse("hax://example.com")).error().response.err, //
protocol::Error::RedirectLimit);
});
 
etest::test("load", [] {