| 382 |
382 |
|
//! that one page: audiofiles' library browser and goingson's filters are the
|
| 383 |
383 |
|
//! same shape.
|
| 384 |
384 |
|
//!
|
|
385 |
+ |
//! 0.31.0 finishes the file field. [`FieldKind::File`] arrived at 0.11.0
|
|
386 |
+ |
//! carrying neither an accepted-types list nor a multiplicity flag, and said so
|
|
387 |
+ |
//! in its own doc: `accept` appeared at zero sites in either app, and a member
|
|
388 |
+ |
//! added for a case nobody has is a member designed against nothing. That count
|
|
389 |
+ |
//! was taken over goingson and Balanced Breakfast, and the MNW server is a third
|
|
390 |
+ |
//! consumer with 14 `accept` lists across 10 templates and 4 of its 16 file
|
|
391 |
+ |
//! inputs marked `multiple`. The reasoning was right and the measurement went
|
|
392 |
+ |
//! stale, so [`Field::accept`] and [`Field::multiple`] arrive now.
|
|
393 |
+ |
//!
|
|
394 |
+ |
//! - [`Accepted`] is an enum rather than the comma-joined string the templates
|
|
395 |
+ |
//! hold, because the list is read twice and only one of the readings is
|
|
396 |
+ |
//! filtering. The other is which disclosure to offer — a preview, a duration,
|
|
397 |
+ |
//! a waveform — and a renderer deciding that from raw strings is three
|
|
398 |
+ |
//! renderers each writing a media-type parser. [`Accepted::family`] answers it
|
|
399 |
+ |
//! once. All three shapes are in the measured sites and none can be dropped:
|
|
400 |
+ |
//! `image/*` is a [`Family`], `image/jpeg` and `text/csv` are a
|
|
401 |
+ |
//! [`Type`](Accepted::Type), and `.zip`, `.tar.gz` and `.clap` are a
|
|
402 |
+ |
//! [`Suffix`](Accepted::Suffix). One site carries `.csv,text/csv`, which is
|
|
403 |
+ |
//! both in one list.
|
|
404 |
+ |
//! - A suffix carries no family and this crate will not infer one. `.mp3` is
|
|
405 |
+ |
//! audio in fact, and a table here saying so is a mapping that rots in a
|
|
406 |
+ |
//! published crate and is wrong for the first container format someone hands
|
|
407 |
+ |
//! it. A call site that wants the disclosure writes the family or the media
|
|
408 |
+ |
//! type, which is what the sites offering previews already do.
|
|
409 |
+ |
//! - There is one upload shape, not one per type. What a media upload shows
|
|
410 |
+ |
//! beyond a plain one is disclosure layered on this shape, which is why the
|
|
411 |
+ |
//! accept list is load-bearing beyond validation and why nothing here names a
|
|
412 |
+ |
//! media upload as its own kind.
|
|
413 |
+ |
//! - Progress is not a member and needs none. An upload in flight is a control
|
|
414 |
+ |
//! in flight with a number attached, so it is [`Awaiting`] with the file's
|
|
415 |
+ |
//! length as its [`amount`](Awaiting::amount) — the case that type's own doc
|
|
416 |
+ |
//! names. Where the bytes go is an address, and this crate holds none; that is
|
|
417 |
+ |
//! the router's [`Action`], which the field already points at when it writes
|
|
418 |
+ |
//! on its own.
|
|
419 |
+ |
//!
|
| 385 |
420 |
|
//! # Reach, focus and the focus ring
|
| 386 |
421 |
|
//!
|
| 387 |
422 |
|
//! Three terms, and no others, for what 0.19.0 moved out of the description.
|
| 3312 |
3347 |
|
/// [`Email`](Self::Email), which exists because it changes the keyboard,
|
| 3313 |
3348 |
|
/// than to anything bespoke.
|
| 3314 |
3349 |
|
///
|
| 3315 |
|
- |
/// It carries no accepted-types list and no multiple flag, and that is
|
| 3316 |
|
- |
/// measured rather than deferred: `accept` appears at zero sites in either
|
| 3317 |
|
- |
/// app. A member added for a case nobody has is a member designed against
|
| 3318 |
|
- |
/// nothing.
|
|
3350 |
+ |
/// # The four things an upload says, and where each of them lives
|
|
3351 |
+ |
///
|
|
3352 |
+ |
/// | axis | where |
|
|
3353 |
+ |
/// |---|---|
|
|
3354 |
+ |
/// | what it accepts | [`Field::accept`] |
|
|
3355 |
+ |
/// | one file or several | [`Field::multiple`] |
|
|
3356 |
+ |
/// | where the bytes go | the router's action, not here |
|
|
3357 |
+ |
/// | how far along it is | [`Awaiting`] on that action |
|
|
3358 |
+ |
///
|
|
3359 |
+ |
/// Only the first two are this crate's, and that split is the answer to
|
|
3360 |
+ |
/// "describe an upload in full" rather than a gap in it. A destination is an
|
|
3361 |
+ |
/// address and this crate holds no addresses; progress is a live number and
|
|
3362 |
+ |
/// a description is built once, so the number is the renderer's to observe
|
|
3363 |
+ |
/// against the size [`Awaiting::amount`] carried before the transfer began.
|
|
3364 |
+ |
///
|
|
3365 |
+ |
/// # How the file is handed over is the host's
|
|
3366 |
+ |
///
|
|
3367 |
+ |
/// A drop area, a button opening a native picker, a path typed at a prompt:
|
|
3368 |
+ |
/// all three are the same field, and every measured site has the first. It
|
|
3369 |
+ |
/// is not described for the reason no gesture is — this crate owns no
|
|
3370 |
+ |
/// coordinates and no pointer, and a terminal that cannot be dropped on
|
|
3371 |
+ |
/// would be refusing a description it can otherwise honour completely.
|
|
3372 |
+ |
///
|
|
3373 |
+ |
/// The first two were absent until 0.31.0, and the doc here said why: they
|
|
3374 |
+ |
/// were measured rather than deferred, `accept` appearing at zero sites in
|
|
3375 |
+ |
/// either app. The count was taken over goingson and Balanced Breakfast, and
|
|
3376 |
+ |
/// the MNW server is a third consumer with 14 of them. A member designed
|
|
3377 |
+ |
/// against nothing is still the rule; the measurement is what changed.
|
| 3319 |
3378 |
|
File,
|
| 3320 |
3379 |
|
/// Carried through the form and never shown.
|
| 3321 |
3380 |
|
Hidden,
|
| 3399 |
3458 |
|
pub const fn multiline(self) -> bool {
|
| 3400 |
3459 |
|
matches!(self, Self::Textarea | Self::Rich)
|
| 3401 |
3460 |
|
}
|
|
3461 |
+ |
|
|
3462 |
+ |
/// Whether the value is a file the host picks rather than a string typed
|
|
3463 |
+ |
/// into a box.
|
|
3464 |
+ |
///
|
|
3465 |
+ |
/// One member answers yes, which is [`visible`](Self::visible)'s and
|
|
3466 |
+ |
/// [`confidential`](Self::confidential)'s footing rather than a departure
|
|
3467 |
+ |
/// from it: the question gets a name because three renderers ask it before
|
|
3468 |
+ |
/// they can read [`Field::accept`] or [`Field::multiple`], and a `matches!`
|
|
3469 |
+ |
/// per renderer is where a second file-taking kind would go missing.
|
|
3470 |
+ |
#[must_use]
|
|
3471 |
+ |
pub const fn takes_files(self) -> bool {
|
|
3472 |
+ |
matches!(self, Self::File)
|
|
3473 |
+ |
}
|
|
3474 |
+ |
}
|
|
3475 |
+ |
|
|
3476 |
+ |
/// A family of media a file can belong to.
|
|
3477 |
+ |
///
|
|
3478 |
+ |
/// Three members, because three is what a media type's own first segment offers
|
|
3479 |
+ |
/// that a renderer can do anything with. `text` and `application` are families
|
|
3480 |
+ |
/// too and neither buys a disclosure — there is no preview of an
|
|
3481 |
+ |
/// `application/octet-stream` — so naming them would be a member added for a
|
|
3482 |
+ |
/// case nobody has.
|
|
3483 |
+ |
///
|
|
3484 |
+ |
/// It is the answer to "which disclosure", not a validation rule.
|
|
3485 |
+ |
/// [`Field::accept`] is what a host filters on.
|
|
3486 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
3487 |
+ |
#[non_exhaustive]
|
|
3488 |
+ |
pub enum Family {
|
|
3489 |
+ |
/// A still picture.
|
|
3490 |
+ |
Image,
|
|
3491 |
+ |
/// Sound.
|
|
3492 |
+ |
Audio,
|
|
3493 |
+ |
/// Moving pictures, with or without sound.
|
|
3494 |
+ |
Video,
|
|
3495 |
+ |
}
|
|
3496 |
+ |
|
|
3497 |
+ |
impl Family {
|
|
3498 |
+ |
/// The wildcard media type that means the whole family.
|
|
3499 |
+ |
///
|
|
3500 |
+ |
/// `image/*` and its two siblings, which is what the measured sites write
|
|
3501 |
+ |
/// and what a webview puts in an `accept` attribute. Named here so the three
|
|
3502 |
+ |
/// renderers do not each spell the star.
|
|
3503 |
+ |
#[must_use]
|
|
3504 |
+ |
pub const fn wildcard(self) -> &'static str {
|
|
3505 |
+ |
match self {
|
|
3506 |
+ |
Self::Image => "image/*",
|
|
3507 |
+ |
Self::Audio => "audio/*",
|
|
3508 |
+ |
Self::Video => "video/*",
|
|
3509 |
+ |
}
|
|
3510 |
+ |
}
|
|
3511 |
+ |
|
|
3512 |
+ |
/// The family a media type's first segment names, if it is one of these.
|
|
3513 |
+ |
///
|
|
3514 |
+ |
/// Case-insensitive on the segment, because a media type is
|
|
3515 |
+ |
/// case-insensitive and half the tree writes them lowercase by habit rather
|
|
3516 |
+ |
/// than by rule.
|
|
3517 |
+ |
#[must_use]
|
|
3518 |
+ |
pub fn of_type(media_type: &str) -> Option<Self> {
|
|
3519 |
+ |
let (top, _) = media_type.split_once('/')?;
|
|
3520 |
+ |
if top.eq_ignore_ascii_case("image") {
|
|
3521 |
+ |
Some(Self::Image)
|
|
3522 |
+ |
} else if top.eq_ignore_ascii_case("audio") {
|
|
3523 |
+ |
Some(Self::Audio)
|
|
3524 |
+ |
} else if top.eq_ignore_ascii_case("video") {
|
|
3525 |
+ |
Some(Self::Video)
|
|
3526 |
+ |
} else {
|
|
3527 |
+ |
None
|
|
3528 |
+ |
}
|
|
3529 |
+ |
}
|
|
3530 |
+ |
}
|
|
3531 |
+ |
|
|
3532 |
+ |
/// One entry in a file field's accept list.
|
|
3533 |
+ |
///
|
|
3534 |
+ |
/// Three shapes rather than a string, and all three are in the measured sites:
|
|
3535 |
+ |
/// the MNW server writes `image/*`, `image/jpeg,image/png,image/webp`,
|
|
3536 |
+ |
/// `.zip,.dmg,.exe,.appimage,.deb,.tar.gz,.clap,.vst3` and, in one place,
|
|
3537 |
+ |
/// `.csv,text/csv`. A single string would carry all of them and answer nothing
|
|
3538 |
+ |
/// about any of them.
|
|
3539 |
+ |
///
|
|
3540 |
+ |
/// # Why the list is not just a filter
|
|
3541 |
+ |
///
|
|
3542 |
+ |
/// It is read twice. Once to decide what the picker offers, which any of the
|
|
3543 |
+ |
/// three shapes serves, and once to decide **which disclosure** the field gets:
|
|
3544 |
+ |
/// a preview for a picture, a duration or a waveform for a sound. There is one
|
|
3545 |
+ |
/// upload shape and a media upload is that shape with more of it shown, so the
|
|
3546 |
+ |
/// accept list is what says which more. [`family`](Self::family) is that
|
|
3547 |
+ |
/// question answered once here instead of a media-type parser in each renderer.
|
|
3548 |
+ |
///
|
|
3549 |
+ |
/// # A suffix names no family, on purpose
|
|
3550 |
+ |
///
|
|
3551 |
+ |
/// `.mp3` is audio in fact, and nothing here says so. A suffix-to-family table
|
|
3552 |
+ |
/// in a published crate is a mapping that goes stale, disagrees with the host's
|
|
3553 |
+ |
/// own idea of what a file is, and is wrong the first time somebody hands it a
|
|
3554 |
+ |
/// container. A call site that wants a picture's preview writes
|
|
3555 |
+ |
/// [`Family::Image`] or `image/jpeg`; a call site listing installer suffixes
|
|
3556 |
+ |
/// wants no disclosure anyway, which is the measured case.
|
|
3557 |
+ |
///
|
|
3558 |
+ |
/// Added 0.31.0, `f7261a5a`.
|
|
3559 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
3560 |
+ |
#[non_exhaustive]
|
|
3561 |
+ |
pub enum Accepted<'a> {
|
|
3562 |
+ |
/// Every file of a family: `image/*` and its siblings.
|
|
3563 |
+ |
Family(Family),
|
|
3564 |
+ |
/// One media type, written the way a media type is written:
|
|
3565 |
+ |
/// `image/jpeg`, `text/csv`.
|
|
3566 |
+ |
Type(&'a str),
|
|
3567 |
+ |
/// One file-name suffix, written with its leading dot: `.zip`, `.tar.gz`.
|
|
3568 |
+ |
///
|
|
3569 |
+ |
/// A suffix and not an extension, because `.tar.gz` is a measured site and
|
|
3570 |
+ |
/// is two dots.
|
|
3571 |
+ |
Suffix(&'a str),
|
|
3572 |
+ |
}
|
|
3573 |
+ |
|
|
3574 |
+ |
impl<'a> Accepted<'a> {
|
|
3575 |
+ |
/// The family this entry belongs to, when it names one.
|
|
3576 |
+ |
///
|
|
3577 |
+ |
/// [`None`] for a [`Suffix`](Self::Suffix) and for any media type outside
|
|
3578 |
+ |
/// the three families, which is the honest answer rather than a missing
|
|
3579 |
+ |
/// one: the description did not say.
|
|
3580 |
+ |
#[must_use]
|
|
3581 |
+ |
pub fn family(self) -> Option<Family> {
|
|
3582 |
+ |
match self {
|
|
3583 |
+ |
Self::Family(family) => Some(family),
|
|
3584 |
+ |
Self::Type(media_type) => Family::of_type(media_type),
|
|
3585 |
+ |
Self::Suffix(_) => None,
|
|
3586 |
+ |
}
|
|
3587 |
+ |
}
|
|
3588 |
+ |
|
|
3589 |
+ |
/// How a host that wants one string writes this entry.
|
|
3590 |
+ |
///
|
|
3591 |
+ |
/// A webview's `accept` attribute takes exactly these spellings, and a
|
|
3592 |
+ |
/// terminal listing what it will take reads the same words.
|
|
3593 |
+ |
#[must_use]
|
|
3594 |
+ |
pub const fn as_str(self) -> &'a str {
|
|
3595 |
+ |
match self {
|
|
3596 |
+ |
Self::Family(family) => family.wildcard(),
|
|
3597 |
+ |
Self::Type(text) | Self::Suffix(text) => text,
|
|
3598 |
+ |
}
|
|
3599 |
+ |
}
|
| 3402 |
3600 |
|
}
|
| 3403 |
3601 |
|
|
| 3404 |
3602 |
|
/// One option offered by a field [`FieldKind::offers_options`] accepts.
|
| 3564 |
3762 |
|
/// Which option is *current* is not here. That is the value, and the value
|
| 3565 |
3763 |
|
/// is renderer state.
|
| 3566 |
3764 |
|
pub options: &'a [Choice<'a>],
|
|
3765 |
+ |
/// What a file field takes, in the order a host offering the list shows it.
|
|
3766 |
+ |
///
|
|
3767 |
+ |
/// Empty for every kind [`FieldKind::takes_files`] rejects, and empty is
|
|
3768 |
+ |
/// also a real answer for one that accepts it: a field that takes any file
|
|
3769 |
+ |
/// says so by listing nothing, which is what an `<input type="file">` with
|
|
3770 |
+ |
/// no `accept` does and what most of the measured sites are.
|
|
3771 |
+ |
///
|
|
3772 |
+ |
/// It is a filter and it is the disclosure cue, and [`Accepted`]'s doc
|
|
3773 |
+ |
/// carries which reading is which. Nothing here validates: a host may hand
|
|
3774 |
+ |
/// back a file the list does not cover, exactly as a browser does when the
|
|
3775 |
+ |
/// user switches the picker to "All Files", and deciding a value is wrong
|
|
3776 |
+ |
/// stays with whoever validated.
|
|
3777 |
+ |
///
|
|
3778 |
+ |
/// Added 0.31.0, `f7261a5a`.
|
|
3779 |
+ |
pub accept: &'a [Accepted<'a>],
|
|
3780 |
+ |
/// Whether more than one file may be picked at once.
|
|
3781 |
+ |
///
|
|
3782 |
+ |
/// Only [`FieldKind::takes_files`] reads it. A multi-valued answer to any
|
|
3783 |
+ |
/// other question is a different shape — a set of options, a repeated
|
|
3784 |
+ |
/// group — and neither is this flag with a different kind beside it.
|
|
3785 |
+ |
///
|
|
3786 |
+ |
/// False is the common case: 4 of the MNW server's 16 file inputs carry it.
|
|
3787 |
+ |
///
|
|
3788 |
+ |
/// Added 0.31.0, `f7261a5a`.
|
|
3789 |
+ |
pub multiple: bool,
|
| 3567 |
3790 |
|
/// Whether the form refuses to submit without it.
|
| 3568 |
3791 |
|
pub required: bool,
|
| 3569 |
3792 |
|
/// The longest the value may be, in characters.
|
| 3615 |
3838 |
|
error: None,
|
| 3616 |
3839 |
|
placeholder: None,
|
| 3617 |
3840 |
|
options: &[],
|
|
3841 |
+ |
accept: &[],
|
|
3842 |
+ |
multiple: false,
|
| 3618 |
3843 |
|
required: false,
|
| 3619 |
3844 |
|
max_length: None,
|
| 3620 |
3845 |
|
min: None,
|
| 3644 |
3869 |
|
}
|
| 3645 |
3870 |
|
}
|
| 3646 |
3871 |
|
|
|
3872 |
+ |
/// A file field, taking the given accept list.
|
|
3873 |
+ |
///
|
|
3874 |
+ |
/// The fourth under-described kind and it gets a constructor for
|
|
3875 |
+ |
/// [`range`](Self::range)'s reason rather than [`select`](Self::select)'s:
|
|
3876 |
+ |
/// a file field with no accept list is not broken, it is a field that takes
|
|
3877 |
+ |
/// anything, and the hazard is the opposite one. A call site that meant to
|
|
3878 |
+ |
/// restrict and forgot has a picker offering every file on the machine and
|
|
3879 |
+ |
/// a server refusing the upload afterwards, which is the failure the list
|
|
3880 |
+ |
/// exists to move forward. Taking it as an argument is what makes an
|
|
3881 |
+ |
/// accidental omission a deliberate `&[]`.
|
|
3882 |
+ |
///
|
|
3883 |
+ |
/// [`multiple`](Self::multiple) stays a field. One file is the common case
|
|
3884 |
+ |
/// and the honest default; several is the thing worth saying.
|
|
3885 |
+ |
#[must_use]
|
|
3886 |
+ |
pub const fn upload(name: &'a str, label: &'a str, accept: &'a [Accepted<'a>]) -> Self {
|
|
3887 |
+ |
Self {
|
|
3888 |
+ |
accept,
|
|
3889 |
+ |
..Self::new(FieldKind::File, name, label)
|
|
3890 |
+ |
}
|
|
3891 |
+ |
}
|
|
3892 |
+ |
|
| 3647 |
3893 |
|
/// A select offering the given options.
|
| 3648 |
3894 |
|
///
|
| 3649 |
3895 |
|
/// One of the two kinds under-described by [`Field::new`], so it gets a
|
| 3711 |
3957 |
|
pub const fn bounded(&self) -> bool {
|
| 3712 |
3958 |
|
self.min.is_some() && self.max.is_some()
|
| 3713 |
3959 |
|
}
|
|
3960 |
+ |
|
|
3961 |
+ |
/// Whether anything in [`accept`](Self::accept) names a media family.
|
|
3962 |
+ |
///
|
|
3963 |
+ |
/// The question a renderer asks before it decides to keep room for a
|
|
3964 |
+ |
/// preview, and it is deliberately the *whole list* rather than one entry:
|
|
3965 |
+ |
/// the media dropzone this was measured against takes `image/*,video/*`, so
|
|
3966 |
+ |
/// there is no single family to return and there is still a disclosure to
|
|
3967 |
+ |
/// offer. Which one it turns out to be is known once a file is picked, which
|
|
3968 |
+ |
/// is renderer-side and after the description is gone.
|
|
3969 |
+ |
///
|
|
3970 |
+ |
/// False for an empty list, for a list of suffixes, and for `text/csv`. A
|
|
3971 |
+ |
/// renderer that wants the family of a particular entry reads
|
|
3972 |
+ |
/// [`Accepted::family`].
|
|
3973 |
+ |
#[must_use]
|
|
3974 |
+ |
pub fn accepts_media(&self) -> bool {
|
|
3975 |
+ |
self.accept.iter().any(|one| one.family().is_some())
|
|
3976 |
+ |
}
|
| 3714 |
3977 |
|
}
|
| 3715 |
3978 |
|
|
| 3716 |
3979 |
|
/// How much room a placement asks for.
|
| 4453 |
4716 |
|
assert_eq!(FacetValue::of("Ambient").counted(0).count, Some(0));
|
| 4454 |
4717 |
|
}
|
| 4455 |
4718 |
|
|
|
4719 |
+ |
#[test]
|
|
4720 |
+ |
fn one_kind_takes_files_and_the_two_file_members_are_its_alone() {
|
|
4721 |
+ |
assert!(FieldKind::File.takes_files());
|
|
4722 |
+ |
for kind in [
|
|
4723 |
+ |
FieldKind::Text,
|
|
4724 |
+ |
FieldKind::Textarea,
|
|
4725 |
+ |
FieldKind::Rich,
|
|
4726 |
+ |
FieldKind::Select,
|
|
4727 |
+ |
FieldKind::Checkbox,
|
|
4728 |
+ |
FieldKind::Hidden,
|
|
4729 |
+ |
] {
|
|
4730 |
+ |
assert!(!kind.takes_files());
|
|
4731 |
+ |
}
|
|
4732 |
+ |
// The default is a field that takes any one file, which is what an
|
|
4733 |
+ |
// input with no accept and no multiple already is.
|
|
4734 |
+ |
let plain = Field::new(FieldKind::File, "cover", "Cover");
|
|
4735 |
+ |
assert!(plain.accept.is_empty());
|
|
4736 |
+ |
assert!(!plain.multiple);
|
|
4737 |
+ |
}
|
|
4738 |
+ |
|
|
4739 |
+ |
#[test]
|
|
4740 |
+ |
fn an_accept_list_says_which_disclosure_and_a_suffix_says_none() {
|
|
4741 |
+ |
// The three shapes are the MNW server's own three, and the family is
|
|
4742 |
+ |
// the question a renderer asks before it keeps room for a preview.
|
|
4743 |
+ |
assert_eq!(
|
|
4744 |
+ |
Accepted::Family(Family::Image).family(),
|
|
4745 |
+ |
Some(Family::Image)
|
|
4746 |
+ |
);
|
|
4747 |
+ |
assert_eq!(Accepted::Type("image/jpeg").family(), Some(Family::Image));
|
|
4748 |
+ |
assert_eq!(Accepted::Type("audio/flac").family(), Some(Family::Audio));
|
|
4749 |
+ |
assert_eq!(
|
|
4750 |
+ |
Accepted::Type("video/quicktime").family(),
|
|
4751 |
+ |
Some(Family::Video)
|
|
4752 |
+ |
);
|
|
4753 |
+ |
// A media type outside the three families names none, and neither does
|
|
4754 |
+ |
// a suffix. `.mp3` is audio in fact and this crate will not infer it:
|
|
4755 |
+ |
// the table that said so would rot.
|
|
4756 |
+ |
assert_eq!(Accepted::Type("text/csv").family(), None);
|
|
4757 |
+ |
assert_eq!(Accepted::Suffix(".mp3").family(), None);
|
|
4758 |
+ |
assert_eq!(Accepted::Suffix(".tar.gz").family(), None);
|
|
4759 |
+ |
// Media types are case-insensitive and half the tree writes them
|
|
4760 |
+ |
// lowercase by habit rather than by rule.
|
|
4761 |
+ |
assert_eq!(Accepted::Type("IMAGE/PNG").family(), Some(Family::Image));
|
|
4762 |
+ |
}
|
|
4763 |
+ |
|
|
4764 |
+ |
#[test]
|
|
4765 |
+ |
fn every_accepted_entry_has_one_spelling_a_host_can_write() {
|
|
4766 |
+ |
assert_eq!(Accepted::Family(Family::Image).as_str(), "image/*");
|
|
4767 |
+ |
assert_eq!(Accepted::Family(Family::Audio).as_str(), "audio/*");
|
|
4768 |
+ |
assert_eq!(Accepted::Family(Family::Video).as_str(), "video/*");
|
|
4769 |
+ |
assert_eq!(Accepted::Type("text/csv").as_str(), "text/csv");
|
|
4770 |
+ |
assert_eq!(Accepted::Suffix(".tar.gz").as_str(), ".tar.gz");
|
|
4771 |
+ |
}
|
|
4772 |
+ |
|
|
4773 |
+ |
#[test]
|
|
4774 |
+ |
fn a_list_accepting_two_families_still_has_a_disclosure_to_offer() {
|
|
4775 |
+ |
// The measured dropzone: `accept="image/*,video/*"`. There is no single
|
|
4776 |
+ |
// family to return and there is still a preview to keep room for, which
|
|
4777 |
+ |
// is why the question is asked of the list rather than of one entry.
|
|
4778 |
+ |
const MEDIA: &[Accepted<'_>] = &[
|
|
4779 |
+ |
Accepted::Family(Family::Image),
|
|
4780 |
+ |
Accepted::Family(Family::Video),
|
|
4781 |
+ |
];
|
|
4782 |
+ |
assert!(Field::upload("media", "Media", MEDIA).accepts_media());
|
|
4783 |
+ |
// An installer's suffix list wants no disclosure, which is the measured
|
|
4784 |
+ |
// case rather than a hypothetical one.
|
|
4785 |
+ |
const BUILDS: &[Accepted<'_>] = &[Accepted::Suffix(".zip"), Accepted::Suffix(".dmg")];
|
|
4786 |
+ |
assert!(!Field::upload("build", "Build", BUILDS).accepts_media());
|
|
4787 |
+ |
// And a field that takes anything says so by listing nothing.
|
|
4788 |
+ |
assert!(!Field::upload("any", "File", &[]).accepts_media());
|
|
4789 |
+ |
}
|
|
4790 |
+ |
|
|
4791 |
+ |
#[test]
|
|
4792 |
+ |
fn an_upload_carries_its_list_and_takes_one_file_until_it_says_otherwise() {
|
|
4793 |
+ |
const IMAGES: &[Accepted<'_>] = &[
|
|
4794 |
+ |
Accepted::Type("image/jpeg"),
|
|
4795 |
+ |
Accepted::Type("image/png"),
|
|
4796 |
+ |
Accepted::Type("image/webp"),
|
|
4797 |
+ |
];
|
|
4798 |
+ |
let avatar = Field::upload("avatar", "Avatar", IMAGES);
|
|
4799 |
+ |
assert_eq!(avatar.kind, FieldKind::File);
|
|
4800 |
+ |
assert_eq!(avatar.accept, IMAGES);
|
|
4801 |
+ |
assert!(!avatar.multiple);
|
|
4802 |
+ |
let several = Field {
|
|
4803 |
+ |
multiple: true,
|
|
4804 |
+ |
..avatar
|
|
4805 |
+ |
};
|
|
4806 |
+ |
assert!(several.multiple);
|
|
4807 |
+ |
}
|
|
4808 |
+ |
|
| 4456 |
4809 |
|
#[test]
|
| 4457 |
4810 |
|
fn the_four_readiness_states_are_one_axis_and_only_one_shows_content() {
|
| 4458 |
4811 |
|
// Mutually exclusive is the test for one enum against several fields: a
|