Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(upload): accept type should be more lenient #1064

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 22 additions & 9 deletions src/utility/upload.ts
Expand Up @@ -27,12 +27,17 @@ export async function upload(
}
if (isDisabled(element)) return

const files = (Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles])
const selectedFiles = Array.isArray(fileOrFiles) ? fileOrFiles : [fileOrFiles]
const files = selectedFiles
.filter(
file => !this.config.applyAccept || isAcceptableFile(file, input.accept),
)
.slice(0, input.multiple ? undefined : 1)

if (selectedFiles.length > 0 && files.length === 0) {
throw new Error('No files were accepted by the `accept` attribute')
}

const fileDialog = () => {
// do not fire an input event if the file selection does not change
if (
Expand All @@ -54,20 +59,28 @@ export async function upload(
input.removeEventListener('fileDialog', fileDialog)
}

// When matching files, browsers ignore case and consider jpeg/jpg interchangeable.
function normalize(nameOrType: string) {
return nameOrType.toLowerCase().replace(/(\.|\/)jpg\b/g, '$1jpeg')

Choose a reason for hiding this comment

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

yes came here to complain about them being case sensitive :D

}

function isAcceptableFile(file: File, accept: string) {
if (!accept) {
return true
}

const wildcards = ['audio/*', 'image/*', 'video/*']

return accept.split(',').some(acceptToken => {
if (acceptToken.startsWith('.')) {
return normalize(accept)
.trim()
.split(/\s*,\s*/)
.some(acceptToken => {
// tokens starting with a dot represent a file extension
return file.name.endsWith(acceptToken)
} else if (wildcards.includes(acceptToken)) {
return file.type.startsWith(acceptToken.substr(0, acceptToken.length - 1))
}
return file.type === acceptToken
})
if (acceptToken.startsWith('.')) {
return normalize(file.name).endsWith(acceptToken)
} else if (wildcards.includes(acceptToken)) {
return normalize(file.type).startsWith(acceptToken.replace('*', ''))
}
return normalize(file.type) === acceptToken
})
}
2 changes: 1 addition & 1 deletion src/utils/misc/isElementType.ts
Expand Up @@ -2,7 +2,7 @@ type tag = keyof HTMLElementTagNameMap

export function isElementType<
T extends tag,
P extends {[k: string]: unknown} | undefined = undefined
P extends {[k: string]: unknown} | undefined = undefined,
>(
element: Element,
tag: T | T[],
Expand Down
4 changes: 2 additions & 2 deletions tests/_helpers/listeners.ts
Expand Up @@ -139,10 +139,10 @@ export function addListeners(
}
}

function hasProperty<T extends {}, K extends PropertyKey>(
function hasProperty<T extends {}, K extends PropertyKey, V = string>(
obj: T,
prop: K,
): obj is T & {[k in K]: unknown} {
): obj is T & {[k in K]: V} {
return prop in obj
}

Expand Down
82 changes: 71 additions & 11 deletions tests/utility/upload.ts
Expand Up @@ -151,19 +151,62 @@ test('do nothing when element is disabled', async () => {
})

test.each([
[true, 'video/*,audio/*', 2],
[true, '.png', 1],
[true, 'text/csv', 1],
[true, '', 4],
[false, 'video/*', 4],
[true, 'video/*,audio/*', ['audio.mp3', 'mp3.jpg', 'video.mp4']],
[
true,
'image/png, image/gif, image/jpeg',
['image.png', 'image2.PNG', 'image.jpeg', 'image.jpg'],
],
[
true,
`image/jpeg,
image/png, image/gif`,
['image.png', 'image2.PNG', 'image.jpeg', 'image.jpg'],
],
[true, 'image/JPG', ['image.jpeg', 'image.jpg']],
[true, '.JPEG', ['image.jpeg', 'image.jpg', 'mp3.jpg']],
[true, '.png', ['image.png', 'image2.PNG']],
[true, 'text/csv', ['file.csv']],
[
true,
'',
[
'image.png',
'image2.PNG',
'image.jpeg',
'image.jpg',
'audio.mp3',
'mp3.jpg',
'file.csv',
'video.mp4',
],
],
[
false,
'video/*',
[
'image.png',
'image2.PNG',
'image.jpeg',
'image.jpg',
'audio.mp3',
'mp3.jpg',
'file.csv',
'video.mp4',
],
],
])(
'filter according to accept attribute applyAccept=%s, acceptAttribute=%s',
async (applyAccept, acceptAttribute, expectedLength) => {
async (applyAccept, acceptAttribute, expectedFileNames) => {
const files = [
new File(['hello'], 'hello.png', {type: 'image/png'}),
new File(['there'], 'there.jpg', {type: 'audio/mp3'}),
new File(['there'], 'there.csv', {type: 'text/csv'}),
new File(['there'], 'there.jpg', {type: 'video/mp4'}),
new File(['hello'], 'image.png', {type: 'image/png'}),

Choose a reason for hiding this comment

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

put in some image.PNG for good measure

new File(['hello'], 'image2.PNG', {type: 'image/png'}),
new File(['hello'], 'image.jpeg', {type: 'image/jpeg'}),
new File(['hello'], 'image.jpg', {type: 'image/jpeg'}),
new File(['hello'], 'audio.mp3', {type: 'audio/mp3'}),
new File(['hello'], 'mp3.jpg', {type: 'audio/mp3'}),
new File(['hello'], 'file.csv', {type: 'text/csv'}),
new File(['hello'], 'video.mp4', {type: 'video/mp4'}),
]
const {element, user} = setup<HTMLInputElement>(
`
Expand All @@ -176,8 +219,25 @@ test.each([
)

await user.upload(element, files)
expect(
Array.from(element.files as FileList).map(item => item.name),
).toEqual(expectedFileNames)
},
)

expect(element.files).toHaveLength(expectedLength)
test.each([true, false])(
'throw if no files are accepted, multiple=%s',
async multiple => {
const files = [
new File(['hello'], 'hello.png', {type: 'image/png'}),
new File(['hello'], 'hello.jpeg', {type: 'image/jpg'}),
]
const {element, user} = setup<HTMLInputElement>(
`<input type="file" accept="video/*" ${multiple ? 'multiple' : ''} />`,
)
await expect(async () => {
await user.upload(element, multiple ? files : files[0])
}).rejects.toThrowError('No files were accepted by the `accept` attribute')
},
)

Expand Down