Skip to content

Add DROP - #30

Merged
brynary merged 10 commits into
video-no-framesfrom
drop-file
Oct 5, 2026
Merged

brynary merged 10 commits into
video-no-framesfrom
drop-file

Conversation

@brynary

@brynary brynary commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Stacked on #29. Many upload widgets are drop zones with no <input type=file>, so UPLOAD cannot reach them. DROP locator file:path drops a file on an element, as a user drops one from the desktop, with Playwright 1.62's locator.drop.

Change

Part Change
Syntax DROP locator file:path, parsed and formatted exactly like UPLOAD; the two now share one parser helper. The path resolves relative to the .whirl file.
Default engine text:: a drop zone says what it takes, as in DROP "Drop files here" file:report.csv.
Behavior dragenter, dragover, and drop at the element's center, with the usual strictness, visibility, stability, and hit checks. The page sees the file's name and size, and a type from its extension (application/octet-stream when unknown).
Rejected drop A zone whose dragover does not call preventDefault() rejects the drop. The step fails at once: the drop target … did not accept the drop (its dragover did not call preventDefault).
Missing file Fails at once: the file /abs/path does not exist, instead of Node's raw ENOENT text.
ACT No method: ACT does not upload or drop files, and SPEC §7.4 says to write an UPLOAD or DROP line.

SPEC §7 also states two limits. A zone that appears only while a drag is over the page cannot be the target, because the element must be visible first. And the events are synthetic, so a page that checks isTrusted rejects them.

Tests

All browser tests run in Chromium, Firefox, and WebKit:

  • A dropped file shows its name, size, and type (text/csv, text/plain, and the application/octet-stream fallback), including drops into a same-origin and a cross-origin frame.
  • The rejecting zone fails at once with exit 1 and the message above.
  • A missing file fails with its resolved path.
  • An ambiguous zone fails with a strictness error that lists the candidates.
  • Unit: parsing (a quoted "file:..." is a value), the default engine, the fmt round trip, and the wire form of the drop command.
  • mise run check passes. SPEC §6.1, §7, §7.4, and §17, the shim protocol doc, and the CHANGELOG describe the change.

🤖 Generated with Claude Code

brynary and others added 7 commits September 27, 2026 13:24
With --video on Chromium, a short flow on a still page could fail as a
runtime error: "video recording failed: the page produced no frames".
On macOS CI this failed one run of
combined_report_keeps_embedded_media_and_missing_scenarios_offline, and
the rerun passed.

Chrome sends a screencast frame only when the page paints, and the frame
reaches the shim some time after the paint: 5 to 450 ms on a loaded
machine. The recorder starts the screencast on the blank page, and a flow
such as VISIT then SCREENSHOT can reach endFlow first. The recorder then
stopped the screencast, dropped the frame on its way, and failed the
file. With logging added, each such frame arrived 10 to 33 ms after the
stop.

Now, when no frame has arrived, the recorder captures the page once with
Page.captureScreenshot and holds that frame from the start of the
recording at the constant rate. When the capture fails or takes more
than 5 seconds, as for a crashed page, the shim skips the recording and
says why in the new endFlow field videoSkipped. Rust reports it as a
warning, and the file's result does not change, as for SCREENSHOT.

The recorder now takes a frame source, so its unit tests can use a page
that sends no frame.

Before the fix, the CI test failed 3 of 50 runs with 10 copies at once
on low-priority cores, and a new test of four short flows failed 20 of
20 runs. After the fix, they pass 100 of 100 and 20 of 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A skipped recording kept runtime.videoFps in the JSON report, but the
machine-output doc says the field is absent without a recording. Rust
now clears it when the shim skips the recording. The fake shim starts a
60 fps recording before it reports the skip, so the test sees the field
go away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Many upload widgets are drop zones with no file input, so UPLOAD, which
sets an input's files, cannot test them. DROP locator file:path drops
one file on the element. It parses like UPLOAD, and the path resolves
beside the .whirl file. An unprefixed locator uses the text default
engine, because a drop zone says what it takes, such as "Drop files
here".

The shim calls Playwright's locator.drop, which waits for the element as
other pointer actions do and fires dragenter, dragover, and drop with
the file. Playwright reads the file and gives it its name and a type
from its extension. When the zone's dragover does not call
preventDefault, the page rejects the drop, and the step fails at once
with an action error that names the target. A missing file fails before
the shim looks for the element, with the path that Whirl tried.

ACT has no method for DROP, as it has none for UPLOAD, because it
cannot choose a file. The SPEC now says so.

Tests drop files on a zone, into a same-origin frame, and into a
cross-origin frame, and cover a rejected drop, a missing file, and an
ambiguous zone, in all three engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drop zone that appears only while a drag is over the page cannot be
the target, because Playwright waits for the element to be visible
before it fires any drag event. The events are synthetic, so a page that
checks isTrusted rejects them. Authors hit both, so the SPEC says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --video on Chromium, an ffmpeg failure at endFlow failed endFlow, so
the file became a runtime error (exit 3). But a recording is evidence,
not a result (SPEC 14.1). Now the shim discards the recording and says
why in videoSkipped, as it does for a page that it cannot capture. Rust
reports a warning, and the file's result does not change. A missing
ffmpeg at startFlow is still a runtime error (SPEC 13).

The recorder now waits for ffmpeg's "close" event, not "exit". Node can
send "exit" before the last stderr output, and the warning then loses
ffmpeg's error text.

A new unit test runs a fake ffmpeg that writes part of its output,
prints an error, and exits with status 1. The recorder skips the
recording with that error and leaves no file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On GitHub Actions ubuntu-24.04, a lone VISIT /stable.html in
short_video_flows_on_still_pages_pass_with_a_recording passed with
"warning: video recording skipped: the page produced no frames, and
capturing it failed: ... Unable to capture screenshot". The test expects
a recording, so it failed. Chrome gives that error when the page has not
painted yet, as right after a navigation or on a new tab: it has nothing
to copy.

With 10 copies of the test at once on low-priority cores, the old
recorder failed 7 of 250 runs this way: 6 times right after a VISIT, and
once on the blank page of an HTTP-only flow. Each failed capture took 2
to 11 ms. With logging added, a new capture 25 ms later, or the one
after it, worked every time, within 360 ms.

Now, when a capture finds nothing painted, the recorder waits 50 ms and
tries again, for up to 1 second. The screencast still feeds the recorder
while it tries, so a frame that arrives is used as a capture would be.
When the recorder still has no frame, or the capture fails for another
reason, as for a crashed page, it holds a white frame of the viewport's
size for the whole recording, as Playwright's recorder does. All the
captures together still stop after 5 seconds. Only an ffmpeg failure
skips a recording now.

The white frame is a JPEG from jpegjs in playwright-core's utilsBundle,
the encoder that Playwright's recorder uses for its own white frame. The
package's export map includes that bundle, and the shim already takes
the image comparator from its core bundle. Node has no JPEG encoder, and
Playwright's ffmpeg decodes only MJPEG and VP8, so a PNG frame cannot go
into the stream.

The frame source now gives the retry time, so the unit tests use 200 ms,
not 1 second. New tests cover a capture that works on the second try, a
page that never paints (its decoded frames are white), a crashed page (a
white frame with no retry), and a screencast frame that arrives between
tries. The fake shim test now reports an ffmpeg failure, the only reason
for a skip.

After the change, under the same load, the short test passed 250 of 250
runs. For 100 of them, rounds of the old and the new recorder
alternated, so both had the same load; the old recorder failed 4 of its
100. In the 1,000 new recordings, 7 captures found nothing painted, and
each of those recordings got a frame on the second or third try, within
650 ms. No recording needed the white frame, and none was skipped. The
combined-report test passed 100 of 100 runs, as it passed 50 of 50
before. Its 18 recordings without a frame at the stop needed no retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the finished no-frames fix from #29: retries for a page that has
not painted, a white frame when there is still none, and a skip only
when ffmpeg fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brynary and others added 2 commits September 27, 2026 15:32
With --video on Chromium, a page that the recorder cannot capture, as a
crashed one, gets a recording that holds a white frame. The report then
showed a white video with no explanation. Now the shim says why in the
new endFlow field videoBlank, and Rust reports "warning: video recording
is blank: <reason>", masked like other warnings. The recording stays in
the report with its frame rate, and the file's result does not change.

A page that has not painted after 1 second of tries also gets the
warning, with the reason "the page did not paint within 1000ms". Its
video is white too, so it needs the same explanation. A capture that
takes more than 5 seconds gives "capturing the page took more than
5000ms". For a crashed page, Chrome answers "cdpSession.send: Protocol
error (Page.captureScreenshot): Internal error", and the warning shows
that text after "capturing the page failed:".

The recorder's unit tests now check the reason for a page that never
paints and for a crashed page. A real frame or a working capture still
gives no reason. The fake shim has a new FAKE_SHIM_VIDEO_BLANK option,
and a new acceptance test checks the masked warning in stdout and in
the JSON report, the passed status, the listed recording, and the frame
rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings the blank-recording warning from #29.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brynary added a commit that referenced this pull request Sep 27, 2026
Brings the blank-recording warning from #29 through #30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brynary
brynary added this pull request to stack #55 October 5, 2026 13:05
@brynary
brynary merged commit 6064828 into pointer-buttons Oct 5, 2026
3 checks passed
@brynary
brynary deleted the drop-file branch October 5, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant