From ddfea8d3b9005d48e30caa6c97144b9507826ee5 Mon Sep 17 00:00:00 2001 From: Eldar Dadashov Date: Fri, 26 Jun 2026 23:16:05 +0300 Subject: [PATCH 1/5] feat(VAlert): add duration prop to auto-dismiss alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `duration` prop that automatically dismisses the alert after the given time in milliseconds. A value of `0` (default) preserves the current behavior and never auto-dismisses. The timer restarts when the alert becomes visible or the duration changes, pauses while the alert is hovered or focused, and is cleared on manual close and on unmount — mirroring VSnackbar's timeout handling. resolves #20409 --- .../api-generator/src/locale/en/VAlert.json | 1 + .../vuetify/src/components/VAlert/VAlert.tsx | 62 ++++++++++++++++++- .../VAlert/__tests__/VAlert.spec.browser.tsx | 50 ++++++++++++++- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/packages/api-generator/src/locale/en/VAlert.json b/packages/api-generator/src/locale/en/VAlert.json index 65235ce29e6..70574d9b110 100644 --- a/packages/api-generator/src/locale/en/VAlert.json +++ b/packages/api-generator/src/locale/en/VAlert.json @@ -6,6 +6,7 @@ "closable": "Adds a close icon that can hide the alert.", "closeIcon": "Change the default icon used for **closable** alerts.", "closeLabel": "Text used for *aria-label* on **closable** alerts. Can also be customized globally in [Internationalization](/customization/internationalization).", + "duration": "Time (in milliseconds) to wait until the alert is automatically dismissed. A value of `0` keeps the alert visible.", "height": "Sets the height for the component.", "maxHeight": "Sets the maximum height for the component.", "maxWidth": "Sets the maximum width for the component.", diff --git a/packages/vuetify/src/components/VAlert/VAlert.tsx b/packages/vuetify/src/components/VAlert/VAlert.tsx index 42672ddc0e0..eea164c419d 100644 --- a/packages/vuetify/src/components/VAlert/VAlert.tsx +++ b/packages/vuetify/src/components/VAlert/VAlert.tsx @@ -25,7 +25,7 @@ import { makeThemeProps, provideTheme } from '@/composables/theme' import { genOverlays, makeVariantProps, useVariant } from '@/composables/variant' // Utilities -import { toRef } from 'vue' +import { onMounted, onScopeDispose, shallowRef, toRef, watch } from 'vue' import { genericComponent, propsFactory } from '@/util' // Types @@ -57,6 +57,10 @@ export const makeVAlertProps = propsFactory({ type: String, default: '$vuetify.close', }, + duration: { + type: [Number, String], + default: 0, + }, icon: { type: [Boolean, String, Function, Object] as PropType, default: null, @@ -107,6 +111,58 @@ export const VAlert = genericComponent()({ setup (props, { emit, slots }) { const isActive = useProxiedModel(props, 'modelValue') + const isHovering = shallowRef(false) + const isFocused = shallowRef(false) + + let activeTimeout = -1 + function startTimeout () { + window.clearTimeout(activeTimeout) + const duration = Number(props.duration) + + if (!isActive.value || !duration || duration < 0 || isHovering.value || isFocused.value) return + + activeTimeout = window.setTimeout(() => { + isActive.value = false + }, duration) + } + + function clearTimeout () { + window.clearTimeout(activeTimeout) + } + + function onPointerenter () { + isHovering.value = true + clearTimeout() + } + + function onPointerleave () { + isHovering.value = false + startTimeout() + } + + function onFocusin () { + isFocused.value = true + clearTimeout() + } + + function onFocusout (e: FocusEvent) { + if (e.relatedTarget && (e.currentTarget as HTMLElement | null)?.contains(e.relatedTarget as Node)) return + + isFocused.value = false + startTimeout() + } + + watch(isActive, startTimeout) + watch(() => props.duration, startTimeout) + + onMounted(() => { + if (isActive.value) startTimeout() + }) + + onScopeDispose(() => { + window.clearTimeout(activeTimeout) + }) + const icon = toRef(() => { if (props.icon === false) return undefined if (!props.type) return props.icon @@ -179,6 +235,10 @@ export const VAlert = genericComponent()({ props.style, ]} role="alert" + onPointerenter={ onPointerenter } + onPointerleave={ onPointerleave } + onFocusin={ onFocusin } + onFocusout={ onFocusout } > { genOverlays(false, 'v-alert') } diff --git a/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx b/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx index b14ed3eda95..0cf2d696152 100644 --- a/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx +++ b/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx @@ -1,7 +1,7 @@ import { VAlert } from '..' // Utilities -import { render, screen, showcase } from '@test' +import { render, screen, showcase, userEvent, wait } from '@test' const defaultColors = ['success', 'info', 'warning', 'error', 'invalid'] @@ -40,5 +40,53 @@ describe('VAlert', () => { }) }) + describe('duration', () => { + it('keeps the alert visible until the duration elapses, then dismisses it', async () => { + render(() => ) + + await screen.findByCSS('.v-alert') + + // should not be dismissed before the duration elapses + await wait(100) + expect(document.querySelector('.v-alert')).not.toBeNull() + + // should be dismissed after the duration + await expect.poll(() => document.querySelector('.v-alert'), { timeout: 2000 }).toBeNull() + }) + + it('emits update:modelValue when auto-dismissed', async () => { + const onUpdate = vi.fn() + + render(() => ) + + await expect.poll(() => onUpdate.mock.calls.length, { timeout: 2000 }).toBeGreaterThan(0) + expect(onUpdate).toHaveBeenLastCalledWith(false) + }) + + it('pauses the timer while hovered and resumes on leave', async () => { + render(() => ) + + const alert = await screen.findByCSS('.v-alert') + + await userEvent.hover(alert) + // stays visible past its duration while hovered + await wait(700) + expect(document.querySelector('.v-alert')).not.toBeNull() + + // resumes and dismisses once the pointer leaves + await userEvent.unhover(alert) + await expect.poll(() => document.querySelector('.v-alert'), { timeout: 2000 }).toBeNull() + }) + + it('does not auto-dismiss by default', async () => { + render(() => ) + + await screen.findByCSS('.v-alert') + await wait(300) + + expect(document.querySelector('.v-alert')).not.toBeNull() + }) + }) + showcase({ stories, props, component: VAlert }) }) From b3a4904215c7a6c5f2adeb5b67149f6b435ce39f Mon Sep 17 00:00:00 2001 From: Eldar Dadashov Date: Mon, 29 Jun 2026 00:59:40 +0300 Subject: [PATCH 2/5] refactor(VAlert): rename duration to timeout, share dismiss logic Addresses review feedback on #22947: - Rename the `VAlert` `duration` prop to `timeout` so it matches `VSnackbar` and avoids the cognitive load of two differently-named props with the same behavior. - Extract the auto-dismiss timeout and pause-on-hover/focus handling that was duplicated between `VSnackbar` and `VAlert` into a shared `useAutoDismiss` composable. `VSnackbar` keeps driving its visual countdown timer through the composable's `onStart`/`onClear` hooks. Both components now share the same `timeout` semantics: `-1` keeps the component visible indefinitely and `0` dismisses immediately. `VAlert` defaults to `-1`, so it stays persistent unless a timeout is set and remains backwards compatible. `VSnackbar` behavior is unchanged. --- .../api-generator/src/locale/en/VAlert.json | 2 +- .../vuetify/src/components/VAlert/VAlert.tsx | 63 ++----------- .../VAlert/__tests__/VAlert.spec.browser.tsx | 16 ++-- .../src/components/VSnackbar/VSnackbar.tsx | 69 +++----------- .../vuetify/src/composables/autoDismiss.ts | 94 +++++++++++++++++++ 5 files changed, 126 insertions(+), 118 deletions(-) create mode 100644 packages/vuetify/src/composables/autoDismiss.ts diff --git a/packages/api-generator/src/locale/en/VAlert.json b/packages/api-generator/src/locale/en/VAlert.json index 70574d9b110..c1ba0951640 100644 --- a/packages/api-generator/src/locale/en/VAlert.json +++ b/packages/api-generator/src/locale/en/VAlert.json @@ -6,7 +6,6 @@ "closable": "Adds a close icon that can hide the alert.", "closeIcon": "Change the default icon used for **closable** alerts.", "closeLabel": "Text used for *aria-label* on **closable** alerts. Can also be customized globally in [Internationalization](/customization/internationalization).", - "duration": "Time (in milliseconds) to wait until the alert is automatically dismissed. A value of `0` keeps the alert visible.", "height": "Sets the height for the component.", "maxHeight": "Sets the maximum height for the component.", "maxWidth": "Sets the maximum width for the component.", @@ -15,6 +14,7 @@ "modelValue": "Controls whether the component is visible or hidden.", "prominent": "Displays a larger vertically centered icon to draw more attention.", "tile": "Removes the component's border-radius.", + "timeout": "Time (in milliseconds) to wait until the alert is automatically dismissed. Use `-1` to keep the alert visible indefinitely. Changes to this property will reset the countdown.", "type": "Create a specialized alert that uses a contextual color and has a pre-defined icon.", "width": "Sets the width for the component." }, diff --git a/packages/vuetify/src/components/VAlert/VAlert.tsx b/packages/vuetify/src/components/VAlert/VAlert.tsx index eea164c419d..96161416d8d 100644 --- a/packages/vuetify/src/components/VAlert/VAlert.tsx +++ b/packages/vuetify/src/components/VAlert/VAlert.tsx @@ -8,6 +8,7 @@ import { VDefaultsProvider } from '@/components/VDefaultsProvider' import { VIcon } from '@/components/VIcon' // Composables +import { useAutoDismiss } from '@/composables/autoDismiss' import { useTextColor } from '@/composables/color' import { makeComponentProps } from '@/composables/component' import { makeDensityProps, useDensity } from '@/composables/density' @@ -25,7 +26,7 @@ import { makeThemeProps, provideTheme } from '@/composables/theme' import { genOverlays, makeVariantProps, useVariant } from '@/composables/variant' // Utilities -import { onMounted, onScopeDispose, shallowRef, toRef, watch } from 'vue' +import { toRef } from 'vue' import { genericComponent, propsFactory } from '@/util' // Types @@ -57,9 +58,9 @@ export const makeVAlertProps = propsFactory({ type: String, default: '$vuetify.close', }, - duration: { + timeout: { type: [Number, String], - default: 0, + default: -1, }, icon: { type: [Boolean, String, Function, Object] as PropType, @@ -111,57 +112,13 @@ export const VAlert = genericComponent()({ setup (props, { emit, slots }) { const isActive = useProxiedModel(props, 'modelValue') - const isHovering = shallowRef(false) - const isFocused = shallowRef(false) - let activeTimeout = -1 - function startTimeout () { - window.clearTimeout(activeTimeout) - const duration = Number(props.duration) - - if (!isActive.value || !duration || duration < 0 || isHovering.value || isFocused.value) return - - activeTimeout = window.setTimeout(() => { - isActive.value = false - }, duration) - } - - function clearTimeout () { - window.clearTimeout(activeTimeout) - } - - function onPointerenter () { - isHovering.value = true - clearTimeout() - } - - function onPointerleave () { - isHovering.value = false - startTimeout() - } - - function onFocusin () { - isFocused.value = true - clearTimeout() - } - - function onFocusout (e: FocusEvent) { - if (e.relatedTarget && (e.currentTarget as HTMLElement | null)?.contains(e.relatedTarget as Node)) return - - isFocused.value = false - startTimeout() - } - - watch(isActive, startTimeout) - watch(() => props.duration, startTimeout) - - onMounted(() => { - if (isActive.value) startTimeout() - }) - - onScopeDispose(() => { - window.clearTimeout(activeTimeout) - }) + const { + onPointerenter, + onPointerleave, + onFocusin, + onFocusout, + } = useAutoDismiss(isActive, () => Number(props.timeout)) const icon = toRef(() => { if (props.icon === false) return undefined diff --git a/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx b/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx index 0cf2d696152..d73e6f06bd3 100644 --- a/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx +++ b/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx @@ -40,36 +40,36 @@ describe('VAlert', () => { }) }) - describe('duration', () => { - it('keeps the alert visible until the duration elapses, then dismisses it', async () => { - render(() => ) + describe('timeout', () => { + it('keeps the alert visible until the timeout elapses, then dismisses it', async () => { + render(() => ) await screen.findByCSS('.v-alert') - // should not be dismissed before the duration elapses + // should not be dismissed before the timeout elapses await wait(100) expect(document.querySelector('.v-alert')).not.toBeNull() - // should be dismissed after the duration + // should be dismissed after the timeout await expect.poll(() => document.querySelector('.v-alert'), { timeout: 2000 }).toBeNull() }) it('emits update:modelValue when auto-dismissed', async () => { const onUpdate = vi.fn() - render(() => ) + render(() => ) await expect.poll(() => onUpdate.mock.calls.length, { timeout: 2000 }).toBeGreaterThan(0) expect(onUpdate).toHaveBeenLastCalledWith(false) }) it('pauses the timer while hovered and resumes on leave', async () => { - render(() => ) + render(() => ) const alert = await screen.findByCSS('.v-alert') await userEvent.hover(alert) - // stays visible past its duration while hovered + // stays visible past its timeout while hovered await wait(700) expect(document.querySelector('.v-alert')).not.toBeNull() diff --git a/packages/vuetify/src/components/VSnackbar/VSnackbar.tsx b/packages/vuetify/src/components/VSnackbar/VSnackbar.tsx index ea3c953a80c..0d30f7cef35 100644 --- a/packages/vuetify/src/components/VSnackbar/VSnackbar.tsx +++ b/packages/vuetify/src/components/VSnackbar/VSnackbar.tsx @@ -13,6 +13,7 @@ import { useSnackbarItem } from '@/components/VSnackbarQueue/queue' // Composables import { useLayout } from '@/composables' +import { useAutoDismiss } from '@/composables/autoDismiss' import { forwardRefs } from '@/composables/forwardRefs' import { IconValue } from '@/composables/icons' import { VuetifyLayoutKey } from '@/composables/layout' @@ -26,7 +27,7 @@ import { useToggleScope } from '@/composables/toggleScope' import { genOverlays, makeVariantProps, useVariant } from '@/composables/variant' // Utilities -import { computed, inject, mergeProps, nextTick, onMounted, onScopeDispose, ref, shallowRef, watch, watchEffect } from 'vue' +import { computed, inject, mergeProps, nextTick, onScopeDispose, ref, shallowRef, watchEffect } from 'vue' import { convertToUnit, genericComponent, noop, omit, propsFactory, refElement, useRender } from '@/util' // Types @@ -147,8 +148,6 @@ export const VSnackbar = genericComponent()({ let _lastOffset: string const timerRef = ref() - const isHovering = shallowRef(false) - const isFocused = shallowRef(false) const startY = shallowRef(0) const mainStyles = ref() const hasLayout = inject(VuetifyLayoutKey, undefined) @@ -161,61 +160,19 @@ export const VSnackbar = genericComponent()({ }) }) - watch(isActive, startTimeout) - watch(() => props.timeout, startTimeout) - - onMounted(() => { - if (isActive.value) startTimeout() + const { + isHovering, + isFocused, + onPointerenter, + onPointerleave, + onFocusin, + onFocusout, + } = useAutoDismiss(isActive, () => Number(props.timeout), { + getContentEl: () => overlay.value?.contentEl, + onClear: () => countdown.reset(), + onStart: () => nextTick(() => countdown.start(refElement(timerRef.value))), }) - let activeTimeout = -1 - function startTimeout () { - countdown.clear() - window.clearTimeout(activeTimeout) - const timeout = Number(props.timeout) - - if (!isActive.value || timeout === -1) return - - countdown.reset() - - const element = refElement(timerRef.value) - - nextTick(() => countdown.start(element)) - - activeTimeout = window.setTimeout(() => { - isActive.value = false - }, timeout) - } - - function clearTimeout () { - countdown.reset() - window.clearTimeout(activeTimeout) - } - - function onPointerenter () { - isHovering.value = true - clearTimeout() - } - - function onPointerleave () { - isHovering.value = false - if (!isFocused.value) startTimeout() - } - - function onFocusin () { - isFocused.value = true - clearTimeout() - } - - function onFocusout (event: FocusEvent) { - const contentEl = overlay.value?.contentEl - if (contentEl?.contains(event.relatedTarget as Node)) { - return - } - isFocused.value = false - if (!isHovering.value) startTimeout() - } - function onTouchstart (event: TouchEvent) { startY.value = event.touches[0].clientY } diff --git a/packages/vuetify/src/composables/autoDismiss.ts b/packages/vuetify/src/composables/autoDismiss.ts new file mode 100644 index 00000000000..74d7d0c5f22 --- /dev/null +++ b/packages/vuetify/src/composables/autoDismiss.ts @@ -0,0 +1,94 @@ +// Utilities +import { onMounted, onScopeDispose, shallowRef, watch } from 'vue' + +// Types +import type { Ref } from 'vue' + +export interface AutoDismissOptions { + /** Called when the timeout (re)starts */ + onStart?: () => void + /** Called when the pending timeout is cleared */ + onClear?: () => void + /** Element scoping the focusout containment check, defaults to the event's currentTarget */ + getContentEl?: () => HTMLElement | null | undefined +} + +/** + * Toggles `isActive` off after `timeout` ms, pausing while hovered or focused. + * A negative `timeout` never dismisses. Shared by VSnackbar and VAlert. + */ +export function useAutoDismiss ( + isActive: Ref, + timeout: () => number, + options: AutoDismissOptions = {}, +) { + const isHovering = shallowRef(false) + const isFocused = shallowRef(false) + + let activeTimeout = -1 + + function startTimeout () { + options.onClear?.() + window.clearTimeout(activeTimeout) + + const value = timeout() + + if (!isActive.value || value < 0 || isHovering.value || isFocused.value) return + + options.onStart?.() + + activeTimeout = window.setTimeout(() => { + isActive.value = false + }, value) + } + + function clearTimeout () { + options.onClear?.() + window.clearTimeout(activeTimeout) + } + + function onPointerenter () { + isHovering.value = true + clearTimeout() + } + + function onPointerleave () { + isHovering.value = false + if (!isFocused.value) startTimeout() + } + + function onFocusin () { + isFocused.value = true + clearTimeout() + } + + function onFocusout (e: FocusEvent) { + const contentEl = options.getContentEl?.() ?? (e.currentTarget as HTMLElement | null) + if (contentEl?.contains(e.relatedTarget as Node)) return + + isFocused.value = false + if (!isHovering.value) startTimeout() + } + + watch(isActive, startTimeout) + watch(timeout, startTimeout) + + onMounted(() => { + if (isActive.value) startTimeout() + }) + + onScopeDispose(() => { + window.clearTimeout(activeTimeout) + }) + + return { + isHovering, + isFocused, + startTimeout, + clearTimeout, + onPointerenter, + onPointerleave, + onFocusin, + onFocusout, + } +} From 69b014d8e982b1e4a183c9323e15e503518e1ac2 Mon Sep 17 00:00:00 2001 From: Eldar Dadashov Date: Mon, 29 Jun 2026 00:59:40 +0300 Subject: [PATCH 3/5] docs(VAlert): add timeout prop example Document the new `timeout` prop on the alerts page with a runnable example, mirroring the existing `VSnackbar` timeout example. --- .../src/examples/v-alert/prop-timeout.vue | 37 +++++++++++++++++++ .../docs/src/pages/en/components/alerts.md | 6 +++ 2 files changed, 43 insertions(+) create mode 100644 packages/docs/src/examples/v-alert/prop-timeout.vue diff --git a/packages/docs/src/examples/v-alert/prop-timeout.vue b/packages/docs/src/examples/v-alert/prop-timeout.vue new file mode 100644 index 00000000000..ff451880f86 --- /dev/null +++ b/packages/docs/src/examples/v-alert/prop-timeout.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/packages/docs/src/pages/en/components/alerts.md b/packages/docs/src/pages/en/components/alerts.md index 97701b717a5..30244220757 100644 --- a/packages/docs/src/pages/en/components/alerts.md +++ b/packages/docs/src/pages/en/components/alerts.md @@ -137,6 +137,12 @@ The close icon automatically applies a default `aria-label` and is configurable For more information on how to global modify your locale settings, navigate to the [Internationalization page](/features/internationalization). ::: +#### Timeout + +The **timeout** property automatically dismisses the alert after the specified time in milliseconds. Use `-1` (the default) to keep the alert visible indefinitely. The countdown pauses while the alert is hovered or focused, so it stays visible while being read or interacted with. + + + ## Additional Examples The following is a collection of `v-alert` examples that demonstrate how different the properties work in an application. From d4a852213640d2e8308547672058051737e309cb Mon Sep 17 00:00:00 2001 From: Eldar Dadashov Date: Mon, 29 Jun 2026 01:20:19 +0300 Subject: [PATCH 4/5] test(VSnackbar): cover timeout auto-dismiss and hover pause VSnackbar previously had no test coverage. Add browser tests for the timeout behavior now shared through `useAutoDismiss`: auto-dismiss after the timeout, pause while hovered, and stay open when the timeout is -1. --- .../__tests__/VSnackbar.spec.browser.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx diff --git a/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx b/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx new file mode 100644 index 00000000000..3ca87dd57eb --- /dev/null +++ b/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx @@ -0,0 +1,47 @@ +import { VSnackbar } from '..' + +// Utilities +import { render, screen, userEvent, wait } from '@test' + +const CONTENT = '.v-snackbar .v-overlay__content' + +describe('VSnackbar', () => { + describe('timeout', () => { + it('auto-dismisses after the timeout elapses', async () => { + render(() => ) + + await expect.poll(() => screen.queryByCSS(CONTENT)).toBeVisible() + + // should still be visible shortly before the timeout elapses + await wait(100) + expect(screen.queryByCSS(CONTENT)).toBeVisible() + + // should be dismissed after the timeout + await expect.poll(() => screen.queryByCSS(CONTENT), { timeout: 2000 }).toBeNull() + }) + + it('pauses the timer while hovered and resumes on leave', async () => { + render(() => ) + + const content = await screen.findByCSS(CONTENT) + + await userEvent.hover(content) + // stays visible past its timeout while hovered + await wait(700) + expect(screen.queryByCSS(CONTENT)).toBeVisible() + + // resumes and dismisses once the pointer leaves + await userEvent.unhover(content) + await expect.poll(() => screen.queryByCSS(CONTENT), { timeout: 2000 }).toBeNull() + }) + + it('stays open indefinitely when timeout is -1', async () => { + render(() => ) + + await expect.poll(() => screen.queryByCSS(CONTENT)).toBeVisible() + await wait(300) + + expect(screen.queryByCSS(CONTENT)).toBeVisible() + }) + }) +}) From c603d0429c1babb845e6c20704c86b12bc87726c Mon Sep 17 00:00:00 2001 From: Eldar Dadashov Date: Mon, 29 Jun 2026 01:36:02 +0300 Subject: [PATCH 5/5] test: keep auto-dismiss alerts clear of the headless CI cursor In headless CI the pointer rests at the top-left, over the full-width VAlert, which fires pointerenter and pauses its auto-dismiss timer, so the alert never closed (VSnackbar is unaffected since it renders away from the corner). Render the auto-dismiss alerts offset from the corner so their timer runs, and give the dismiss assertions a larger poll budget for slower CI. --- .../VAlert/__tests__/VAlert.spec.browser.tsx | 16 +++++++++++----- .../__tests__/VSnackbar.spec.browser.tsx | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx b/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx index d73e6f06bd3..834db070ee5 100644 --- a/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx +++ b/packages/vuetify/src/components/VAlert/__tests__/VAlert.spec.browser.tsx @@ -42,7 +42,9 @@ describe('VAlert', () => { describe('timeout', () => { it('keeps the alert visible until the timeout elapses, then dismisses it', async () => { - render(() => ) + // Offset from the top-left corner so the resting headless-CI cursor doesn't + // hover the full-width alert and pause its auto-dismiss timer. + render(() =>
) await screen.findByCSS('.v-alert') @@ -51,15 +53,19 @@ describe('VAlert', () => { expect(document.querySelector('.v-alert')).not.toBeNull() // should be dismissed after the timeout - await expect.poll(() => document.querySelector('.v-alert'), { timeout: 2000 }).toBeNull() + await expect.poll(() => document.querySelector('.v-alert'), { timeout: 5000 }).toBeNull() }) it('emits update:modelValue when auto-dismissed', async () => { const onUpdate = vi.fn() - render(() => ) + render(() => ( +
+ +
+ )) - await expect.poll(() => onUpdate.mock.calls.length, { timeout: 2000 }).toBeGreaterThan(0) + await expect.poll(() => onUpdate.mock.calls.length, { timeout: 5000 }).toBeGreaterThan(0) expect(onUpdate).toHaveBeenLastCalledWith(false) }) @@ -75,7 +81,7 @@ describe('VAlert', () => { // resumes and dismisses once the pointer leaves await userEvent.unhover(alert) - await expect.poll(() => document.querySelector('.v-alert'), { timeout: 2000 }).toBeNull() + await expect.poll(() => document.querySelector('.v-alert'), { timeout: 5000 }).toBeNull() }) it('does not auto-dismiss by default', async () => { diff --git a/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx b/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx index 3ca87dd57eb..ab2c2ca8837 100644 --- a/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx +++ b/packages/vuetify/src/components/VSnackbar/__tests__/VSnackbar.spec.browser.tsx @@ -8,7 +8,7 @@ const CONTENT = '.v-snackbar .v-overlay__content' describe('VSnackbar', () => { describe('timeout', () => { it('auto-dismisses after the timeout elapses', async () => { - render(() => ) + render(() => ) await expect.poll(() => screen.queryByCSS(CONTENT)).toBeVisible() @@ -17,7 +17,7 @@ describe('VSnackbar', () => { expect(screen.queryByCSS(CONTENT)).toBeVisible() // should be dismissed after the timeout - await expect.poll(() => screen.queryByCSS(CONTENT), { timeout: 2000 }).toBeNull() + await expect.poll(() => screen.queryByCSS(CONTENT), { timeout: 5000 }).toBeNull() }) it('pauses the timer while hovered and resumes on leave', async () => { @@ -32,7 +32,7 @@ describe('VSnackbar', () => { // resumes and dismisses once the pointer leaves await userEvent.unhover(content) - await expect.poll(() => screen.queryByCSS(CONTENT), { timeout: 2000 }).toBeNull() + await expect.poll(() => screen.queryByCSS(CONTENT), { timeout: 5000 }).toBeNull() }) it('stays open indefinitely when timeout is -1', async () => {