Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions selftests/test_job_script_escaping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Guard against a raw newline reaching the console as a premature Enter keypress.

``_run_console_command`` types R commands via Playwright's ``type()``, which
aliases embedded "\n"/"\r" characters to the Enter key — a raw newline in the
command submits it early, splitting it into two invalid R statements (see the
regression where ``write_test_script``'s ``writeLines()`` call never created
the test job script).
"""
Comment on lines +1 to +8

from __future__ import annotations

import pytest

from vip_tests.workbench.test_jobs import _JOB_SCRIPT_CONTENT, _escape_for_r_string_literal


def test_job_script_content_has_no_raw_newline_after_escaping():
escaped = _escape_for_r_string_literal(_JOB_SCRIPT_CONTENT)
assert "\n" not in escaped
assert "\r" not in escaped


@pytest.mark.parametrize(
"content",
["line one\nline two", 'has a "quote"\nand a newline', "carriage\rreturn"],
)
def test_escape_removes_raw_newlines_and_carriage_returns(content):
escaped = _escape_for_r_string_literal(content)
assert "\n" not in escaped
assert "\r" not in escaped
24 changes: 23 additions & 1 deletion src/vip_tests/workbench/test_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,18 @@ def _run_console_command(page: Page, r_cmd: str) -> None:
focused hidden Ace textarea (matches test_packages.py). ControlOrMeta maps
select-all to Cmd+A on macOS, where Ctrl+A is "go to line start" and would
not clear the input.

Playwright's ``type()`` sends a real Enter keypress for any embedded
``\\n``/``\\r`` (they're aliased to the Enter key in its US keyboard
layout), which would submit *r_cmd* early and mid-statement. Assert there
isn't one rather than silently mistyping it, since a caller building a
multi-line R string must already escape it into the string literal (e.g.
``\\n``) before it reaches here.
"""
assert "\n" not in r_cmd and "\r" not in r_cmd, (
f"r_cmd contains a raw newline, which Playwright's type() would submit as "
f"a premature Enter keypress: {r_cmd!r}"
)
Comment on lines +218 to +221
console_input = page.locator(ConsolePaneSelectors.INPUT)
expect(console_input).to_be_visible(timeout=TIMEOUT_DIALOG)
console_input.click()
Expand All @@ -220,6 +231,17 @@ def _run_console_command(page: Page, r_cmd: str) -> None:
expect(console_input).to_be_visible(timeout=TIMEOUT_CODE_EXEC)


def _escape_for_r_string_literal(content: str) -> str:
"""Escape *content* to embed inside a double-quoted R string literal on one line.

Quotes and real newlines both need escaping: a raw newline typed into the
console would be sent as an Enter keypress (Playwright's type() aliases
embedded "\\n"/"\\r" to the Enter key), submitting the command early and
splitting it into two invalid R statements.
"""
return content.replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")


@when("the user writes a test R script file via the console")
def write_test_script(page: Page):
"""Write the test R script to a file using writeLines() and confirm it landed.
Expand All @@ -232,7 +254,7 @@ def write_test_script(page: Page):
error. Verify ``file.exists()`` right here so a dropped write fails loudly at
its source with the path, rather than masquerading as a chooser regression.
"""
escaped = _JOB_SCRIPT_CONTENT.replace('"', '\\"')
escaped = _escape_for_r_string_literal(_JOB_SCRIPT_CONTENT)
# writeLines tilde-expands the path, so the file lands in the session home
# directory — the same location the file chooser and cleanup step target.
_run_console_command(page, f'writeLines("{escaped}", "{_JOB_SCRIPT_PATH}")')
Expand Down
Loading