|
1 | 1 | import type { Meta, StoryObj } from '@storybook/vue3-vite'; |
2 | 2 | import { expect, userEvent, waitFor, within } from 'storybook/test'; |
3 | | -import { ref } from 'vue'; |
| 3 | +import { onMounted, ref } from 'vue'; |
4 | 4 | import Combobox from './Combobox.vue'; |
5 | 5 | import Field from './Field.vue'; |
6 | 6 |
|
@@ -55,3 +55,59 @@ export const Default: Story = { |
55 | 55 | await expect(input).toHaveValue('e_brand_new'); |
56 | 56 | }, |
57 | 57 | }; |
| 58 | + |
| 59 | +/** |
| 60 | + * Options that arrive AFTER the field is focused — the remote-search shape, |
| 61 | + * where a consumer replaces `options` with each debounced response. |
| 62 | + * |
| 63 | + * This pins the focus handler opening unconditionally. With the previous |
| 64 | + * `open = filtered.length > 0` on focus, a click that landed before the first |
| 65 | + * response found an empty list, left the dropdown closed, and nothing ever |
| 66 | + * reopened it when the options arrived — the user saw a combobox that showed |
| 67 | + * nothing until they typed. Locally the response tends to win that race, which |
| 68 | + * is exactly why it shipped: the failure needed a slow network or a loaded CI |
| 69 | + * runner to show itself. |
| 70 | + */ |
| 71 | +export const RemoteOptions: Story = { |
| 72 | + render: () => ({ |
| 73 | + components: { Combobox, Field }, |
| 74 | + setup: () => { |
| 75 | + const value = ref(''); |
| 76 | + const options = ref<{ value: string; label: string }[]>([]); |
| 77 | + |
| 78 | + // Long enough that the play function's click below reliably beats |
| 79 | + // it — the point is focus-before-options, not a realistic latency. |
| 80 | + onMounted(() => { |
| 81 | + setTimeout(() => { |
| 82 | + options.value = cabinets; |
| 83 | + }, 600); |
| 84 | + }); |
| 85 | + |
| 86 | + return { value, options }; |
| 87 | + }, |
| 88 | + template: ` |
| 89 | + <div class="w-80 pb-48"> |
| 90 | + <Field label="File cabinet" name="cabinet" hint="Options load remotely."> |
| 91 | + <Combobox v-model="value" name="cabinet" :options="options" placeholder="e_invoices" /> |
| 92 | + </Field> |
| 93 | + </div>`, |
| 94 | + }), |
| 95 | + play: async ({ canvasElement }) => { |
| 96 | + const canvas = within(canvasElement); |
| 97 | + const input = canvas.getByRole('combobox'); |
| 98 | + |
| 99 | + // Focus while the list is still empty: no options, no empty-message, |
| 100 | + // so nothing may render yet — an open flag alone must not paint a box. |
| 101 | + await userEvent.click(input); |
| 102 | + await expect(canvas.queryByRole('listbox')).not.toBeInTheDocument(); |
| 103 | + |
| 104 | + // The options land ~600ms later. No typing, no ArrowDown, no second |
| 105 | + // click — the already-focused field must show them on its own. |
| 106 | + const listbox = await canvas.findByRole('listbox', {}, { timeout: 3000 }); |
| 107 | + await expect(within(listbox).getAllByRole('option')).toHaveLength(cabinets.length); |
| 108 | + |
| 109 | + // And the late-arriving list is live, not just visible. |
| 110 | + await userEvent.keyboard('{ArrowDown}{Enter}'); |
| 111 | + await expect(input).toHaveValue('e_invoices'); |
| 112 | + }, |
| 113 | +}; |
0 commit comments