Compare commits

...

4 Commits

Author SHA1 Message Date
Daniel Lok e891985ce1 style(e2e_ui): format assert with ruff 0.15.16 to match CI
Co-authored-by: Isaac
2026-06-16 16:50:34 -07:00
Daniel Lok 002d649f90 style(e2e_ui): apply ruff format to composer attachments test
Co-authored-by: Isaac
2026-06-16 16:47:45 -07:00
Daniel Lok 3e317e80ba test(e2e_ui): cover JSON attachment in the chat composer
Adds test_attach_json_file to the composer attachments suite, guarding the
accept-list change. It asserts the hidden file input advertises
application/json (what the OS picker and the drag-drop matchesAccept
validator read) and that a real .json file drives the attach -> chip ->
remove flow end-to-end.

Satisfies the "E2E UI Required" gate, which flagged the ap-web accept-list
change as a user-facing behavior change without e2e_ui coverage.

Co-authored-by: Isaac
2026-06-16 15:06:05 -07:00
Daniel Lok 8b3493243e feat(ap-web): allow JSON files in the chat attachment picker
Add application/json to the accept lists for both the landing-page and
in-session chat composers so .json files can be attached. The backend
content_resolver already passes application/json through to providers,
so no server-side change is needed.

Co-authored-by: Isaac
2026-06-16 14:39:19 -07:00
3 changed files with 47 additions and 5 deletions
+1 -1
View File
@@ -3324,7 +3324,7 @@ export function Composer({
ref={fileInputRef}
type="file"
multiple
accept="image/*,application/pdf,text/*"
accept="image/*,application/pdf,text/*,application/json"
className="hidden"
onChange={(e) => {
if (e.target.files) {
+1 -1
View File
@@ -1298,7 +1298,7 @@ export function NewChatLandingScreen() {
ref={fileInputRef}
type="file"
multiple
accept="image/*,application/pdf,text/*"
accept="image/*,application/pdf,text/*,application/json"
className="hidden"
data-testid="new-chat-landing-file-input"
onChange={(e) => {
+45 -3
View File
@@ -26,12 +26,19 @@ from pathlib import Path
from playwright.sync_api import Page, expect
_COMPOSER = "Ask the agent anything…"
# Composer accepts image/*,application/pdf,text/* (the hidden input's accept
# attr); a .txt file is in-scope and keeps the fixture trivial. ``set_input_files``
# bypasses the accept filter anyway — ``addFiles`` does no client-side filtering.
# Composer accepts image/*,application/pdf,text/*,application/json (the hidden
# input's accept attr); a .txt file is in-scope and keeps the fixture trivial.
# ``set_input_files`` bypasses the accept filter anyway — ``addFiles`` does no
# client-side filtering.
_ATTACH_NAME = "attach_sample.txt"
_ATTACH_BODY = "composer attachment e2e sample\n"
# JSON is its own MIME (``application/json``), which is NOT covered by the
# ``text/*`` wildcard, so it has to be listed in the ``accept`` attr explicitly
# for the OS picker (and the drag-drop ``matchesAccept`` validator) to admit it.
_JSON_NAME = "attach_sample.json"
_JSON_BODY = '{"composer": "attachment", "e2e": true}\n'
def test_attach_then_remove_file(
page: Page, seeded_session: tuple[str, str], tmp_path: Path
@@ -59,3 +66,38 @@ def test_attach_then_remove_file(
remove_button.click()
expect(remove_button).to_be_hidden(timeout=10_000)
expect(page.get_by_text(_ATTACH_NAME, exact=True)).to_be_hidden()
def test_attach_json_file(page: Page, seeded_session: tuple[str, str], tmp_path: Path) -> None:
"""A ``.json`` file is admitted by the picker and attaches as a chip.
Guards the change that added ``application/json`` to the composer's
``accept`` list. Two things are asserted:
1. The hidden input advertises ``application/json`` in its ``accept`` attr.
This is the part the OS file picker and the drag-drop ``matchesAccept``
validator (``prompt-input.tsx``) actually read — and the part that would
regress if the MIME were dropped from the list. ``set_input_files`` can't
cover it because it bypasses the accept filter entirely.
2. Driving a real ``.json`` file through the input still yields the chip +
remove control, i.e. ``addFiles`` accepts the JSON end-to-end.
"""
base_url, session_id = seeded_session
sample = tmp_path / _JSON_NAME
sample.write_text(_JSON_BODY)
page.goto(f"{base_url}/c/{session_id}")
expect(page.get_by_placeholder(_COMPOSER)).to_be_visible(timeout=30_000)
file_input = page.locator('input[type="file"][accept*="image/"]')
# The accept attr is what gates the picker/drag-drop; assert JSON is listed.
accept = file_input.get_attribute("accept")
assert accept is not None and "application/json" in accept, (
f"composer file input should accept application/json; got {accept!r}"
)
file_input.set_input_files(str(sample))
remove_button = page.get_by_role("button", name=f"Remove {_JSON_NAME}")
expect(remove_button).to_be_visible(timeout=10_000)
expect(page.get_by_text(_JSON_NAME, exact=True)).to_be_visible()