🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

[fix] Add AppTest support for st.camera_input - #17459

Merged
lukasmasuch merged 3 commits into
developfrom
feature/apptest-p3-3-camera-input
Oct 13, 2026
Merged

lukasmasuch merged 3 commits into
developfrom
feature/apptest-p3-3-camera-input

Conversation

@lukasmasuch

@lukasmasuch lukasmasuch commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Describe your changes

AppTest can stage one JPEG photo for st.camera_input and read the file the script receives.

  • at.camera_input.value is the photo from the last .run() (UploadedFile or None). .set_value() and .upload() stage the next photo; .upload() replaces any staged photo because the widget holds one file. .clear() stages no photo. .resolution is "480p", "720p", "1080p", or None when the script omits it.
  • A filename that does not end in .jpg, a MIME type other than image/jpeg, or a list of photos raises AppTestError. The browser widget only produces one JPEG, so a one-item list is rejected too.
  • A photo inside a form stays uncommitted until that form is submitted.

Implements wiki queue P3.3 from the App testing fixes backlog.

GitHub Issue Link (if applicable)

N/A

Testing Plan

  • Unit Tests (JS and/or Python): lib/tests/streamlit/testing/element_tree_test.py covers a JPEG photo, persistence across reruns, on_change, disabled widgets, invalid files, and form submit / clear_on_submit.
  • E2E Tests: Not applicable (AppTest-only)
  • Any manual testing needed?: No

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

Testers can stage one JPEG photo the way the browser widget captures it.
Copilot AI balanced review requested due to automatic review settings October 13, 2026 08:20
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users feature:app-testing Related to `AppTest` testing framework labels Oct 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@snyk-io

snyk-io Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-17459/streamlit-1.65.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts

@greptile-apps

greptile-apps Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge.

Summary

Adds AppTest support for staging, reading, replacing, and clearing one JPEG photo from st.camera_input.

  • AppTest can stage and read one camera photo.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["set_value / upload / clear"] --> B["Stage camera photo"]
  B --> C["AppTest.run"]
  C --> D["Register uploaded bytes"]
  D --> E["Script receives UploadedFile or None"]
  E --> F["camera_input.value"]
Loading

Reviews (3) · Last reviewed commit: "Simplify AppTest camera resolution looku..." · Reviewed by Greptile

The docs now say which shapes are rejected and that clear stages no photo.
@lukasmasuch

Copy link
Copy Markdown
Collaborator Author

Added Agent Docs

@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Oct 13, 2026
@github-actions github-actions Bot removed the ai-review If applied to PR or issue will run AI review workflow label Oct 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds st.camera_input to the public AppTest API. at.camera_input returns a CameraInput node. .set_value(), .upload(), and .clear() stage one JPEG for the next .run(). .value is the committed UploadedFile or None. .resolution reports "480p", "720p", "1080p", or None. A filename that does not end in .jpg, a MIME type other than image/jpeg, a list, or non-bytes content raises AppTestError before the script runs. Staged bytes go through the existing in-memory upload registration path, so form submit and clear_on_submit behave like st.file_uploader and st.audio_input. The inspect-only example that used an untyped camera node now uses st.plotly_chart.

All three expected reviews completed: gpt-5.6-terra-xhigh, claude-opus-5-5-medium, and grok-4.7-xhigh. None failed to complete review. All three approve. They agree on product fit, correctness, test coverage, compatibility, security, performance, and external-test risk. The disagreements are optional wording and two small cleanups. Those are resolved below and do not block merge.

Product Alignment

This PR is user-facing for people writing AppTest tests. The production st.camera_input signature, capture behavior, and rendered UI stay as they are.

It is a small extension of the existing testing API, in the same shape as AudioInput from #17346, so it does not need a new product spec. The spec already merged on develop, specs/2026-05-19-camera-input-size/product-spec.md, defines resolution as None, "480p", "720p", or "1080p". .resolution reports those preset strings, mapped back from protobuf heights 480, 720, and 1080. The PR body does not claim a separate product approval; the behavior it describes matches the implementation.

All three reviews agree the surface follows existing AppTest file-widget conventions and the principles in specs/AGENTS.md:

  • One photo, a .jpg name, and image/jpeg match what the browser widget produces and what CameraInputSerde accepts (.jpg only).
  • .upload() replaces the staged photo. A list, including a one-item list, is rejected, consistent with AudioInput.
  • .value stays UploadedFile | None, the type st.camera_input returns, and it updates only after .run().
  • Invalid input fails in the test call with an AppTestError that names the rejected filename or MIME type.
  • A photo inside a form stays uncommitted until that form is submitted.

No material product-alignment issues.

Code Quality

CameraInput follows AudioInput: parse one file, stage (file_id, filename, content, mime_type), rebuild file_uploader_state_value, and reuse the session-state UploadedFile when the test has not staged a change. AppTest._register_uploaded_files, _unset_value_marker, and parse_tree_from_messages include the new class, so form gating and clear_on_submit use the same path as the other file widgets. The resolution map is the inverse of _RESOLUTION_TO_HEIGHT in lib/streamlit/elements/widgets/camera_input.py. An unknown height becomes None. The public widget only writes 480, 720, and 1080.

The reviews agree the implementation is correct. They differ on two cleanups, both optional:

  • claude-opus-5-5-medium suggests sharing _get_files_to_register, _widget_state, value, upload, and clear with AudioInput and FileUploader. gpt-5.6-terra-xhigh and grok-4.7-xhigh treat the explicit widget class as the established AppTest pattern. That fits this PR: AudioInput landed as its own class with the same structure. A shared helper is a reasonable later refactor if more single-file widgets appear.
  • claude-opus-5-5-medium says the HasField("resolution_height") branch in CameraInput.__init__ can be a single _CAMERA_RESOLUTION_BY_HEIGHT.get(proto.resolution_height). grok-4.7-xhigh says HasField is required because an unset optional int reads as 0. The field is optional int32 in CameraInput.proto. Unset reads as 0, and 0 is not a key in the map, so .get() already returns None for an omitted resolution, for an explicit 0, and for any height other than 480, 720, or 1080. The branch and the direct lookup return the same value. The current code is correct; the shorter form is an optional simplification.

Performance

No production hot-path changes. There is no frontend code, and the backend widget, protobuf, session state, and WebSocket paths are untouched. The extra camera widgets in _register_uploaded_files run once per AppTest.run(), with the same per-file copy FileUploader and AudioInput already do inside the test runner. All three reviews agree. No benchmark coverage is warranted.

Test Coverage

The unit tests in lib/tests/streamlit/testing/element_tree_test.py match the audio_input suite and cover the new behavior:

  • A JPEG commits on .run(), .value stays unchanged until then, and a second .upload() replaces the staged photo.
  • .JPG / Image/JPEG is accepted and stored as image/jpeg. enforce_filename_restriction lowercases the name before the .jpg check, so the case-variant matches the backend.
  • The photo survives a later .run() without firing on_change again, and .clear() removes it and fires on_change.
  • .resolution is "720p" when the script sets it and None when the script omits it, including a sidebar widget.
  • .jpeg, a non-JPEG MIME type, a list, non-bytes content, and raw bytes raise AppTestError.
  • A disabled widget rejects .upload() and .clear().
  • A form photo stays uncommitted until submit, and clear_on_submit clears it on the next submit.

E2E coverage is unnecessary. AppTest runs the script in-process. Browser capture stays covered by e2e_playwright/st_camera_input_test.py. No typing test is added, which matches AudioInput; AppTest accessors are not covered under lib/tests/streamlit/typing. All three reviews agree this coverage is sufficient.

Backwards Compatibility

App scripts are unchanged. st.camera_input still returns UploadedFile | None.

at.camera_input is new. at.get("camera_input") now returns CameraInput where it used to return UnknownElement, and .set_value() stages a photo where it used to raise the inspect-only error. .click() still raises AppTestError. The in-repo inspect-only example moved to st.plotly_chart, which remains an UnknownElement. All three reviews treat this as additive. Tests outside this repo that pinned the old "not supported" string for camera_input would need the same fixture swap.

Security & Risk

No security concern. Test-supplied bytes stay in MemoryUploadedFileManager. The /mock/upload/... URLs are copied onto the proto and are not routes. There is no new dependency, endpoint, cookie, HTML rendering, or dynamic code execution. .jpg checks run before the script, and deserialize still calls enforce_filename_restriction. All three reviews agree. Regression risk is limited to the AppTest harness.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • lib/streamlit/testing/v1/element_tree.py: CameraInput builds an in-memory FileUploaderState for the test runner. It does not change upload routes, cookies, WebSocket handshake, iframe/postMessage, asset serving, CORS, CSP, or browser storage.
    • lib/streamlit/testing/v1/app_test.py: _register_uploaded_files passes staged bytes to the existing MemoryUploadedFileManager through LocalScriptRunner.register_file.
    • lib/tests/streamlit/testing/element_tree_test.py: in-process AppTest cases only.
  • Suggested external_test focus areas: None. Embedded, proxied, and SiS runs do not exercise this API. Existing camera-input e2e coverage remains the browser check.
  • Confidence: High
  • Assumptions and gaps: The diff is limited to the AppTest harness and its unit tests. All three reviews reached the same recommendation.

Accessibility

No frontend or rendered UI changes. Accessibility is unchanged. All three reviews agree.

Readability

lib/streamlit/testing/v1/element_tree.py

CameraInput.value — claude-opus-5-5-medium proposed a rewrite so the docstring would lead with the committed UploadedFile. The docstring already opens with "The photo committed by the last run()." gpt-5.6-terra-xhigh and grok-4.7-xhigh found it consistent with AudioInput. Leave it.

_parse_camera_input_file MIME comment — grok-4.7-xhigh preferred AudioInput's longer "Normalize case-variants..." wording. The current comment already says the return is the browser MIME type for case-variants such as "Image/JPEG". The other two reviews did not flag it. The current sentence is enough.

CameraInput.__init__ resolution comment — grok-4.7-xhigh would keep HasField and rewrite the comment to say the check is required. That would document a distinction the map does not make: an unset field and a height of 0 both become None. The optional simplification is the inline comment. The current comment correctly describes proto presence.

lib/tests/streamlit/testing/element_tree_test.py

test_camera_input_captures_one_jpeg — claude-opus-5-5-medium called the docstring a long list of behaviors. It is one sentence: "One JPEG is committed on run, replaced by upload, kept across reruns, and cleared." That meets the brief-docstring guidance in lib/tests/AGENTS.md. gpt-5.6-terra-xhigh found the test names and docstrings clear. Leave it.

PR title and description

Title — [feature] Add AppTest support for st.camera_input stands alone and uses the [type] Description form. All three reviews accept it.

Description — The opening states the change. The bullets explain single-file JPEG validation, staging, and form submit. claude-opus-5-5-medium would rephrase "wiki queue P3.3" and mention the st.plotly_chart fixture swap. The backlog link already sits next to P3.3, which is the context grok-4.7-xhigh and gpt-5.6-terra-xhigh treat as sufficient, and the testing plan already points at element_tree_test.py. grok-4.7-xhigh would give .resolution its own bullet. Those values are already stated at the end of the first bullet. The description can stay as written.

Recommendations

  1. No change is required before merge.
  2. Optional: apply the inline suggestion and replace the HasField branch in CameraInput.__init__ with a single map lookup.
  3. Optional follow-up, outside this PR: shared helpers for the single-file upload widgets, if another widget of this shape is added. Two of the three reviews would leave CameraInput as its own class.

Verdict

APPROVED: All three reviews approve, and the implementation matches the AudioInput testing API and the camera widget's single-JPEG contract, with unit coverage for commit, replace, persistence, validation, disabled widgets, and forms.


This is an automated AI review consolidated by grok-4.7-xhigh. Please verify the feedback and use your judgment.

This review also includes 2 inline comment(s) on specific code lines.

Comment thread lib/streamlit/testing/v1/element_tree.py Outdated
Comment thread lib/streamlit/testing/v1/element_tree.py
@lukasmasuch lukasmasuch added the ai-final-review If applied to PR or issue will run AI review workflow with the latest generation of AI models. label Oct 13, 2026
@lukasmasuch lukasmasuch changed the title [feature] Add AppTest support for st.camera_input [fix] Add AppTest support for st.camera_input Oct 13, 2026
@lukasmasuch lukasmasuch added change:bugfix PR contains bug fix implementation ai-final-review If applied to PR or issue will run AI review workflow with the latest generation of AI models. and removed change:feature PR contains new feature or enhancement implementation feature:app-testing Related to `AppTest` testing framework ai-final-review If applied to PR or issue will run AI review workflow with the latest generation of AI models. labels Oct 13, 2026
@github-actions github-actions Bot removed the ai-final-review If applied to PR or issue will run AI review workflow with the latest generation of AI models. label Oct 13, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds typed AppTest support for st.camera_input. Tests can stage, replace, and clear one JPEG, read the UploadedFile committed by the last .run(), and inspect resolution. The st.camera_input command and the browser widget are unchanged.

All four expected reviews completed: gpt-5.6-sol-high, claude-opus-5-5-high, gemini-3.8-flash-high, and grok-4.7-xhigh. Each verdict is APPROVED. No expected model failed to complete a review.

The implementation follows the AudioInput path already on develop:

  • CameraInput in lib/streamlit/testing/v1/element_tree.py accepts one .jpg / image/jpeg photo, exposes value, resolution, set_value(), upload(), and clear(), and builds file_uploader_state_value.
  • AppTest.camera_input and ElementTree.camera_input expose the nodes. _register_uploaded_files registers the bytes with MemoryUploadedFileManager. _unset_value_marker includes CameraInput, so form submit and clear_on_submit use the shared file-widget logic.
  • Unit tests cover commit versus staged value, persistence, on_change, replacement (including .JPG / Image/JPEG), clear, disabled widgets, invalid payloads, and forms. The inspectable-only fixture moved from camera_input to plotly_chart.

The reviewers agreed this is a small, additive testing-API change. They differed only on optional maintainability and wording, covered below.

Product Alignment

The user-facing impact is the public streamlit.testing.v1 surface: at.camera_input and the CameraInput node. st.camera_input's signature, return type, and browser behavior stay the same.

No dedicated AppTest spec is required, and all four reviewers agreed. specs/2026-05-19-camera-input-size/product-spec.md, already on the base branch, defines resolution as "480p", "720p", "1080p", or omitted. CameraInput.resolution returns those literals, or None when the proto height is unset (protobuf returns 0 for an omitted optional int, and 0 is absent from the height map). Claude treated that spec as covering the widget's resolution values and the AppTest API as a separate, non-controversial extension. The other three cited the same spec for the exposed strings. Those readings agree: the spec approves the strings, and the testing API is an incremental extension of AudioInput and FileUploader (file_uploader, audio_input, pagination, and link_button landed the same way).

The surface matches the API principles in specs/AGENTS.md. upload() defaults the MIME type to image/jpeg. set_value(), clear(), and value match the neighboring upload widgets. A non-.jpg name, a non-JPEG MIME type, or a list fails in AppTest before the script run, which matches the browser capture and CameraInputSerde.enforce_filename_restriction(..., [".jpg"]). The PR body does not claim a separate product approval, and none is needed for this scope.

Code Quality

The integration points are complete and consistent with AudioInput:

  • parse_tree_from_messages builds a CameraInput for the camera_input proto (lib/streamlit/testing/v1/element_tree.py around line 3945).
  • ElementTree.camera_input (around line 3146) and AppTest.camera_input (lib/streamlit/testing/v1/app_test.py around line 849) expose it.
  • _register_uploaded_files (around line 586) includes *self._tree.camera_input.
  • _unset_value_marker (around line 3672) treats CameraInput._files like AudioInput and FileUploader.

Typing follows the file: Self returns, proto types, and # ty: ignore[invalid-method-override] on set_value.

Claude flagged two duplication sites. Both are real:

  • _CAMERA_RESOLUTION_BY_HEIGHT (element_tree.py lines 1733–1737) inverts _RESOLUTION_TO_HEIGHT in lib/streamlit/elements/widgets/camera_input.py (lines 60–64). The three heights match today. A future resolution would read as None in AppTest until this map is updated, and the current tests would still pass.
  • _get_files_to_register and _widget_state (lines 1889–1941) repeat the AudioInput implementations, so the FileUploaderState loop now exists in three classes.

Gemini, GPT, and Grok treat that copy as the existing per-widget pattern and proportionate to the feature. That is the right call for merge: the class stays easy to follow, and every integration point is updated. Sharing the resolution map is the higher-value follow-up because drift fails silently. Extracting upload helpers can wait.

Performance

There is no production backend or frontend hot-path change. The new work runs only inside AppTest: one pass over camera widgets and the same in-memory registration st.file_uploader and st.audio_input already do, including a getvalue() copy when a committed photo is replayed on a later .run(). All four reviewers agreed. Performance coverage is not warranted.

Test Coverage

Unit coverage is sufficient. Playwright e2e coverage does not apply, because the change is an in-process AppTest simulation.

test_camera_input_captures_one_jpeg checks the empty state, a committed JPEG, that a second upload() leaves value pointing at the previous UploadedFile until .run(), persistence across a plain rerun, on_change incrementing once per committed change, .JPG / Image/JPEG normalization, repr, click() rejection, and clear(). test_camera_input_rejects_invalid_photos covers a .jpeg name, a non-JPEG MIME type, a list, string content, and raw bytes, and asserts value stays empty after the rejection. Disabled widgets and form submit / clear_on_submit have their own tests. "720p" and an omitted resolution are asserted.

Claude called out two small gaps. Checked against the implementation:

  • Mixed-case MIME through set_value() has no direct case. upload() calls set_value(), which calls _parse_camera_input_file, and the happy-path test already uploads mime_type="Image/JPEG". That covers the shared parser.
  • "480p" and "1080p" are unasserted. They use the same _CAMERA_RESOLUTION_BY_HEIGHT.get lookup as "720p".

A one-item list hits the same isinstance(file, list) branch as the two-item case the parametrize already runs. These are optional extras, and the other three reviewers treated the suite as sufficient.

Backwards Compatibility

App scripts are unchanged. at.camera_input is new. Nodes that previously parsed as UnknownElement now parse as CameraInput. .type stays "camera_input". .key, .label, and .proto still come from Widget. set_value() on a valid JPEG tuple now stages a photo. Invalid payloads still raise AppTestError. click() still raises. All four reviewers agreed this is an additive upgrade of an inspect-only node, with the same shape as the earlier AudioInput change.

Security & Risk

No new routes, auth, cookies, CORS, CSP, filesystem writes, or browser upload behavior. Photos stay in AppTest's MemoryUploadedFileManager and use the existing /mock/upload/test session id/… URLs. Filename and MIME checks limit staged values to one JPEG. CameraInputSerde still enforces .jpg when the script runs. Regression risk is low because the path mirrors the tested audio and file widgets. All four reviewers agreed.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • lib/streamlit/testing/v1/app_test.py: in-memory registration of camera photos with MemoryUploadedFileManager. No upload endpoint, route, transport, or external-host change.
    • lib/streamlit/testing/v1/element_tree.py: local test representation and widget-state serialization. Bytes are attached to file_uploader_state_value the same way as AudioInput.
    • lib/tests/streamlit/testing/element_tree_test.py: in-process AppTest unit tests.
  • Suggested external_test focus areas: None. Embedded and hosted runs do not execute this harness.
  • Confidence: High. All four reviewers agreed, assuming AppTest remains an in-process harness and production camera/upload infrastructure is unchanged on the base branch.

Accessibility

Not applicable. There are no frontend or rendered-UI changes. All four reviewers agreed.

Readability

GPT and Gemini found the code comments and docstrings clear. Claude and Grok proposed small rewrites. The current text supports the rewrites as optional polish.

lib/streamlit/testing/v1/element_tree.py

  • Line 1764: # Return the browser MIME type for case-variants such as "Image/JPEG". The sibling AudioInput comment states the rule. Proposed: # Normalize case-variants such as "Image/JPEG" to the MIME type the browser widget reports.
  • Lines 1787–1788 and set_value (lines 1836–1837): the checks use .lower(), and a mixed-case MIME is stored as image/jpeg. The docs read as exact matches. Add "(any case)" on the filename and MIME type, and say the stored MIME type is image/jpeg.
  • Lines 1822–1823: the comment leads with the proto default. Proposed: # Omitted and unknown capture heights are both None. Protobuf returns 0 for an unset optional int, and 0 is not 480, 720, or 1080.
  • clear (line 1880): """Clear the photo.""" omits that the clear is staged. Proposed: Stage no photo. ``value`` updates on the next ``run()``.

lib/streamlit/testing/v1/app_test.py and lib/tests/streamlit/testing/element_tree_test.py

Names, the updated upload-registration docstring, and the test docstrings are clear. No rewrite.

PR title and description

The four reviews described the title as [feature] Add AppTest support for st.camera_input and accepted it. The current title is [fix] Add AppTest support for st.camera_input, and the PR is labeled change:bugfix. This adds a public testing API. [feature] and change:feature match the types in wiki/pull-requests.md. Either title fits the 63-character limit.

The description leads with the user-visible behavior and covers single-JPEG validation and form commit. Two wording fixes, both optional:

  • The first bullet says at.camera_input.value. at.camera_input is a WidgetList. The accessors are at.camera_input[0].value and at.camera_input(key="...").value. Gemini flagged this, and the AppTest.camera_input docstring already shows both forms.
  • That same bullet packs value, staging, clear, and resolution into one sentence, and "wiki queue P3.3" is an internal id. Split those behaviors, and link the App testing fixes backlog without the queue id.

GPT found the description adequate as written. The edits above are optional.

Recommendations

  1. Optionally derive _CAMERA_RESOLUTION_BY_HEIGHT from the widget resolution map, or share one constant, so a new resolution cannot read as None in AppTest.
  2. Optionally extract the shared single-file upload helpers used by AudioInput, CameraInput, and FileUploader. This can be a follow-up.
  3. Optionally apply the docstring, comment, title/label, and PR-description edits above. If the list-rejection message is rewritten to name the (filename, content, mime_type) tuple, update the unit-test match and the parallel AudioInput message.

Verdict

APPROVED: All four reviews approve this additive, well-tested AppTest extension, and the remaining notes are optional maintainability and wording follow-ups.


This is an automated AI review consolidated by grok-4.7-xhigh. Please verify the feedback and use your judgment.

This review also includes 5 inline comment(s) on specific code lines.



_CAMERA_INPUT_MIME: Final = "image/jpeg"
_CAMERA_RESOLUTION_BY_HEIGHT: Final[dict[int, str]] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: _CAMERA_RESOLUTION_BY_HEIGHT is a hand-copied inverse of _RESOLUTION_TO_HEIGHT in lib/streamlit/elements/widgets/camera_input.py. The three heights match today, but a new resolution such as "1440p" would make CameraInput.resolution return None with no test failure. Derive it, for example {height: label for label, height in camera_input._RESOLUTION_TO_HEIGHT.items()}, or share one non-private constant.

return filename, bytes(content), _CAMERA_INPUT_MIME
if isinstance(file, list):
raise AppTestError(
"st.camera_input captures one photo. A list is not accepted."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Name the shape that works. A one-item list is the FileUploader.set_value habit, and this branch only says a list is rejected. Prefer st.camera_input captures one photo. Pass one (filename, content, mime_type) tuple, not a list. If the text changes, update the "list is not accepted" match in test_camera_input_rejects_invalid_photos, and keep AudioInput's matching message in sync.

]

@property
def _widget_state(self) -> WidgetState:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: _get_files_to_register and _widget_state copy the AudioInput versions, so the FileUploaderState proto loop now lives in AudioInput, CameraInput, and FileUploader. A follow-up can extract helpers such as _committed_single_upload(root, widget_id) and _file_uploader_widget_state(widget_id, files) so a mock-URL or file-info fix lands once.

``.upload()`` replaces any staged photo. Unlike ``FileUploader``, this
widget does not accept a list or append files.

``AppTest`` accepts a photo only when:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: These bullets repeat the "one JPEG" rule above, and they read as exact matches even though the checks are case-insensitive (.JPG and Image/JPEG are accepted and the MIME type is stored as image/jpeg). Lead with the rule: the photo is one (filename, content, mime_type) tuple; the filename must end in .jpg (any case); the MIME type must be image/jpeg (any case). .upload() replaces any staged photo, and unlike FileUploader this widget does not accept a list. resolution is "480p", "720p", "1080p", or None when the script omits it.

f"st.camera_input photos use MIME type {_CAMERA_INPUT_MIME!r}. "
f"Got {mime_type!r}."
)
# Return the browser MIME type for case-variants such as "Image/JPEG".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: "Return the browser MIME type" reads as a lookup. Match the AudioInput comment: # Normalize case-variants such as "Image/JPEG" to the MIME type the browser widget reports.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved by AI final review (workflow run).

@lukasmasuch
lukasmasuch merged commit 10da7ea into develop Oct 13, 2026
97 checks passed
@lukasmasuch
lukasmasuch deleted the feature/apptest-p3-3-camera-input branch October 13, 2026 10:08

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

AppTest can drive st.camera_input the same way it drives st.audio_input. at.camera_input returns a typed CameraInput: tests stage one JPEG with .set_value() or .upload(), clear it with .clear(), and read the UploadedFile committed by the last .run(). .resolution reports "480p", "720p", "1080p", or None. Invalid filenames, MIME types, and lists raise AppTestError. A photo inside a form stays uncommitted until submit, including clear_on_submit.

All four expected reviews completed (gpt-5.6-sol-high, claude-opus-5-5-high, gemini-3.8-flash-high, grok-4.7-xhigh). None failed to complete review. All four approve. They agree the change is additive, limited to the in-process test harness, and consistent with the merged camera-resolution spec and the existing file-widget testing API.

Product Alignment

This PR is user-facing for authors of streamlit.testing.v1.AppTest. It does not change st.camera_input in running apps.

The merged spec specs/2026-05-19-camera-input-size/product-spec.md defines resolution as None, "480p", "720p", or "1080p". .resolution maps the protobuf heights 480, 720, and 1080 back to those labels, matching _RESOLUTION_TO_HEIGHT in camera_input.py. The PR body states the testing contract and does not claim a separate product approval. AppTest support for an existing widget is a small enhancement, so a new product spec is not required.

The surface matches AudioInput and the API principles in specs/AGENTS.md:

  • The common call is upload("shot.jpg", jpeg_bytes), with MIME type defaulting to image/jpeg.
  • .upload() replaces the staged photo, because the widget holds one file. FileUploader.upload() still appends, which is the right split for a multi-file widget.
  • A non-.jpg name, a non-image/jpeg MIME type, and any list fail immediately with AppTestError. The browser widget emits one image/jpeg whose name ends in .jpg (CameraInput.tsx), and CameraInputSerde enforces .jpg on the way back in. .jpeg is rejected in both places.
  • .resolution is read-only. AppTest does not resize the staged bytes, which matches how .sample_rate is exposed for audio and how the spec treats the returned image as whatever the camera actually captured.

Reviewers agreed there is no material product or API mismatch.

Code Quality

CameraInput is wired through the three existing extension points: element parsing, _register_uploaded_files, and _unset_value_marker. Form gating, file-id reuse across reruns, and disabled-widget checks stay on that shared path.

Reviewers differed on the copy of AudioInput (about 200 lines in _parse_camera_input_file and CameraInput). One review suggested a private _SingleFileInputWidget base class. The copy should stay. lib/AGENTS.md prefers composition over inheritance, and a new abstraction for two widgets would add more structure than this PR needs. A shared helper can wait until a third single-file widget appears.

Reviewers also differed on resolution lookup. One review suggested proto.HasField("resolution_height"), matching AudioInput.sample_rate. The dict lookup is the right check here. sample_rate needs HasField because an unset proto field reads as 0, and 0 is a different signal from sample_rate=None. For camera, an omitted optional int32 also reads as 0, and 0 is outside {480, 720, 1080}, so omitted and unknown heights both become None. That matches the latest commit, which simplified this lookup on purpose. The public presets still round-trip.

enforce_filename_restriction lowercases extensions, so the test upload of other.JPG is consistent with the backend.

Performance

No production hot-path impact. The new work runs inside AppTest when a test reruns a script that contains a camera widget. Persisting a photo copies that file's bytes once via UploadedFile.getvalue(), the same as audio and st.file_uploader. Widget parsing adds one camera_input branch. No extra renders, remounts, or full-state scans on the app runtime. Benchmark coverage is not warranted.

Test Coverage

Unit coverage in lib/tests/streamlit/testing/element_tree_test.py matches the audio tests and is sufficient to merge:

  • Commit, session state, on_change, upload replacement (including .JPG and Image/JPEG), persistence across a plain .run(), repr, rejected .click(), and .clear().
  • Parametrized rejection of .jpeg, the wrong MIME type, a list, non-bytes content, and raw bytes. Any list is rejected, so a one-item list hits the same branch.
  • Disabled widgets, form submit, and clear_on_submit.
  • Sidebar lookup for an omitted resolution.

One review asked to split test_camera_input_captures_one_jpeg and to annotate the nested script() with -> None. The long test should stay. It mirrors test_audio_input_records_one_wav in the same file, and lib/tests/AGENTS.md asks for targeted checks rather than a larger matrix. The nested script() return annotation matches that audio test; the form test already uses -> None, as the audio form test does.

Replacing st.camera_input with st.plotly_chart in the inspectable-element test keeps an UnknownElement assertion. Playwright coverage is unnecessary. This change never renders the camera widget, and existing camera e2e tests still cover browser capture.

Backwards Compatibility

st.camera_input in running apps is unchanged.

For AppTest, a camera widget used to be an UnknownElement. Inspecting .value, .label, and proto fields such as resolution_height still works. .set_value() and .upload() now stage a photo where they previously raised. .click() still raises AppTestError, with the typed-widget message. Tests that asserted the old "does not implement interactions" string for camera_input need the new message; this PR updates the in-repo test. CameraInput is reached through at.camera_input, the same way AudioInput is reached, and testing.v1.__all__ stays AppTest and AppTestError.

Security & Risk

Staged bytes stay in MemoryUploadedFileManager under the existing AppTest session id. The filename is metadata, not a filesystem path. Widget state uses the same mock /mock/upload/test session id/{file_id} URLs as audio and file uploads. No new route, cookie, websocket, HTML, subprocess, or dependency. CameraInputSerde still rejects names that do not end in .jpg if a payload reaches the widget.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • lib/streamlit/testing/v1/element_tree.py: in-process CameraInput widget state and JPEG checks only. Mock file URLs are copied from the existing audio and file-uploader AppTest path. Upload endpoints, CSRF wrapping, and download attributes are unchanged.
    • lib/streamlit/testing/v1/app_test.py: _register_uploaded_files also registers camera_input bytes with MemoryUploadedFileManager. No server route, websocket, embedding, or origin behavior.
    • lib/tests/streamlit/testing/element_tree_test.py: AppTest unit tests only.
  • Suggested external_test focus areas:
    • None. Externally hosted, proxied, embedded, and SiS deployments do not exercise this API.
  • Confidence: High
  • Assumptions and gaps: Form commit, clear_on_submit, and file-id reuse use the same helpers as AudioInput, which is already on develop. Production camera capture and upload code are unchanged.

Accessibility

No frontend or rendered-UI change. The camera widget's browser behavior is unchanged.

Readability

Reviewers agreed the title [fix] Add AppTest support for st.camera_input stands alone and fits the title pattern. They differed on the description. The body already leads with the capability and explains the single-JPEG and form rules. The one clear improvement is dropping the internal queue id.

PR description

  • Issue: "wiki queue P3.3" is backlog jargon. The link already identifies the item.
  • Proposed rewrite: Implements the camera input item from the [App testing fixes backlog](https://github.com/streamlit/streamlit/wiki/2026-07-22-app-testing-fixes).

lib/streamlit/testing/v1/element_tree.py

  • CameraInput class docstring, line 1781. "The photo is one JPEG file, (filename, content, mime_type)" can be read as the shape of .value. .value is an UploadedFile. Proposed opening:

    """A representation of ``st.camera_input``.
    
    Stage one JPEG as ``(filename, content, mime_type)``. ``.value`` returns
    the ``UploadedFile`` committed by the last ``.run()``, and ``.upload()``
    replaces any staged photo. Unlike ``FileUploader``, this widget does not
    accept a list or append files.
  • CameraInput.__init__, lines 1822–1824. The comment opens with proto mechanics. Lead with the rule:

    # None means the script omitted resolution. An unset optional int reads as
    # 0, and 0 is not a mapped height, so an unknown height is None too.
    self.resolution = _CAMERA_RESOLUTION_BY_HEIGHT.get(proto.resolution_height)

The remaining comments, names, and docstrings match AudioInput and are clear. The "or None to clear" text in _parse_camera_input_file matches the audio helper; set_value already handles None before that helper runs.

Recommendations

  1. No merge-blocking changes. The docstring and PR-description rewrites above are optional.
  2. Optional follow-up: add CameraInput to the public AppTest API reference if that page is maintained outside this repo. The class docstring already documents the testing contract.

Verdict

APPROVED: AppTest camera support matches the audio-input testing API, preserves st.camera_input for running apps, and has unit coverage for commit, rejection, disabled widgets, and forms.


This is an automated AI review consolidated by grok-4.7-xhigh. Please verify the feedback and use your judgment.

This review also includes 1 inline comment(s) on specific code lines.

cam = at.camera_input(key="cam")
assert cam.value is None
assert cam.label == "Cam"
assert cam.resolution == "720p"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Parameterize this assertion over "480p", "720p", and "1080p". .resolution is a closed public map of those three heights, and this test only locks 720p plus the omitted None case.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved by AI final review (workflow run).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants