@@ -222,11 +222,42 @@ def _run_console_command(page: Page, r_cmd: str) -> None:
222222
223223@when ("the user writes a test R script file via the console" )
224224def write_test_script (page : Page ):
225- """Write the test R script to a file using writeLines() in the R console."""
225+ """Write the test R script to a file using writeLines() and confirm it landed.
226+
227+ The console is an Ace editor driven by real keystrokes, which can be dropped
228+ if the console has not fully settled after the IDE loads. A dropped
229+ ``writeLines()`` leaves no file, and the Workbench Job file chooser then
230+ silently rejects Open (the file must exist), so the readonly script field
231+ stays empty and the failure only surfaces later as an opaque empty-field
232+ error. Verify ``file.exists()`` right here so a dropped write fails loudly at
233+ its source with the path, rather than masquerading as a chooser regression.
234+ """
226235 escaped = _JOB_SCRIPT_CONTENT .replace ('"' , '\\ "' )
227236 # writeLines tilde-expands the path, so the file lands in the session home
228237 # directory — the same location the file chooser and cleanup step target.
229238 _run_console_command (page , f'writeLines("{ escaped } ", "{ _JOB_SCRIPT_PATH } ")' )
239+ _assert_script_file_exists (page )
240+
241+
242+ def _assert_script_file_exists (page : Page ) -> None :
243+ """Fail loudly if the test script is not on disk after the write step.
244+
245+ Emits a unique sentinel alongside ``file.exists()`` and reads it back from
246+ the console output, so a dropped ``writeLines()`` (or a write to an
247+ unexpected directory) is caught at the write step — before the file chooser
248+ turns it into an empty-script-field mystery.
249+ """
250+ marker = "VIP_JOB_SCRIPT_CHECK"
251+ _run_console_command (page , f'cat("{ marker } :", file.exists("{ _JOB_SCRIPT_PATH } "), "\\ n")' )
252+ output = page .locator (ConsolePaneSelectors .OUTPUT_ELEMENT )
253+ expect (
254+ output ,
255+ (
256+ f"Test R script { _JOB_SCRIPT_PATH !r} was not created by the console write step — "
257+ f"the writeLines() keystrokes may have been dropped before the console settled, or "
258+ f"the file landed outside the session home directory"
259+ ),
260+ ).to_contain_text (f"{ marker } : TRUE" , timeout = TIMEOUT_CODE_EXEC )
230261
231262
232263@when ("the user runs the script as a Background Job" )
@@ -289,7 +320,7 @@ def run_as_workbench_job(page: Page, job_context: dict):
289320 new_btn .wait_for (state = "visible" , timeout = TIMEOUT_DIALOG )
290321 except PlaywrightTimeoutError :
291322 pytest .skip ("Run Script as Workbench Job button not found" )
292- new_btn . click ( )
323+ _open_workbench_job_dialog ( page , new_btn )
293324
294325 # Select the script via the file chooser. Unlike the Background Job dialog,
295326 # the Workbench Job dialog's script field (#rstudio_tbb_text_pro_job_script)
@@ -306,6 +337,49 @@ def run_as_workbench_job(page: Page, job_context: dict):
306337 submit_btn .click ()
307338
308339
340+ def _open_workbench_job_dialog (page : Page , new_btn ) -> None :
341+ """Click the "Start Workbench Job" toolbar button until its dialog opens.
342+
343+ The button is a GWT toolbar widget whose first click does not always open
344+ the "Run Script as Workbench Job" dialog — the click can land before the
345+ handler is armed, leaving the dialog closed and the later Browse-button
346+ wait to skip as if the feature were absent. Re-click a few times, checking
347+ for the Browse button (the dialog's first interactive control) between
348+ attempts, so a dropped first click self-heals instead of masquerading as a
349+ capability gap. Verified live over CDP against Workbench 2026.07.0.
350+ """
351+ browse_btn = page .locator (RStudioSession .WORKBENCH_JOB_SCRIPT_BROWSE_BUTTON ).first
352+ for _ in range (3 ):
353+ new_btn .click ()
354+ try :
355+ browse_btn .wait_for (state = "visible" , timeout = TIMEOUT_DIALOG )
356+ return
357+ except PlaywrightTimeoutError :
358+ continue
359+ # Final attempt surfaces the real state to _select_workbench_job_script,
360+ # which skips with a precise message if the Browse button never appears.
361+ new_btn .click ()
362+
363+
364+ def _fill_until_stable (locator , value : str , attempts : int = 5 ) -> None :
365+ """Fill *locator* with *value*, re-filling until the value stays put.
366+
367+ The Workbench Job file chooser clears its name field ~1s after it first
368+ appears (GWT finishes initializing it), silently wiping an early fill(). Fill
369+ the field, wait past that reset window, and re-fill if it was cleared, so the
370+ value survives into the Open click regardless of when the reset lands.
371+ """
372+ for _ in range (attempts ):
373+ locator .fill (value )
374+ # Wait out the chooser's post-init reset, then check the value held.
375+ locator .page .wait_for_timeout (TIMEOUT_QUICK // 4 )
376+ if locator .input_value () == value :
377+ return
378+ # One last fill; the caller's downstream check reports an empty script field
379+ # with a precise message if the value still refuses to stick.
380+ locator .fill (value )
381+
382+
309383def _select_workbench_job_script (page : Page , script_filename : str ) -> None :
310384 """Choose *script_filename* in the Workbench Job dialog via its file chooser.
311385
@@ -330,7 +404,13 @@ def _select_workbench_job_script(page: Page, script_filename: str) -> None:
330404 name_input .wait_for (state = "visible" , timeout = TIMEOUT_DIALOG )
331405 except PlaywrightTimeoutError :
332406 pytest .skip ("Workbench Job file chooser did not open" )
333- name_input .fill (script_filename )
407+
408+ # The chooser clears the name field ~1s after it first appears, as GWT
409+ # finishes initializing it. A fill() that lands before that reset is wiped,
410+ # so Open then submits an empty name and the readonly script field stays
411+ # empty. Fill, then re-fill through the reset until the value sticks, before
412+ # clicking Open. Verified live over CDP against Workbench 2026.07.0.
413+ _fill_until_stable (name_input , script_filename )
334414
335415 open_btn = page .locator (RStudioSession .FILE_CHOOSER_OPEN_BUTTON ).first
336416 expect (open_btn ).to_be_visible (timeout = TIMEOUT_QUICK )
0 commit comments