From b7c7d2a497c25c82a4c6fd6aa2d62df78cb879a3 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Tue, 18 Aug 2026 06:55:04 -0400 Subject: [PATCH] Add --init-script option for running scripts before page load Registers JavaScript as a Playwright init script so it runs on every navigation before the page's own scripts, useful for seeding Math.random, freezing Date or hiding navigator.webdriver. Available on shot, multi, javascript, accessibility, pdf, html and har. Refs #147 --- docs/accessibility.md | 2 ++ docs/har.md | 2 ++ docs/html.md | 3 ++ docs/javascript.md | 17 +++++++++ docs/multi.md | 3 ++ docs/pdf.md | 3 ++ docs/screenshots.md | 3 ++ shot_scraper/cli.py | 37 +++++++++++++++++++ tests/test_shot_scraper.py | 74 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 144 insertions(+) diff --git a/docs/accessibility.md b/docs/accessibility.md index 0432d5d..43d74dd 100644 --- a/docs/accessibility.md +++ b/docs/accessibility.md @@ -48,6 +48,8 @@ Options: --bypass-csp Bypass Content-Security-Policy --auth-password TEXT Password for HTTP Basic authentication --auth-username TEXT Username for HTTP Basic authentication + --init-script TEXT JavaScript to run as a page init script, before the + page's own scripts run. Can be passed more than once. --help Show this message and exit. ``` diff --git a/docs/har.md b/docs/har.md index 3c8693f..9e8b1d6 100644 --- a/docs/har.md +++ b/docs/har.md @@ -131,6 +131,8 @@ Options: --bypass-csp Bypass Content-Security-Policy --auth-password TEXT Password for HTTP Basic authentication --auth-username TEXT Username for HTTP Basic authentication + --init-script TEXT JavaScript to run as a page init script, before the + page's own scripts run. Can be passed more than once. --help Show this message and exit. ``` diff --git a/docs/html.md b/docs/html.md index b2676ee..df1428e 100644 --- a/docs/html.md +++ b/docs/html.md @@ -80,6 +80,9 @@ Options: --silent Do not output any messages --auth-password TEXT Password for HTTP Basic authentication --auth-username TEXT Username for HTTP Basic authentication + --init-script TEXT JavaScript to run as a page init script, + before the page's own scripts run. Can be + passed more than once. --help Show this message and exit. ``` diff --git a/docs/javascript.md b/docs/javascript.md index 519b477..d5a89d3 100644 --- a/docs/javascript.md +++ b/docs/javascript.md @@ -137,6 +137,20 @@ This example [uses GitHub Actions](https://docs.github.com/en/actions/quickstart }" ``` +## Running an init script + +The `-j/--javascript` option runs code *after* the page has loaded. Sometimes you need to run code *before* the page's own scripts execute instead - for example to seed `Math.random()`, freeze `Date`, or hide the `navigator.webdriver` flag that some sites use to detect headless browsers. + +The `--init-script` option registers JavaScript as a [Playwright init script](https://playwright.dev/python/docs/api/class-page#page-add-init-script), which runs on every navigation before any of the page's own scripts: + +```bash +shot-scraper javascript https://example.com/ \ + --init-script "delete Object.getPrototypeOf(navigator).webdriver" \ + "navigator.webdriver" +``` + +You can pass `--init-script` more than once to register several scripts, and it works with the `shot`, `multi`, `javascript`, `accessibility`, `pdf`, `html` and `har` commands. + ## Running JavaScript from a file You can also save JavaScript to a file and execute it like this: @@ -260,6 +274,9 @@ Options: --bypass-csp Bypass Content-Security-Policy --auth-password TEXT Password for HTTP Basic authentication --auth-username TEXT Username for HTTP Basic authentication + --init-script TEXT JavaScript to run as a page init script, + before the page's own scripts run. Can be + passed more than once. --help Show this message and exit. ``` diff --git a/docs/multi.md b/docs/multi.md index a0ef14d..25bc086 100644 --- a/docs/multi.md +++ b/docs/multi.md @@ -269,6 +269,9 @@ Options: --har Save all requests to trace.har file --har-zip Save all requests to trace.har.zip file --har-file FILE Path to HAR file to save all requests + --init-script TEXT JavaScript to run as a page init script, + before the page's own scripts run. Can be + passed more than once. --help Show this message and exit. ``` diff --git a/docs/pdf.md b/docs/pdf.md index c19b50d..f4a67bb 100644 --- a/docs/pdf.md +++ b/docs/pdf.md @@ -75,6 +75,9 @@ Options: --silent Do not output any messages --auth-password TEXT Password for HTTP Basic authentication --auth-username TEXT Username for HTTP Basic authentication + --init-script TEXT JavaScript to run as a page init script, + before the page's own scripts run. Can be + passed more than once. --help Show this message and exit. ``` diff --git a/docs/screenshots.md b/docs/screenshots.md index 3cdf3d6..9e7277c 100644 --- a/docs/screenshots.md +++ b/docs/screenshots.md @@ -387,6 +387,9 @@ Options: --silent Do not output any messages --auth-password TEXT Password for HTTP Basic authentication --auth-username TEXT Username for HTTP Basic authentication + --init-script TEXT JavaScript to run as a page init script, + before the page's own scripts run. Can be + passed more than once. --help Show this message and exit. ``` diff --git a/shot_scraper/cli.py b/shot_scraper/cli.py index 9015e24..6c233c9 100644 --- a/shot_scraper/cli.py +++ b/shot_scraper/cli.py @@ -171,6 +171,19 @@ def js_file_option(fn): return fn +def init_script_option(fn): + click.option( + "init_scripts", + "--init-script", + multiple=True, + help=( + "JavaScript to run as a page init script, before the page's own " + "scripts run. Can be passed more than once." + ), + )(fn) + return fn + + def _load_javascript_source(source): "Load JavaScript from a file path, '-' for stdin or gh:username/script" if source.startswith("gh:"): @@ -308,6 +321,7 @@ def cli(): @bypass_csp_option @silent_option @http_auth_options +@init_script_option def shot( url, auth, @@ -342,6 +356,7 @@ def shot( silent, auth_username, auth_password, + init_scripts, ): """ Take a single screenshot of a page or portion of a page. @@ -410,6 +425,7 @@ def shot( bypass_csp=bypass_csp, auth_username=auth_username, auth_password=auth_password, + init_scripts=init_scripts, ) if interactive or devtools: use_existing_page = True @@ -469,6 +485,7 @@ def _browser_context( record_video_dir=None, record_video_size=None, viewport=None, + init_scripts=None, ): # Playwright 1.58 removed the `devtools` launch option. Emulate the # previous behavior for Chromium by passing the corresponding flag. @@ -512,6 +529,8 @@ def _browser_context( if viewport: context_args["viewport"] = viewport context = browser_obj.new_context(**context_args) + for init_script in init_scripts or []: + context.add_init_script(script=init_script) if timeout: context.set_default_timeout(timeout) return context, browser_obj @@ -803,6 +822,7 @@ def _convert_video_to_mp4(output, silent=False): type=click.Path(file_okay=True, writable=True, dir_okay=False), help="Path to HAR file to save all requests", ) +@init_script_option def multi( config, auth, @@ -826,6 +846,7 @@ def multi( har, har_zip, har_file, + init_scripts, ): """ Take multiple screenshots, defined by a YAML file @@ -877,6 +898,7 @@ def multi( auth_username=auth_username, auth_password=auth_password, record_har_path=har_file or None, + init_scripts=init_scripts, ) try: for shot in shots: @@ -954,6 +976,7 @@ def multi( @skip_fail_options @bypass_csp_option @http_auth_options +@init_script_option def accessibility( url, auth, @@ -967,6 +990,7 @@ def accessibility( bypass_csp, auth_username, auth_password, + init_scripts, ): """ Dump the Chromium accessibility tree for the specifed page @@ -985,6 +1009,7 @@ def accessibility( bypass_csp=bypass_csp, auth_username=auth_username, auth_password=auth_password, + init_scripts=init_scripts, ) page = context.new_page() if log_console: @@ -1037,6 +1062,7 @@ def accessibility( @skip_fail_options @bypass_csp_option @http_auth_options +@init_script_option def har( url, zip_, @@ -1054,6 +1080,7 @@ def har( bypass_csp, auth_username, auth_password, + init_scripts, ): """ Record a HAR file for the specified page @@ -1094,6 +1121,7 @@ def har( auth_username=auth_username, auth_password=auth_password, record_har_path=str(output), + init_scripts=init_scripts, ) page = context.new_page() if log_console: @@ -1272,6 +1300,7 @@ def _extract_har_entry(entry, extract_dir, existing_files, file_exists_fn, zip_f @skip_fail_options @bypass_csp_option @http_auth_options +@init_script_option def javascript( url, javascript, @@ -1292,6 +1321,7 @@ def javascript( bypass_csp, auth_username, auth_password, + init_scripts, ): """ Execute JavaScript against the page and return the result as JSON @@ -1334,6 +1364,7 @@ def javascript( bypass_csp=bypass_csp, auth_username=auth_username, auth_password=auth_password, + init_scripts=init_scripts, ) page = context.new_page() if log_console: @@ -1417,6 +1448,7 @@ def javascript( @bypass_csp_option @silent_option @http_auth_options +@init_script_option def pdf( url, auth, @@ -1440,6 +1472,7 @@ def pdf( silent, auth_username, auth_password, + init_scripts, ): """ Create a PDF of the specified page @@ -1468,6 +1501,7 @@ def pdf( auth_username=auth_username, auth_password=auth_password, timeout=timeout, + init_scripts=init_scripts, ) page = context.new_page() if log_console: @@ -1542,6 +1576,7 @@ def pdf( @bypass_csp_option @silent_option @http_auth_options +@init_script_option def html( url, auth, @@ -1561,6 +1596,7 @@ def html( silent, auth_username, auth_password, + init_scripts, ): """ Output the final HTML of the specified page @@ -1588,6 +1624,7 @@ def html( bypass_csp=bypass_csp, auth_username=auth_username, auth_password=auth_password, + init_scripts=init_scripts, ) page = context.new_page() if log_console: diff --git a/tests/test_shot_scraper.py b/tests/test_shot_scraper.py index 7d80404..ead8113 100644 --- a/tests/test_shot_scraper.py +++ b/tests/test_shot_scraper.py @@ -386,6 +386,80 @@ def test_javascript(args, expected): assert result.output == expected +# HTML that records, at parse time, whether the init script already ran +INIT_SCRIPT_HTML = """ + + +