headless-browser.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. /*
  2. * Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
  3. * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
  4. * Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
  5. * Copyright (c) 2023-2024, Sam Atkins <sam@ladybird.org>
  6. *
  7. * SPDX-License-Identifier: BSD-2-Clause
  8. */
  9. #include <AK/Badge.h>
  10. #include <AK/ByteBuffer.h>
  11. #include <AK/ByteString.h>
  12. #include <AK/LexicalPath.h>
  13. #include <AK/NonnullOwnPtr.h>
  14. #include <AK/Platform.h>
  15. #include <AK/String.h>
  16. #include <AK/Vector.h>
  17. #include <Ladybird/HelperProcess.h>
  18. #include <Ladybird/Utilities.h>
  19. #include <LibCore/ArgsParser.h>
  20. #include <LibCore/ConfigFile.h>
  21. #include <LibCore/DirIterator.h>
  22. #include <LibCore/Directory.h>
  23. #include <LibCore/EventLoop.h>
  24. #include <LibCore/File.h>
  25. #include <LibCore/Promise.h>
  26. #include <LibCore/ResourceImplementationFile.h>
  27. #include <LibCore/Timer.h>
  28. #include <LibDiff/Format.h>
  29. #include <LibDiff/Generator.h>
  30. #include <LibFileSystem/FileSystem.h>
  31. #include <LibGfx/Bitmap.h>
  32. #include <LibGfx/Font/FontDatabase.h>
  33. #include <LibGfx/ImageFormats/PNGWriter.h>
  34. #include <LibGfx/Point.h>
  35. #include <LibGfx/ShareableBitmap.h>
  36. #include <LibGfx/Size.h>
  37. #include <LibGfx/SystemTheme.h>
  38. #include <LibIPC/File.h>
  39. #include <LibImageDecoderClient/Client.h>
  40. #include <LibRequests/RequestClient.h>
  41. #include <LibURL/URL.h>
  42. #include <LibWeb/HTML/SelectedFile.h>
  43. #include <LibWebView/Application.h>
  44. #include <LibWebView/ViewImplementation.h>
  45. #include <LibWebView/WebContentClient.h>
  46. constexpr int DEFAULT_TIMEOUT_MS = 30000; // 30sec
  47. static StringView s_current_test_path;
  48. class HeadlessWebContentView final : public WebView::ViewImplementation {
  49. public:
  50. static ErrorOr<NonnullOwnPtr<HeadlessWebContentView>> create(Core::AnonymousBuffer theme, Gfx::IntSize const& window_size, StringView resources_folder)
  51. {
  52. RefPtr<Requests::RequestClient> request_client;
  53. RefPtr<ImageDecoderClient::Client> image_decoder_client;
  54. auto request_server_paths = TRY(get_paths_for_helper_process("RequestServer"sv));
  55. request_client = TRY(launch_request_server_process(request_server_paths, resources_folder));
  56. auto image_decoder_paths = TRY(get_paths_for_helper_process("ImageDecoder"sv));
  57. image_decoder_client = TRY(launch_image_decoder_process(image_decoder_paths));
  58. auto view = TRY(adopt_nonnull_own_or_enomem(new (nothrow) HeadlessWebContentView(image_decoder_client, request_client)));
  59. auto request_server_socket = TRY(connect_new_request_server_client(*request_client));
  60. auto image_decoder_socket = TRY(connect_new_image_decoder_client(*image_decoder_client));
  61. auto candidate_web_content_paths = TRY(get_paths_for_helper_process("WebContent"sv));
  62. view->m_client_state.client = TRY(launch_web_content_process(*view, candidate_web_content_paths, move(image_decoder_socket), move(request_server_socket)));
  63. view->client().async_update_system_theme(0, move(theme));
  64. view->m_viewport_size = window_size;
  65. view->client().async_set_viewport_size(0, view->m_viewport_size.to_type<Web::DevicePixels>());
  66. view->client().async_set_window_size(0, window_size.to_type<Web::DevicePixels>());
  67. if (WebView::Application::chrome_options().allow_popups == WebView::AllowPopups::Yes)
  68. view->client().async_debug_request(0, "block-pop-ups"sv, "off"sv);
  69. if (auto web_driver_ipc_path = WebView::Application::chrome_options().webdriver_content_ipc_path; web_driver_ipc_path.has_value())
  70. view->client().async_connect_to_webdriver(0, *web_driver_ipc_path);
  71. view->m_client_state.client->on_web_content_process_crash = [] {
  72. warnln("\033[31;1mWebContent Crashed!!\033[0m");
  73. if (!s_current_test_path.is_empty()) {
  74. warnln(" Last started test: {}", s_current_test_path);
  75. }
  76. VERIFY_NOT_REACHED();
  77. };
  78. return view;
  79. }
  80. RefPtr<Gfx::Bitmap> take_screenshot()
  81. {
  82. VERIFY(!m_pending_screenshot);
  83. m_pending_screenshot = Core::Promise<RefPtr<Gfx::Bitmap>>::construct();
  84. client().async_take_document_screenshot(0);
  85. auto screenshot = MUST(m_pending_screenshot->await());
  86. m_pending_screenshot = nullptr;
  87. return screenshot;
  88. }
  89. virtual void did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) override
  90. {
  91. VERIFY(m_pending_screenshot);
  92. m_pending_screenshot->resolve(screenshot.bitmap());
  93. }
  94. void clear_content_filters()
  95. {
  96. client().async_set_content_filters(0, {});
  97. }
  98. private:
  99. HeadlessWebContentView(RefPtr<ImageDecoderClient::Client> image_decoder_client, RefPtr<Requests::RequestClient> request_client)
  100. : m_request_client(move(request_client))
  101. , m_image_decoder_client(move(image_decoder_client))
  102. {
  103. on_request_worker_agent = [this]() {
  104. auto worker_client = MUST(launch_web_worker_process(MUST(get_paths_for_helper_process("WebWorker"sv)), *m_request_client));
  105. return worker_client->dup_socket();
  106. };
  107. }
  108. void update_zoom() override { }
  109. void initialize_client(CreateNewClient) override { }
  110. virtual Web::DevicePixelSize viewport_size() const override { return m_viewport_size.to_type<Web::DevicePixels>(); }
  111. virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override { return widget_position; }
  112. virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override { return content_position; }
  113. Gfx::IntSize m_viewport_size;
  114. RefPtr<Core::Promise<RefPtr<Gfx::Bitmap>>> m_pending_screenshot;
  115. RefPtr<Requests::RequestClient> m_request_client;
  116. RefPtr<ImageDecoderClient::Client> m_image_decoder_client;
  117. };
  118. static ErrorOr<NonnullRefPtr<Core::Timer>> load_page_for_screenshot_and_exit(Core::EventLoop& event_loop, HeadlessWebContentView& view, URL::URL const& url, int screenshot_timeout)
  119. {
  120. // FIXME: Allow passing the output path as an argument.
  121. static constexpr auto output_file_path = "output.png"sv;
  122. if (FileSystem::exists(output_file_path))
  123. TRY(FileSystem::remove(output_file_path, FileSystem::RecursionMode::Disallowed));
  124. outln("Taking screenshot after {} seconds", screenshot_timeout);
  125. auto timer = Core::Timer::create_single_shot(
  126. screenshot_timeout * 1000,
  127. [&]() {
  128. if (auto screenshot = view.take_screenshot()) {
  129. outln("Saving screenshot to {}", output_file_path);
  130. auto output_file = MUST(Core::File::open(output_file_path, Core::File::OpenMode::Write));
  131. auto image_buffer = MUST(Gfx::PNGWriter::encode(*screenshot));
  132. MUST(output_file->write_until_depleted(image_buffer.bytes()));
  133. } else {
  134. warnln("No screenshot available");
  135. }
  136. event_loop.quit(0);
  137. });
  138. view.load(url);
  139. timer->start();
  140. return timer;
  141. }
  142. enum class TestMode {
  143. Layout,
  144. Text,
  145. Ref,
  146. };
  147. enum class TestResult {
  148. Pass,
  149. Fail,
  150. Skipped,
  151. Timeout,
  152. };
  153. static StringView test_result_to_string(TestResult result)
  154. {
  155. switch (result) {
  156. case TestResult::Pass:
  157. return "Pass"sv;
  158. case TestResult::Fail:
  159. return "Fail"sv;
  160. case TestResult::Skipped:
  161. return "Skipped"sv;
  162. case TestResult::Timeout:
  163. return "Timeout"sv;
  164. }
  165. VERIFY_NOT_REACHED();
  166. }
  167. static ErrorOr<TestResult> run_dump_test(HeadlessWebContentView& view, URL::URL const& url, StringView expectation_path, TestMode mode, bool rebaseline = false, int timeout_in_milliseconds = DEFAULT_TIMEOUT_MS)
  168. {
  169. Core::EventLoop loop;
  170. bool did_timeout = false;
  171. auto timeout_timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&] {
  172. did_timeout = true;
  173. loop.quit(0);
  174. });
  175. String result;
  176. auto did_finish_test = false;
  177. auto did_finish_loading = false;
  178. if (mode == TestMode::Layout) {
  179. view.on_load_finish = [&](auto const& loaded_url) {
  180. // This callback will be called for 'about:blank' first, then for the URL we actually want to dump
  181. VERIFY(url.equals(loaded_url, URL::ExcludeFragment::Yes) || loaded_url.equals(URL::URL("about:blank")));
  182. if (url.equals(loaded_url, URL::ExcludeFragment::Yes)) {
  183. // NOTE: We take a screenshot here to force the lazy layout of SVG-as-image documents to happen.
  184. // It also causes a lot more code to run, which is good for finding bugs. :^)
  185. (void)view.take_screenshot();
  186. auto promise = view.request_internal_page_info(WebView::PageInfoType::LayoutTree | WebView::PageInfoType::PaintTree);
  187. result = MUST(promise->await());
  188. loop.quit(0);
  189. }
  190. };
  191. view.on_text_test_finish = {};
  192. } else if (mode == TestMode::Text) {
  193. view.on_load_finish = [&](auto const& loaded_url) {
  194. // NOTE: We don't want subframe loads to trigger the test finish.
  195. if (!url.equals(loaded_url, URL::ExcludeFragment::Yes))
  196. return;
  197. did_finish_loading = true;
  198. if (did_finish_test)
  199. loop.quit(0);
  200. };
  201. view.on_text_test_finish = [&](auto const& text) {
  202. result = text;
  203. did_finish_test = true;
  204. if (did_finish_loading)
  205. loop.quit(0);
  206. };
  207. }
  208. view.load(url);
  209. timeout_timer->start();
  210. loop.exec();
  211. if (did_timeout)
  212. return TestResult::Timeout;
  213. if (expectation_path.is_empty()) {
  214. out("{}", result);
  215. return TestResult::Skipped;
  216. }
  217. auto expectation_file_or_error = Core::File::open(expectation_path, rebaseline ? Core::File::OpenMode::Write : Core::File::OpenMode::Read);
  218. if (expectation_file_or_error.is_error()) {
  219. warnln("Failed opening '{}': {}", expectation_path, expectation_file_or_error.error());
  220. return expectation_file_or_error.release_error();
  221. }
  222. auto expectation_file = expectation_file_or_error.release_value();
  223. if (rebaseline) {
  224. TRY(expectation_file->write_until_depleted(result));
  225. return TestResult::Pass;
  226. }
  227. auto expectation = TRY(String::from_utf8(StringView(TRY(expectation_file->read_until_eof()).bytes())));
  228. auto actual = result;
  229. auto actual_trimmed = TRY(actual.trim("\n"sv, TrimMode::Right));
  230. auto expectation_trimmed = TRY(expectation.trim("\n"sv, TrimMode::Right));
  231. if (actual_trimmed == expectation_trimmed)
  232. return TestResult::Pass;
  233. auto const color_output = isatty(STDOUT_FILENO) ? Diff::ColorOutput::Yes : Diff::ColorOutput::No;
  234. if (color_output == Diff::ColorOutput::Yes)
  235. outln("\n\033[33;1mTest failed\033[0m: {}", url);
  236. else
  237. outln("\nTest failed: {}", url);
  238. auto hunks = TRY(Diff::from_text(expectation, actual, 3));
  239. auto out = TRY(Core::File::standard_output());
  240. TRY(Diff::write_unified_header(expectation_path, expectation_path, *out));
  241. for (auto const& hunk : hunks)
  242. TRY(Diff::write_unified(hunk, *out, color_output));
  243. return TestResult::Fail;
  244. }
  245. static ErrorOr<TestResult> run_ref_test(HeadlessWebContentView& view, URL::URL const& url, bool dump_failed_ref_tests, int timeout_in_milliseconds = DEFAULT_TIMEOUT_MS)
  246. {
  247. Core::EventLoop loop;
  248. bool did_timeout = false;
  249. auto timeout_timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&] {
  250. did_timeout = true;
  251. loop.quit(0);
  252. });
  253. RefPtr<Gfx::Bitmap> actual_screenshot, expectation_screenshot;
  254. view.on_load_finish = [&](auto const&) {
  255. if (actual_screenshot) {
  256. expectation_screenshot = view.take_screenshot();
  257. loop.quit(0);
  258. } else {
  259. actual_screenshot = view.take_screenshot();
  260. view.debug_request("load-reference-page");
  261. }
  262. };
  263. view.on_text_test_finish = [&](auto const&) {
  264. dbgln("Unexpected text test finished during ref test for {}", url);
  265. };
  266. view.load(url);
  267. timeout_timer->start();
  268. loop.exec();
  269. if (did_timeout)
  270. return TestResult::Timeout;
  271. VERIFY(actual_screenshot);
  272. VERIFY(expectation_screenshot);
  273. if (actual_screenshot->visually_equals(*expectation_screenshot))
  274. return TestResult::Pass;
  275. if (dump_failed_ref_tests) {
  276. warnln("\033[33;1mRef test {} failed; dumping screenshots\033[0m", url);
  277. auto title = LexicalPath::title(URL::percent_decode(url.serialize_path()));
  278. auto dump_screenshot = [&](Gfx::Bitmap& bitmap, StringView path) -> ErrorOr<void> {
  279. auto screenshot_file = TRY(Core::File::open(path, Core::File::OpenMode::Write));
  280. auto encoded_data = TRY(Gfx::PNGWriter::encode(bitmap));
  281. TRY(screenshot_file->write_until_depleted(encoded_data));
  282. warnln("\033[33;1mDumped {}\033[0m", TRY(FileSystem::real_path(path)));
  283. return {};
  284. };
  285. auto mkdir_result = Core::System::mkdir("test-dumps"sv, 0755);
  286. if (mkdir_result.is_error() && mkdir_result.error().code() != EEXIST)
  287. return mkdir_result.release_error();
  288. TRY(dump_screenshot(*actual_screenshot, ByteString::formatted("test-dumps/{}.png", title)));
  289. TRY(dump_screenshot(*expectation_screenshot, ByteString::formatted("test-dumps/{}-ref.png", title)));
  290. }
  291. return TestResult::Fail;
  292. }
  293. static ErrorOr<TestResult> run_test(HeadlessWebContentView& view, StringView input_path, StringView expectation_path, TestMode mode, bool dump_failed_ref_tests, bool rebaseline)
  294. {
  295. // Clear the current document.
  296. // FIXME: Implement a debug-request to do this more thoroughly.
  297. auto promise = Core::Promise<Empty>::construct();
  298. view.on_load_finish = [&](auto) {
  299. promise->resolve({});
  300. };
  301. view.on_text_test_finish = {};
  302. view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) {
  303. // Create some dummy files for tests.
  304. Vector<Web::HTML::SelectedFile> selected_files;
  305. bool add_txt_files = accepted_file_types.filters.is_empty();
  306. bool add_cpp_files = false;
  307. for (auto const& filter : accepted_file_types.filters) {
  308. filter.visit(
  309. [](Web::HTML::FileFilter::FileType) {},
  310. [&](Web::HTML::FileFilter::MimeType const& mime_type) {
  311. if (mime_type.value == "text/plain"sv)
  312. add_txt_files = true;
  313. },
  314. [&](Web::HTML::FileFilter::Extension const& extension) {
  315. if (extension.value == "cpp"sv)
  316. add_cpp_files = true;
  317. });
  318. }
  319. if (add_txt_files) {
  320. selected_files.empend("file1"sv, MUST(ByteBuffer::copy("Contents for file1"sv.bytes())));
  321. if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
  322. selected_files.empend("file2"sv, MUST(ByteBuffer::copy("Contents for file2"sv.bytes())));
  323. selected_files.empend("file3"sv, MUST(ByteBuffer::copy("Contents for file3"sv.bytes())));
  324. selected_files.empend("file4"sv, MUST(ByteBuffer::copy("Contents for file4"sv.bytes())));
  325. }
  326. }
  327. if (add_cpp_files) {
  328. selected_files.empend("file1.cpp"sv, MUST(ByteBuffer::copy("int main() {{ return 1; }}"sv.bytes())));
  329. if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
  330. selected_files.empend("file2.cpp"sv, MUST(ByteBuffer::copy("int main() {{ return 2; }}"sv.bytes())));
  331. }
  332. }
  333. view.file_picker_closed(move(selected_files));
  334. };
  335. view.load(URL::URL("about:blank"sv));
  336. MUST(promise->await());
  337. auto url = URL::create_with_file_scheme(TRY(FileSystem::real_path(input_path)));
  338. s_current_test_path = input_path;
  339. switch (mode) {
  340. case TestMode::Text:
  341. case TestMode::Layout:
  342. return run_dump_test(view, url, expectation_path, mode, rebaseline);
  343. case TestMode::Ref:
  344. return run_ref_test(view, url, dump_failed_ref_tests);
  345. default:
  346. VERIFY_NOT_REACHED();
  347. }
  348. }
  349. struct Test {
  350. ByteString input_path;
  351. ByteString expectation_path;
  352. TestMode mode;
  353. Optional<TestResult> result;
  354. };
  355. static Vector<ByteString> s_skipped_tests;
  356. static ErrorOr<void> load_test_config(StringView test_root_path)
  357. {
  358. auto config_path = LexicalPath::join(test_root_path, "TestConfig.ini"sv);
  359. auto config_or_error = Core::ConfigFile::open(config_path.string());
  360. if (config_or_error.is_error()) {
  361. if (config_or_error.error().code() == ENOENT)
  362. return {};
  363. dbgln("Unable to open test config {}", config_path);
  364. return config_or_error.release_error();
  365. }
  366. auto config = config_or_error.release_value();
  367. for (auto const& group : config->groups()) {
  368. if (group == "Skipped"sv) {
  369. for (auto& key : config->keys(group))
  370. s_skipped_tests.append(LexicalPath::join(test_root_path, key).string());
  371. } else {
  372. warnln("Unknown group '{}' in config {}", group, config_path);
  373. }
  374. }
  375. return {};
  376. }
  377. static ErrorOr<void> collect_dump_tests(Vector<Test>& tests, StringView path, StringView trail, TestMode mode)
  378. {
  379. Core::DirIterator it(ByteString::formatted("{}/input/{}", path, trail), Core::DirIterator::Flags::SkipDots);
  380. while (it.has_next()) {
  381. auto name = it.next_path();
  382. auto input_path = TRY(FileSystem::real_path(ByteString::formatted("{}/input/{}/{}", path, trail, name)));
  383. if (FileSystem::is_directory(input_path)) {
  384. TRY(collect_dump_tests(tests, path, ByteString::formatted("{}/{}", trail, name), mode));
  385. continue;
  386. }
  387. if (!name.ends_with(".html"sv) && !name.ends_with(".svg"sv))
  388. continue;
  389. auto basename = LexicalPath::title(name);
  390. auto expectation_path = ByteString::formatted("{}/expected/{}/{}.txt", path, trail, basename);
  391. tests.append({ input_path, move(expectation_path), mode, {} });
  392. }
  393. return {};
  394. }
  395. static ErrorOr<void> collect_ref_tests(Vector<Test>& tests, StringView path)
  396. {
  397. TRY(Core::Directory::for_each_entry(path, Core::DirIterator::SkipDots, [&](Core::DirectoryEntry const& entry, Core::Directory const&) -> ErrorOr<IterationDecision> {
  398. if (entry.type == Core::DirectoryEntry::Type::Directory)
  399. return IterationDecision::Continue;
  400. auto input_path = TRY(FileSystem::real_path(ByteString::formatted("{}/{}", path, entry.name)));
  401. tests.append({ input_path, {}, TestMode::Ref, {} });
  402. return IterationDecision::Continue;
  403. }));
  404. return {};
  405. }
  406. static ErrorOr<int> run_tests(HeadlessWebContentView* view, StringView test_root_path, StringView test_glob, bool dump_failed_ref_tests, bool dump_gc_graph, bool dry_run, bool rebaseline)
  407. {
  408. if (view)
  409. view->clear_content_filters();
  410. TRY(load_test_config(test_root_path));
  411. Vector<Test> tests;
  412. TRY(collect_dump_tests(tests, ByteString::formatted("{}/Layout", test_root_path), "."sv, TestMode::Layout));
  413. TRY(collect_dump_tests(tests, ByteString::formatted("{}/Text", test_root_path), "."sv, TestMode::Text));
  414. TRY(collect_ref_tests(tests, ByteString::formatted("{}/Ref", test_root_path)));
  415. #ifndef AK_OS_MACOS
  416. TRY(collect_ref_tests(tests, ByteString::formatted("{}/Screenshot", test_root_path)));
  417. #endif
  418. tests.remove_all_matching([&](auto const& test) {
  419. return !test.input_path.matches(test_glob, CaseSensitivity::CaseSensitive);
  420. });
  421. size_t pass_count = 0;
  422. size_t fail_count = 0;
  423. size_t timeout_count = 0;
  424. size_t skipped_count = 0;
  425. bool is_tty = isatty(STDOUT_FILENO);
  426. if (dry_run)
  427. outln("Found {} tests...", tests.size());
  428. else
  429. outln("Running {} tests...", tests.size());
  430. for (size_t i = 0; i < tests.size(); ++i) {
  431. auto& test = tests[i];
  432. if (is_tty && !dry_run) {
  433. // Keep clearing and reusing the same line if stdout is a TTY.
  434. out("\33[2K\r");
  435. }
  436. out("{}/{}: {}", i + 1, tests.size(), LexicalPath::relative_path(test.input_path, test_root_path));
  437. if (dry_run) {
  438. outln("");
  439. continue;
  440. }
  441. if (is_tty)
  442. fflush(stdout);
  443. else
  444. outln("");
  445. if (s_skipped_tests.contains_slow(test.input_path)) {
  446. test.result = TestResult::Skipped;
  447. ++skipped_count;
  448. continue;
  449. }
  450. test.result = TRY(run_test(*view, test.input_path, test.expectation_path, test.mode, dump_failed_ref_tests, rebaseline));
  451. switch (*test.result) {
  452. case TestResult::Pass:
  453. ++pass_count;
  454. break;
  455. case TestResult::Fail:
  456. ++fail_count;
  457. break;
  458. case TestResult::Timeout:
  459. ++timeout_count;
  460. break;
  461. case TestResult::Skipped:
  462. VERIFY_NOT_REACHED();
  463. break;
  464. }
  465. }
  466. if (dry_run)
  467. return 0;
  468. if (is_tty)
  469. outln("\33[2K\rDone!");
  470. outln("==================================================");
  471. outln("Pass: {}, Fail: {}, Skipped: {}, Timeout: {}", pass_count, fail_count, skipped_count, timeout_count);
  472. outln("==================================================");
  473. for (auto& test : tests) {
  474. if (*test.result == TestResult::Pass)
  475. continue;
  476. outln("{}: {}", test_result_to_string(*test.result), test.input_path);
  477. }
  478. if (dump_gc_graph) {
  479. auto path = view->dump_gc_graph();
  480. if (path.is_error()) {
  481. warnln("Failed to dump GC graph: {}", path.error());
  482. } else {
  483. outln("GC graph dumped to {}", path.value());
  484. }
  485. }
  486. if (timeout_count == 0 && fail_count == 0)
  487. return 0;
  488. return 1;
  489. }
  490. struct Application : public WebView::Application {
  491. WEB_VIEW_APPLICATION(Application)
  492. virtual void create_platform_arguments(Core::ArgsParser& args_parser) override
  493. {
  494. args_parser.add_option(screenshot_timeout, "Take a screenshot after [n] seconds (default: 1)", "screenshot", 's', "n");
  495. args_parser.add_option(dump_layout_tree, "Dump layout tree and exit", "dump-layout-tree", 'd');
  496. args_parser.add_option(dump_text, "Dump text and exit", "dump-text", 'T');
  497. args_parser.add_option(test_root_path, "Run tests in path", "run-tests", 'R', "test-root-path");
  498. args_parser.add_option(test_glob, "Only run tests matching the given glob", "filter", 'f', "glob");
  499. args_parser.add_option(test_dry_run, "List the tests that would be run, without running them", "dry-run");
  500. args_parser.add_option(dump_failed_ref_tests, "Dump screenshots of failing ref tests", "dump-failed-ref-tests", 'D');
  501. args_parser.add_option(dump_gc_graph, "Dump GC graph", "dump-gc-graph", 'G');
  502. args_parser.add_option(resources_folder, "Path of the base resources folder (defaults to /res)", "resources", 'r', "resources-root-path");
  503. args_parser.add_option(is_layout_test_mode, "Enable layout test mode", "layout-test-mode");
  504. args_parser.add_option(rebaseline, "Rebaseline any executed layout or text tests", "rebaseline");
  505. }
  506. virtual void create_platform_options(WebView::ChromeOptions& chrome_options, WebView::WebContentOptions& web_content_options) override
  507. {
  508. if (!test_root_path.is_empty()) {
  509. // --run-tests implies --layout-test-mode.
  510. is_layout_test_mode = true;
  511. }
  512. if (is_layout_test_mode) {
  513. // Allow window.open() to succeed for tests.
  514. chrome_options.allow_popups = WebView::AllowPopups::Yes;
  515. }
  516. web_content_options.is_layout_test_mode = is_layout_test_mode ? WebView::IsLayoutTestMode::Yes : WebView::IsLayoutTestMode::No;
  517. }
  518. int screenshot_timeout { 1 };
  519. ByteString resources_folder { s_ladybird_resource_root };
  520. bool dump_failed_ref_tests { false };
  521. bool dump_layout_tree { false };
  522. bool dump_text { false };
  523. bool dump_gc_graph { false };
  524. bool is_layout_test_mode { false };
  525. StringView test_root_path;
  526. ByteString test_glob;
  527. bool test_dry_run { false };
  528. bool rebaseline { false };
  529. };
  530. Application::Application(Badge<WebView::Application>, Main::Arguments&)
  531. {
  532. }
  533. ErrorOr<int> serenity_main(Main::Arguments arguments)
  534. {
  535. platform_init();
  536. auto app = Application::create(arguments, "about:newtab"sv);
  537. Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::from_byte_string(app->resources_folder))));
  538. auto theme_path = LexicalPath::join(app->resources_folder, "themes"sv, "Default.ini"sv);
  539. auto theme = TRY(Gfx::load_system_theme(theme_path.string()));
  540. // FIXME: Allow passing the window size as an argument.
  541. static constexpr Gfx::IntSize window_size { 800, 600 };
  542. if (!app->test_root_path.is_empty()) {
  543. OwnPtr<HeadlessWebContentView> view;
  544. if (!app->test_dry_run)
  545. view = TRY(HeadlessWebContentView::create(move(theme), window_size, app->resources_folder));
  546. auto absolute_test_root_path = LexicalPath::absolute_path(TRY(FileSystem::current_working_directory()), app->test_root_path);
  547. app->test_root_path = absolute_test_root_path;
  548. auto test_glob = ByteString::formatted("*{}*", app->test_glob);
  549. return run_tests(view, app->test_root_path, test_glob, app->dump_failed_ref_tests, app->dump_gc_graph, app->test_dry_run, app->rebaseline);
  550. }
  551. auto view = TRY(HeadlessWebContentView::create(move(theme), window_size, app->resources_folder));
  552. VERIFY(!WebView::Application::chrome_options().urls.is_empty());
  553. auto const& url = WebView::Application::chrome_options().urls.first();
  554. if (!url.is_valid()) {
  555. warnln("Invalid URL: \"{}\"", url);
  556. return Error::from_string_literal("Invalid URL");
  557. }
  558. if (app->dump_layout_tree) {
  559. TRY(run_dump_test(*view, url, ""sv, TestMode::Layout));
  560. return 0;
  561. }
  562. if (app->dump_text) {
  563. TRY(run_dump_test(*view, url, ""sv, TestMode::Text));
  564. return 0;
  565. }
  566. if (!WebView::Application::chrome_options().webdriver_content_ipc_path.has_value()) {
  567. auto timer = TRY(load_page_for_screenshot_and_exit(Core::EventLoop::current(), *view, url, app->screenshot_timeout));
  568. return app->execute();
  569. }
  570. return 0;
  571. }