Skip to content

Add UploadButton unit tests#13336

Open
freddyaboulton wants to merge 5 commits intomainfrom
13109-upload-unit-tests
Open

Add UploadButton unit tests#13336
freddyaboulton wants to merge 5 commits intomainfrom
13109-upload-unit-tests

Conversation

@freddyaboulton
Copy link
Copy Markdown
Collaborator

@freddyaboulton freddyaboulton commented Apr 28, 2026

Description

Closes: #13109

AI Disclosure

We encourage the use of AI tooling in creating PRs, but the any non-trivial use of AI needs be disclosed. E.g. if you used Claude to write a first draft, you should mention that. Trivial tab-completion doesn't need to be disclosed. You should self-review all PRs, especially if they were generated with AI.

  • I used AI to... [fill here]
  • I did not use AI

🎯 PRs Should Target Issues

Before your create a PR, please check to see if there is an existing issue for this change. If not, please create an issue before you create this PR, unless the fix is very small.

Not adhering to this guideline will result in the PR being closed.

Testing and Formatting Your Code

  1. PRs will only be merged if tests pass on CI. We recommend at least running the backend tests locally, please set up your Gradio environment locally and run the backed tests: bash scripts/run_backend_tests.sh

  2. Please run these bash scripts to automatically format your code: bash scripts/format_backend.sh, and (if you made any changes to non-Python files) bash scripts/format_frontend.sh


Note

Medium Risk
Behavior changes now reject non-matching file_types at selection time, which could impact existing upload flows relying on permissive client-side handling; otherwise changes are isolated to a single component plus tests.

Overview
Adds a full Vitest suite for UploadButton covering shared props, file_count return shapes, file_types accept mapping + rejection behavior, icon rendering, event dispatch semantics, get_data/set_data, and max_file_size forwarding.

Adjusts Index.svelte to derive value directly from gradio.props.value (removing the manual $state/$effect sync), and updates the shared implementation to validate selected files against the computed accept rules (wildcard MIME categories and extensions), filtering invalid files and emitting an error instead of uploading them. A changeset bumps @gradio/uploadbutton and gradio as patch releases.

Reviewed by Cursor Bugbot for commit 4fb675e. Bugbot is set up for automated code reviews on this repo. Configure here.

@freddyaboulton freddyaboulton marked this pull request as ready for review April 28, 2026 20:59
@gradio-pr-bot
Copy link
Copy Markdown
Collaborator

gradio-pr-bot commented Apr 28, 2026

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Storybook ready! Storybook preview
🦄 Changes detected! Details

Install Gradio from this PR

pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/4fb675ec39aad9a238de1d41d386ead21933fcb8/gradio-6.13.0-py3-none-any.whl

Install Gradio Python Client from this PR

pip install "gradio-client @ git+https://github.com/gradio-app/gradio@4fb675ec39aad9a238de1d41d386ead21933fcb8#subdirectory=client/python"

Install Gradio JS Client from this PR

npm install https://gradio-npm-previews.s3.amazonaws.com/4fb675ec39aad9a238de1d41d386ead21933fcb8/gradio-client-2.2.0.tgz

@gradio-pr-bot
Copy link
Copy Markdown
Collaborator

gradio-pr-bot commented Apr 28, 2026

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
@gradio/uploadbutton patch
gradio patch

  • Add UploadButton unit tests

✅ Changeset approved by @freddyaboulton

  • Maintainers can remove approval by unchecking this checkbox.

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

@pngwn pngwn marked this pull request as draft April 29, 2026 14:10
@pngwn pngwn marked this pull request as ready for review April 29, 2026 14:10
Comment thread js/uploadbutton/UploadButton.test.ts
Copy link
Copy Markdown
Member

@pngwn pngwn left a comment

Choose a reason for hiding this comment

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

Reviewed against the frontend unit testing skill. Solid foundation — run_shared_prop_tests invocation, mount-time event handling with { retrospective: true }, semantic queries, and test.todo placeholders for visual-only props are all on point.

Main feedback (inline):

  1. Props: file_count tests assert HTML attributes instead of behaviour. The skill is explicit that this is the wrong shape for this kind of test — drive uploads and assert on the resulting value instead.
  2. Props: file_types has the same shape, plus a redundant "empty array → empty string" test and a missing null-input case.
  3. get_data / set_data block is missing the set_data half and the user-interaction round-trip — both are required by the skill.
  4. The max_file_size prop has no coverage at all; it should be asserted to flow through to client.upload.
  5. Minor: the manual { upload, stream } object in the error test can be replaced with { ...mock_client(), upload: failing_upload }.

Plus a few nits inline (icon src assertion, null as any casts, click-side-effect clarification).


Generated by Claude Code

Comment on lines +75 to +107
describe("Props: file_count", () => {
afterEach(() => cleanup());

test("file_count='single' does not set multiple attribute on input", async () => {
const { getByTestId } = await render(UploadButton, {
...base_props,
file_count: "single"
});

const input = getByTestId("Upload-upload-button");
expect(input).not.toHaveAttribute("multiple");
});

test("file_count='multiple' sets multiple attribute on input", async () => {
const { getByTestId } = await render(UploadButton, {
...base_props,
file_count: "multiple"
});

const input = getByTestId("Upload-upload-button");
expect(input).toHaveAttribute("multiple");
});

test("file_count='directory' sets webkitdirectory attribute on input", async () => {
const { getByTestId } = await render(UploadButton, {
...base_props,
file_count: "directory"
});

const input = getByTestId("Upload-upload-button");
expect(input).toHaveAttribute("webkitdirectory");
});
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These tests assert on attributes (multiple, webkitdirectory) rather than user-visible behaviour. Per the frontend testing skill: "BAD: assert that an input has a step attribute set to 5. GOOD: type a value, click the increment button, and assert the value increased by 5."

file_count has an observable behavioural effect — the number of files retained after upload — so the test should drive that:

  • file_count='single' + upload_file of two files → get_data().value is a single FileData (not an array of length 2).
  • file_count='multiple' + multi-file upload → get_data().value is an array of length N.
  • file_count='directory' is the only one for which a behavioural test is genuinely difficult; the webkitdirectory/mozdirectory attribute is the user-visible mechanism. Keep an attribute assertion here, but add a one-line comment explaining why (the skill calls for a comment + rationale when going against the "test behaviour" principle).

Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +109 to +141
describe("Props: file_types", () => {
afterEach(() => cleanup());

test("file_types maps category names to wildcard MIME types", async () => {
const { getByTestId } = await render(UploadButton, {
...base_props,
file_types: ["image", "audio"]
});

const input = getByTestId("Upload-upload-button");
expect(input).toHaveAttribute("accept", "image/*, audio/*");
});

test("file_types passes dot-prefixed extensions through unchanged", async () => {
const { getByTestId } = await render(UploadButton, {
...base_props,
file_types: [".csv", ".json"]
});

const input = getByTestId("Upload-upload-button");
expect(input).toHaveAttribute("accept", ".csv, .json");
});

test("empty file_types array results in empty accept attribute", async () => {
const { getByTestId } = await render(UploadButton, {
...base_props,
file_types: []
});

const input = getByTestId("Upload-upload-button");
expect(input).toHaveAttribute("accept", "");
});
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same critique as file_count: these are attribute assertions. accept is closer to legitimate territory than multiple because it is the user-visible mechanism (it's what the OS file picker reads), but three near-identical string assertions is implementation-heavy.

Suggestions:

  • Keep one attribute test (since this is the mechanism), add a brief comment justifying it, and consider merging the three sub-tests into one parameterised test.
  • Add the missing file_types == null / undefined case — UploadButton.svelte:55 has if (file_types == null) return null; which is currently uncovered.
  • The "empty file_types array results in empty accept attribute" test asserts a quirk of the implementation (that [].join(", ") is "") rather than a meaningful user behaviour. Drop it or replace with the null-input case above.

Generated by Claude Code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In this case I wonder if we can attempt to upload a non accepted file and then check that it doesn't upload?

Claude is being a bit confusing above.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ok in order to test this I had to add some logic to validate the file types in js. Before we relied on the accept attribute of the input html component and upload_file helper would overwrite it. Now the UploadButton behaves similarly to how image/audio/video components work now.

Comment on lines +238 to +260
describe("get_data / set_data", () => {
afterEach(() => cleanup());

test("get_data returns null initially", async () => {
const { get_data } = await render(UploadButton, {
...base_props,
value: null
});

const data = await get_data();
expect(data.value).toBeNull();
});

test("get_data returns initial value when mounted with one", async () => {
const { get_data } = await render(UploadButton, {
...base_props,
value: TEST_TXT
});

const data = await get_data();
expect(data.value).toEqual(TEST_TXT);
});
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The skill mandates four checks for the data contract: DOM reflects set_data, get_data returns current state, set_dataget_data round-trip, user interaction flows back into get_data. Only the second of those is here. Please add:

test("set_data updates the value reported by get_data", async () => {
  const { set_data, get_data } = await render(UploadButton, base_props);
  await set_data({ value: TEST_TXT });
  expect((await get_data()).value).toEqual(TEST_TXT);
});

test("upload interaction is reflected in get_data", async () => {
  const { get_data } = await render(UploadButton, upload_props);
  await upload_file(TEST_TXT);
  await waitFor(() =>
    expect((await get_data()).value).not.toBeNull()
  );
});

The user-interaction round-trip is the most important one and is currently absent.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ok this involved fixing a bug in the UploadButton svelte component to get these two tests to pass. Thanks claude

Comment thread js/uploadbutton/UploadButton.test.ts Outdated
Comment on lines +217 to +220
client: {
upload: failing_upload,
stream: async () => ({ onmessage: null, close: () => {} })
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor: mock_client() already returns this exact stream shape (see js/tootils/src/render.ts:300-305). You can tighten this to:

client: { ...mock_client(), upload: failing_upload }

Avoids the duplicated stream boilerplate.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ty Claude

Comment on lines +195 to +209
test("upload and change events fire after file upload", async () => {
const { listen } = await render(UploadButton, {
...upload_props
});

const upload = listen("upload");
const change = listen("change");

await upload_file(TEST_TXT);

await waitFor(() => {
expect(upload).toHaveBeenCalledTimes(1);
});
expect(change).toHaveBeenCalledTimes(1);
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Missing coverage for max_file_size. It's a meaningful prop — UploadButton.svelte:86 passes it as the fourth argument to upload(). Suggested test (sits naturally next to the upload tests):

test("max_file_size is forwarded to client.upload", async () => {
  const upload_spy = vi.fn(async (file_data) => file_data);
  await render(UploadButton, {
    ...upload_props,
    max_file_size: 100,
    client: { ...mock_client(), upload: upload_spy }
  });
  await upload_file(TEST_TXT);
  await waitFor(() => expect(upload_spy).toHaveBeenCalled());
  expect(upload_spy).toHaveBeenCalledWith(
    expect.anything(), expect.anything(), expect.anything(), 100
  );
});

That's genuine behavioural coverage of the contract between the component and the client.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch claude

Comment thread js/uploadbutton/UploadButton.test.ts Outdated
}
});

expect(getByRole("img")).toBeInTheDocument();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: could also assert toHaveAttribute("src", "https://example.com/icon.png") here — src on <img> is the user-visible behaviour of the icon prop, not just an implementation flag, so it's worth pinning down.


Generated by Claude Code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tenuous but seems reasonable. Visual test would be better.

Comment thread js/uploadbutton/UploadButton.test.ts
Comment thread js/uploadbutton/UploadButton.test.ts
@pngwn
Copy link
Copy Markdown
Member

pngwn commented Apr 29, 2026

I have reviewed Claude's review. It is mostly reasonable but I've commented the comments to ignore.

In general looks good.

return type.endsWith("/*") && file_type.startsWith(category + "/");
})
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing "file/*" wildcard causes all files to be rejected

High Severity

The new is_valid_mimetype function doesn't handle "file/*" as a special "accept all" value. The Python API documents file_types=["file"] as meaning "any file allowed," and the accept_file_types derivation transforms "file" into "file/*". Since no real MIME type starts with "file/", the wildcard match file_type.startsWith("file/") always fails, causing every file to be rejected. The existing Upload.svelte correctly handles this with an early-return check for file_accept === "file/*".

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8b01269. Configure here.

@freddyaboulton freddyaboulton added v: patch A change that requires a patch release t: fix A change that implements a fix labels Apr 29, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4fb675e. Configure here.

return type.endsWith("/*") && file_type.startsWith(category + "/");
})
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated is_valid_mimetype across upload components

Low Severity

The newly added is_valid_mimetype function in UploadButton.svelte duplicates the same function already present in js/upload/src/Upload.svelte. Both implement the same core logic (splitting accept strings, checking extensions, matching wildcard MIME categories). If a bug is found or behavior needs to change, both copies need to be updated independently. This could be extracted into a shared utility in @gradio/upload or @gradio/client.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4fb675e. Configure here.

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

Labels

t: fix A change that implements a fix v: patch A change that requires a patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

uploadbutton: refactor unit tests

3 participants