Skip to content

feat: enforce Upload client-side constraints in UploadTester - #198

Open
totally-not-ai[bot] wants to merge 2 commits into
mainfrom
issues/180-upload-client-side-constraints
Open

feat: enforce Upload client-side constraints in UploadTester#198
totally-not-ai[bot] wants to merge 2 commits into
mainfrom
issues/180-upload-client-side-constraints

Conversation

@totally-not-ai

@totally-not-ai totally-not-ai Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

UploadTester now applies the same checks the vaadin-upload web component does before it sends a file, so maxFiles, maxFileSize and the accepted file types are respected in browserless tests. A file that fails a check is not passed to the upload handler or receiver; a FileRejectedEvent is fired instead, just like in a browser.

Fixes #180

What changed

  • Files go through the gate in the browser order: maxFiles, then maxFileSize, then the accept pattern. The first failing check rejects the file.
  • The rejection message comes from the component UploadI18N when it is set, and from the web component default otherwise (Too Many Files., File is Too Big., Incorrect File Type.).
  • maxFiles is compared against an emulated client file list kept on the component, so files from earlier calls still count. Pending Upload#clearFileList() calls are picked up on the next interaction.
  • New UploadTester#removeFile simulates the user removing an entry: it fires FileRemovedEvent and frees a slot. An aborted upload drops its file the way the browser does; a failed upload keeps it.
  • All three accept APIs (setAcceptedFileTypes, setAcceptedMimeTypes, setAcceptedFileExtensions) end up in the same accept element property, so one pattern derived from that property covers them all.
  • Limits are read from the element property, not from the getter value being greater than zero. The getters report an unset limit as zero, so this is the only way to tell "not set" from setMaxFiles(0), which now rejects every file as it does in a browser.
  • Behaviour change: exceeding maxFiles no longer throws IllegalStateException from uploadAll; the extra files are rejected instead.

Use case

An app lets users attach up to two documents, at most 1 MB each, and shows a notification when a file is refused. The developer wants a test that proves the FileRejectedEvent branch actually runs, which was impossible before because the tester ignored these limits.

@Test
void tooManyFiles_userIsNotified() {
    UploadView view = navigate(UploadView.class);
    UploadTester<Upload> upload = test(view.attachments);

    // view.attachments: setMaxFiles(2), setMaxFileSize(1024 * 1024)
    upload.uploadAll(contract, appendix, extra);

    // the third file never reaches the handler, the app shows its message
    Assertions.assertEquals("Too Many Files.", view.lastRejectionMessage);

    // the user removes one entry, so a new file fits again
    upload.removeFile(contract);
    upload.upload(extra);
    Assertions.assertTrue(view.savedFiles.contains(extra.getName()));
}

API Changes

com.vaadin.flow.component.upload.UploadTester

// Added
public void removeFile(String fileName) // simulates the user removing a file list entry; fires FileRemovedEvent and frees a maxFiles slot
public void removeFile(File file)

Test summary

# Status What the test verifies Why it matters
1 A batch upload beyond maxFiles rejects only the extra file; the ones that fit are received Core limit; a wrong split would silently drop or accept files
2 Files uploaded in separate calls still count towards maxFiles Proves the emulated file list persists between calls, as in the browser
3 setMaxFiles(0) rejects every file Distinguishes an explicit zero limit from an unset limit
4 A file over maxFileSize is rejected and AllFinished is not fired; a file of exactly the limit is accepted Off-by-one at the limit, and no bogus completion event
5 The accept pattern matches by mime wildcard and by file name extension; anything else is rejected One pattern must cover all three accept APIs
6 The rejection message comes from UploadI18N when set, and from the web component default otherwise Apps assert on this message
7 removeFile fires FileRemovedEvent and frees a slot The documented way to make room again
8 removeFile on a file that is not in the list throws IllegalArgumentException; on an unusable component throws IllegalStateException Clear failure instead of a silent no-op
9 Repeated Upload#clearFileList() calls each free the slots Only counting the first call would wrongly reject later files
10 An aborted upload drops its file from the list; a failed upload keeps it Matches browser behaviour; decides whether a slot is free
11 A file that passes the client gate but fails Flow's server-side accepted type check is not received, and no FileRejectedEvent is fired The two checks are separate; the laxer client rule must not hide the server one
  • UploadTesterTest.uploadAll_fileCountExceeded_extraFilesRejected → 1
  • UploadTesterDeprecatedAPITest.uploadAll_fileCountExceeded_extraFilesRejected → 1 (receiver/legacy API path)
  • UploadTesterTest.upload_fileCountExceededOverSeparateUploads_extraFilesRejected → 2
  • UploadTesterTest.upload_maxFilesSetToZero_everyFileRejected → 3
  • UploadTesterTest.upload_exceedsMaxFileSize_rejected → 4
  • UploadTesterTest.upload_acceptedFileTypes_onlyMatchingFilesAccepted → 5
  • UploadTesterTest.upload_acceptedFileExtensions_disallowedExtensionRejected, UploadTesterTest.upload_acceptedMimeTypes_disallowedTypeRejected → 5, 6 (default message)
  • UploadTesterTest.upload_customI18n_rejectionUsesConfiguredMessage → 6
  • UploadTesterTest.removeFile_fileRemovedNotifiedAndSlotFreed → 7
  • UploadTesterTest.removeFile_fileNotInFileList_throws, UploadTesterTest.upload_componentNotUsable_throws → 8
  • UploadTesterTest.clearFileList_slotsFreed → 9
  • UploadTesterTest.uploadAborted_fileRemovedFromFileList, UploadTesterTest.uploadFailed_fileKeptInFileList → 10
  • UploadTesterTest.upload_acceptedByClientButNotByServer_notReceived → 11

Left untested on purpose: the exact regex escaping of every possible accept token, since it is derived from the same property for all three accept APIs and is covered through them; and the removeFile(File) overload, which only delegates to removeFile(String).

UploadTester entered at the transport layer, so everything the
vaadin-upload web component checks in _addFile() before a request is
ever made had no counterpart on the server. maxFileSize and the
accepted file types were ignored outright, and maxFiles was never
compared against the file list, so a test could prove an upload path
works with input the user could never submit and the FileRejectedEvent
branch of an application was unreachable.

Files now go through the same gate as in the browser, in the same
order: maxFiles, then maxFileSize, then the accept pattern. A rejected
file is not delivered to the upload handler or receiver; a
FileRejectedEvent is fired instead, carrying the message from the
component UploadI18N when set and the web component default otherwise.

Since maxFiles is compared against the client file list, the tester
emulates that list on the component. UploadTester#removeFile simulates
the user removing an entry, firing FileRemovedEvent and freeing a slot,
an aborted upload drops its file as the browser does, and pending
Upload#clearFileList() calls are picked up on the next interaction.

The three accept APIs all converge on the accept element property, so
one pattern derived from it covers setAcceptedFileTypes,
setAcceptedMimeTypes and setAcceptedFileExtensions alike.

Fixes #180
maxFiles and maxFileSize were gated on the value being greater than
zero, which cannot tell an unset limit from setMaxFiles(0). The
client-side default is Infinity while the Upload getters report an
unset limit as zero, so the element property now decides whether the
limit applies at all, leaving setMaxFiles(0) rejecting every file as it
does in a browser.

Also cover the paths the client-side gate now sits in front of: a file
accepted by the accept pattern but refused by Flow's server-side
accepted type validation, repeated clearFileList() calls, and
removeFile() on a component that is not usable. The class javadoc now
spells out the emulated file list and how the client-side accepted type
check relates to the server-side one.
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.

UploadTester ignores maxFileSize, maxFiles and accepted file types

0 participants