Skip to content

Commit

Permalink
More tests (#51)
Browse files Browse the repository at this point in the history
* bump pre-commit/mirrors-eslint to fix false-pos undefined vars from {@const} destructuring

* document all props in readme (removeBtnTitle, removeAllTitle, defaultDisabledTitle were missing)

* test all props are mentioned in readme

* add CSS resets for margin + padding of input + button

* add test for remove all button (probably brittle), need to figure out how to isolate test each into its own DOM
  • Loading branch information
janosh authored Feb 27, 2022
1 parent 32b1c9f commit 33864be
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ repos:
exclude: yarn.lock

- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.6.0
rev: v8.10.0
hooks:
- id: eslint
types: [file]
Expand Down
47 changes: 25 additions & 22 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ yarn add -D svelte-multiselect
`Spring`,
]
let selected
let selected = []
</script>
Favorite Web Frameworks?
Expand All @@ -92,26 +92,29 @@ Full list of props/bindable variables for this component:
<div class="table">

<!-- prettier-ignore -->
| name | default | description |
| :----------------- | :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. |
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
| `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. |
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
| `selectedLabels` | `[]` | Labels of currently selected options. |
| `selectedValues` | `[]` | Values of currently selected options. |
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
| `input` | `undefined` | Handle to the `<input>` DOM node. |
| `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
| `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>`. |
| `required` | `false` | Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected. |
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
| `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
| `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
| name | default | description |
| :--------------------- | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `options` | required prop | Array of strings/numbers or `Option` objects that will be listed in the dropdown. See `src/lib/index.ts` for admissible fields. The `label` is the only mandatory one. It must also be unique. |
| `showOptions` | `false` | Bindable boolean that controls whether the options dropdown is visible. |
| `searchText` | `` | Text the user-entered to filter down on the list of options. Binds both ways, i.e. can also be used to set the input text. |
| `activeOption` | `null` | Currently active option, i.e. the one the user currently hovers or navigated to with arrow keys. |
| `maxSelect` | `null` | Positive integer to limit the number of options users can pick. `null` means no limit. |
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
| `selectedLabels` | `[]` | Labels of currently selected options. |
| `selectedValues` | `[]` | Values of currently selected options. |
| `noOptionsMsg` | `'No matching options'` | What message to show if no options match the user-entered search string. |
| `readonly` | `false` | Disable the component. It will still be rendered but users won't be able to interact with it. |
| `placeholder` | `undefined` | String shown in the text input when no option is selected. |
| `input` | `undefined` | Handle to the `<input>` DOM node. |
| `id` | `undefined` | Applied to the `<input>` element for associating HTML form `<label>`s with this component for accessibility. Also, clicking a `<label>` with same `for` attribute as `id` will focus this component. |
| `name` | `id` | Applied to the `<input>` element. If not provided, will be set to the value of `id`. Sets the key of this field in a submitted form data object. Not useful at the moment since the value is stored in Svelte state, not on the `<input>`. |
| `required` | `false` | Whether forms can be submitted without selecting any options. Aborts submission, is scrolled into view and shows help "Please fill out" message when true and user tries to submit with no options selected. |
| `autoScroll` | `true` | `false` disables keeping the active dropdown items in view when going up/down the list of options with arrow keys. |
| `allowUserOptions` | `false` | Whether users are allowed to enter values not in the dropdown list. `true` means add user-defined options to the selected list only, `'append'` means add to both options and selected. |
| `loading` | `false` | Whether the component should display a spinner to indicate it's in loading state. Use `<slot name='spinner'>` to specify a custom spinner. |
| `removeBtnTitle` | `'Remove'` | Title text to display when user hovers over button (cross icon) to remove selected option. |
| `removeAllTitle` | `'Remove all'` | Title text to display when user hovers over remove-all button. |
| `defaultDisabledTitle` | `'This option is disabled'` | Title text to display when user hovers over a disabled option. Each option can override this through its `disabledTitle` attribute. button. |

</div>

Expand Down Expand Up @@ -238,7 +241,7 @@ If you only want to make small adjustments, you can pass the following CSS varia
- `div.multiselect > ul.selected > li > input`
- `color: var(--sms-text-color, inherit)`: Input text color.
- `div.multiselect > ul.selected > li`
- `background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue))`: Background of selected options.
- `background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15))`: Background of selected options.
- `height: var(--sms-selected-li-height)`: Height of selected options.
- `ul.selected > li button:hover, button.remove-all:hover, button:focus`
- `color: var(--sms-remove-x-hover-focus-color, lightskyblue)`: Color of the cross-icon buttons for removing all or individual selected options when in `:focus` or `:hover` state.
Expand Down
3 changes: 0 additions & 3 deletions src/components/Example.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
let activeWeb: Option
let selectedML: Option[]
// used to show user hint to try arrow keys but only once
let neverActive = true
let showConfetti = false
$: if (activeWeb) neverActive = false
const placeholder = `Take your pick...`
const filterFunc = (op: Option, searchText: string) => {
Expand Down
11 changes: 8 additions & 3 deletions src/lib/MultiSelect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
export let removeBtnTitle = `Remove`
export let removeAllTitle = `Remove all`
// https://github.com/sveltejs/svelte/issues/6964
export let defaultDisabledTitle = `This option is disabled`
export let allowUserOptions: boolean | 'append' = false
export let autoScroll = true
Expand All @@ -50,7 +49,7 @@
if (!Array.isArray(selected)) console.error(`selected prop must be an array`)
onMount(() => {
selected = _options.filter((op) => op?.preselected)
selected = _options.filter((op) => op?.preselected) ?? []
})
let wiggle = false
Expand Down Expand Up @@ -366,7 +365,7 @@ display above those of another following shortly after it -->
padding: 1pt 2pt 1pt 5pt;
transition: 0.3s;
white-space: nowrap;
background: var(--sms-selected-bg, var(--sms-active-color, cornflowerblue));
background: var(--sms-selected-bg, rgba(0, 0, 0, 0.15));
height: var(--sms-selected-li-height);
}
:where(div.multiselect > ul.selected > li button, button.remove-all) {
Expand All @@ -383,6 +382,7 @@ display above those of another following shortly after it -->
cursor: pointer;
outline: none;
padding: 0 2pt;
margin: 0; /* CSS reset */
}
:where(ul.selected > li button:hover, button.remove-all:hover, button:focus) {
color: var(--sms-remove-x-hover-focus-color, lightskyblue);
Expand All @@ -391,6 +391,10 @@ display above those of another following shortly after it -->
transform: scale(1.04);
}
:where(div.multiselect input) {
margin: auto 0; /* CSS reset */
padding: 0; /* CSS reset */
}
:where(div.multiselect > ul.selected > li > input) {
border: none;
outline: none;
Expand All @@ -410,6 +414,7 @@ display above those of another following shortly after it -->
outline: none;
z-index: -1;
opacity: 0;
pointer-events: none;
}
:where(div.multiselect > ul.options) {
Expand Down
3 changes: 1 addition & 2 deletions static/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ body {
body > div {
display: flex;
margin: auto;
max-width: 1000px;
max-width: 1100px;
}
main {
max-width: 50em;
margin: auto;
margin-bottom: 3em;
width: 100%;
Expand Down
53 changes: 49 additions & 4 deletions tests/multiselect.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { fireEvent, render } from '@testing-library/svelte'
import { expect, test } from 'vitest'
import { cleanup, fireEvent, render } from '@testing-library/svelte'
import { readFileSync } from 'fs'
import { afterEach, expect, test } from 'vitest'
import MultiSelect from '../src/lib'

const options = [`Banana`, `Watermelon`, `Apple`, `Dates`, `Mango`]
const placeholder = `Select fruits`

afterEach(cleanup)

test(`can focus input, enter text, toggle hidden options and select an option`, async () => {
const { getByPlaceholderText, getByText, container } = render(MultiSelect, {
const { getByText, container, getByPlaceholderText } = render(MultiSelect, {
options,
placeholder,
})
Expand All @@ -15,7 +18,7 @@ test(`can focus input, enter text, toggle hidden options and select an option`,

expect(ul_ops?.classList.contains(`hidden`)).to.equal(true)

const input = getByPlaceholderText(`Select fruits`)
const input = getByPlaceholderText(placeholder)
await fireEvent.focus(input)
await fireEvent.input(input, { target: { value: `Apple` } })

Expand All @@ -34,6 +37,48 @@ test(`can focus input, enter text, toggle hidden options and select an option`,
expect(apple_sel.textContent?.trim()).toBe(`Apple`)
})

test(`readme documents all props`, () => {
const readme = readFileSync(`readme.md`, `utf8`)

const instance = new MultiSelect({
target: document.body,
props: { options },
})

for (const prop of Object.keys(instance.$$.props)) {
expect(readme).to.contain(prop)
}
})

test(`remove all button`, async () => {
const { container, getByPlaceholderText } = render(MultiSelect, {
options,
placeholder,
})

const input = getByPlaceholderText(placeholder)
await fireEvent.focus(input)

const ul_ops = container.querySelector(`ul.options`)

expect(ul_ops?.children.length).toBe(options.length)

const li_ops = container.querySelector(`ul.options`)?.children
for (const li of li_ops ?? []) {
await fireEvent.mouseDown(li)
}

const ul_sel = container.querySelector(`ul.selected`)
// make sure all options are selected
expect(ul_sel?.textContent).toContain(`Mango Apple Banana`)

const rm_all_btn = container.querySelector(`button[title='Remove all']`)
await fireEvent.mouseUp(rm_all_btn)

const ul_sel_after = container.querySelector(`ul.selected`)
expect(ul_sel_after?.textContent).toBe(` `) // only input left
})

test(`default export from index.ts is same as component file`, async () => {
const { default: comp } = await import(`../src/lib/MultiSelect.svelte`)
expect(comp).toBe(MultiSelect)
Expand Down

0 comments on commit 33864be

Please sign in to comment.