feat: article editor core

This commit is contained in:
fsylvain committed 2026-09-27 02:11:38 +02:00
1 parent d493a181e1
commit a4734b2c91
55 files changed
+4186 -496

No files matched your search

+10 -3
View File
@@ -8,7 +8,10 @@ beforeEach(() => {
resetAuthShellNavigation()
})
function mountShell(props: Record<string, unknown>, slots: Record<string, unknown> = {}) {
function mountShell(
props: Record<string, unknown>,
slots: Record<string, unknown> = {},
) {
return mount(AuthShell, {
props,
slots,
@@ -63,13 +66,17 @@ describe('AuthShell renders its notice and footer slots', () => {
const notice = wrapper.find('span')
expect(notice.text()).toBe('Your session expired.')
expect(wrapper.findAll('[data-slot="root"][data-orientation]').length).toBe(1)
expect(
wrapper.findAll('[data-slot="root"][data-orientation]').length,
).toBe(1)
const card = wrapper.find('[data-testid="auth-card"]')
expect(card.element.contains(notice.element)).toBe(true)
const html = wrapper.html()
expect(html.indexOf('<h1')).toBeLessThan(html.indexOf('Your session expired.'))
expect(html.indexOf('<h1')).toBeLessThan(
html.indexOf('Your session expired.'),
)
const footerLink = wrapper.find('a[href="/register/reset/request"]')
expect(footerLink.exists()).toBe(true)
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import uiPlugin from '@nuxt/ui/vue-plugin'
import CharacterCounter from '../CharacterCounter.vue'
function mountCounter(value: number) {
return mount(CharacterCounter, {
props: { value, max: 10000 },
global: { plugins: [uiPlugin] },
})
}
describe('Below the warning threshold', () => {
it('shows the plain count with no icon', () => {
const wrapper = mountCounter(4318)
expect(wrapper.text()).toContain('4 318 / 10 000')
expect(
wrapper
.find('[data-testid="character-counter-warning-icon"]')
.exists(),
).toBe(false)
expect(
wrapper
.find('[data-testid="character-counter-over-icon"]')
.exists(),
).toBe(false)
})
})
describe('Near the limit — 90% or more', () => {
it('shows the warning icon without an over-limit message', () => {
const wrapper = mountCounter(9240)
expect(wrapper.text()).toContain('9 240 / 10 000')
expect(
wrapper
.find('[data-testid="character-counter-warning-icon"]')
.exists(),
).toBe(true)
expect(wrapper.text()).not.toContain('characters over')
})
})
describe('Too long — over the max', () => {
it('shows the over-limit icon and the exact overage copy', () => {
const wrapper = mountCounter(10214)
expect(wrapper.text()).toContain('10 214 / 10 000')
expect(
wrapper
.find('[data-testid="character-counter-over-icon"]')
.exists(),
).toBe(true)
expect(wrapper.text()).toContain(
'214 characters over. Shorten the text or remove an image. Formatting counts too.',
)
})
})
@@ -74,9 +74,14 @@ describe('Cancelling — the action is in flight and the dialog cannot be dismis
await clickButton('Cancel registration')
expect(action).toHaveBeenCalledTimes(1)
expect((findButtonElement('Keep my account') as HTMLButtonElement).disabled).toBe(true)
expect(
(findButtonElement('Keep my account') as HTMLButtonElement)
.disabled,
).toBe(true)
expect(findButtonElement('Cancelling…')).toBeTruthy()
expect((findButtonElement('Cancelling…') as HTMLButtonElement).disabled).toBe(true)
expect(
(findButtonElement('Cancelling…') as HTMLButtonElement).disabled,
).toBe(true)
expect(document.querySelector('[aria-busy="true"]')).toBeTruthy()
resolveAction?.()
@@ -112,18 +117,72 @@ describe('Cancelled — resolving the action closes the dialog', () => {
})
})
describe('Dismissing (Esc or the overlay) defaults to the cancel outcome', () => {
it('emits close(false) without calling the action', async () => {
const action = vi.fn(() => Promise.resolve())
const wrapper = mountDialog(action)
await flushPromises()
await wrapper.setProps({ open: false })
expect(action).not.toHaveBeenCalled()
expect(wrapper.emitted('close')).toEqual([[false]])
wrapper.unmount()
})
})
describe('dismissResolvesTo="confirm" makes Esc/overlay resolve as confirmed', () => {
it('emits close(true) on dismiss without calling the action', async () => {
const action = vi.fn(() => Promise.resolve())
const wrapper = mount(ConfirmDialog, {
attachTo: document.body,
props: {
title: 'Leave without saving?',
description:
'Your changes to "How we price a bathroom renovation" haven’t been saved.',
confirmLabel: 'Keep editing',
cancelLabel: 'Discard changes',
pendingLabel: 'Keep editing',
tone: 'neutral',
action,
dismissResolvesTo: 'confirm',
},
global: {
plugins: [uiPlugin],
},
})
await flushPromises()
await wrapper.setProps({ open: false })
expect(action).not.toHaveBeenCalled()
expect(wrapper.emitted('close')).toEqual([[true]])
wrapper.unmount()
})
})
describe('Cancel failed — the dialog stays open and shows the failure inline', () => {
it('re-enables both buttons and shows the rejection message inline', async () => {
const action = vi.fn(() => Promise.reject(new Error("Couldn't cancel registration.")))
const action = vi.fn(() =>
Promise.reject(new Error("Couldn't cancel registration.")),
)
const wrapper = mountDialog(action)
await flushPromises()
await clickButton('Cancel registration')
expect(wrapper.emitted('close')).toBeFalsy()
expect(document.body.textContent).toContain("Couldn't cancel registration.")
expect((findButtonElement('Keep my account') as HTMLButtonElement).disabled).toBe(false)
expect((findButtonElement('Cancel registration') as HTMLButtonElement).disabled).toBe(false)
expect(document.body.textContent).toContain(
"Couldn't cancel registration.",
)
expect(
(findButtonElement('Keep my account') as HTMLButtonElement)
.disabled,
).toBe(false)
expect(
(findButtonElement('Cancel registration') as HTMLButtonElement)
.disabled,
).toBe(false)
wrapper.unmount()
})
})
@@ -56,7 +56,9 @@ describe('DashboardShell renders the three navigation destinations', () => {
describe('DashboardShell shows the offline banner when the browser reports offline', () => {
it('shows the banner while offline and hides it once back online', async () => {
const onLineSpy = vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(false)
const onLineSpy = vi
.spyOn(navigator, 'onLine', 'get')
.mockReturnValue(false)
const wrapper = await mountShell('/article')
@@ -77,12 +79,16 @@ describe('The sidebar header renders AppLogo and the "RenewCMS" wordmark', () =>
const wrapper = await mountShell('/home')
const header = wrapper.find('[data-slot="header"]')
expect(header.find('svg[role="img"][aria-label="RenewCMS"]').exists()).toBe(true)
expect(
header.find('svg[role="img"][aria-label="RenewCMS"]').exists(),
).toBe(true)
expect(header.text()).toContain('RenewCMS')
expect(header.find('a').exists()).toBe(false)
const headerHtml = header.html()
expect(headerHtml.indexOf('<svg')).toBeLessThan(headerHtml.indexOf('RenewCMS</span>'))
expect(headerHtml.indexOf('<svg')).toBeLessThan(
headerHtml.indexOf('RenewCMS</span>'),
)
})
})
@@ -91,7 +97,9 @@ describe('The sidebar collapse trigger toggles the rail and exposes aria-expande
await mountShell('/home')
const collapseButton = () =>
document.body.querySelector('[data-testid="sidebar-collapse"]') as HTMLButtonElement
document.body.querySelector(
'[data-testid="sidebar-collapse"]',
) as HTMLButtonElement
expect(collapseButton().getAttribute('aria-expanded')).toBe('true')
@@ -112,11 +120,13 @@ describe('A successful logout clears the session and navigates to /login', () =>
await mountShell('/home')
const logOutItem = () =>
Array.from(document.body.querySelectorAll('[role="menuitem"]')).find(
(el) => el.textContent?.trim() === 'Log out',
) as HTMLElement
Array.from(
document.body.querySelectorAll('[role="menuitem"]'),
).find((el) => el.textContent?.trim() === 'Log out') as HTMLElement
const trigger = document.body.querySelector('button[aria-haspopup]') as HTMLButtonElement
const trigger = document.body.querySelector(
'button[aria-haspopup]',
) as HTMLButtonElement
trigger.click()
await flushPromises()
@@ -141,16 +151,28 @@ describe('Collapsed sidebar hides the wordmark and username text but keeps an ac
collapseButton.click()
await flushPromises()
const header = document.body.querySelector('[data-slot="header"]') as HTMLElement
expect(header.querySelector('svg[role="img"][aria-label="RenewCMS"]')).toBeTruthy()
const header = document.body.querySelector(
'[data-slot="header"]',
) as HTMLElement
expect(
header.querySelector('svg[role="img"][aria-label="RenewCMS"]'),
).toBeTruthy()
expect(header.textContent).not.toContain('RenewCMS')
expect(header.querySelector('span[aria-hidden="true"]')).toBeNull()
const footer = document.body.querySelector('[data-slot="footer"]') as HTMLElement
const footer = document.body.querySelector(
'[data-slot="footer"]',
) as HTMLElement
expect(footer.textContent).not.toContain('marta')
const userMenuTrigger = footer.querySelector('button[aria-haspopup]') as HTMLButtonElement
expect(userMenuTrigger.getAttribute('aria-label')).toBe('marta — account menu')
expect(footer.querySelector('[data-slot="fallback"]')?.textContent).toBe('MA')
const userMenuTrigger = footer.querySelector(
'button[aria-haspopup]',
) as HTMLButtonElement
expect(userMenuTrigger.getAttribute('aria-label')).toBe(
'marta — account menu',
)
expect(
footer.querySelector('[data-slot="fallback"]')?.textContent,
).toBe('MA')
})
})
+18 -6
View File
@@ -18,7 +18,9 @@ async function mountNotFound(breadcrumb?: string) {
const router = makeRouter()
await router.push('/')
return mount(ErrorView, {
props: breadcrumb ? { kind: 'not-found', breadcrumb } : { kind: 'not-found' },
props: breadcrumb
? { kind: 'not-found', breadcrumb }
: { kind: 'not-found' },
global: {
plugins: [router],
},
@@ -29,20 +31,30 @@ describe('ErrorView not-found kind', () => {
it('renders the not-found copy with a breadcrumb, primary and secondary actions', async () => {
const wrapper = await mountNotFound('Not found')
expect(wrapper.find('nav[aria-label="Breadcrumb"]').text()).toBe('Not found')
expect(wrapper.find('nav[aria-label="Breadcrumb"]').text()).toBe(
'Not found',
)
expect(wrapper.find('h1').text()).toBe('Page not found')
expect(wrapper.text()).toContain('The link may be mistyped, or the page may have moved.')
expect(wrapper.text()).toContain(
'The link may be mistyped, or the page may have moved.',
)
const buttons = wrapper.findAll('button')
expect(buttons.some((button) => button.text() === 'Go to Home')).toBe(true)
expect(buttons.some((button) => button.text() === 'All articles')).toBe(true)
expect(buttons.some((button) => button.text() === 'Go to Home')).toBe(
true,
)
expect(buttons.some((button) => button.text() === 'All articles')).toBe(
true,
)
})
it('routes the primary action to /home and the secondary action to /article', async () => {
const wrapper = await mountNotFound()
const router = wrapper.vm.$router
const primary = wrapper.findAll('button').find((button) => button.text() === 'Go to Home')
const primary = wrapper
.findAll('button')
.find((button) => button.text() === 'Go to Home')
await primary?.trigger('click')
await flushPromises()
expect(router.currentRoute.value.path).toBe('/home')
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import uiPlugin from '@nuxt/ui/vue-plugin'
import PublishControl from '../PublishControl.vue'
function mountControl(props: {
online: boolean
dirty: boolean
disabled?: boolean
}) {
return mount(PublishControl, {
props,
global: { plugins: [uiPlugin] },
})
}
describe('Draft, clean', () => {
it('reads "Publish"', () => {
const wrapper = mountControl({ online: false, dirty: false })
expect(wrapper.text()).toBe('Publish')
})
})
describe('Draft, dirty', () => {
it('reads "Save and publish"', () => {
const wrapper = mountControl({ online: false, dirty: true })
expect(wrapper.text()).toBe('Save and publish')
})
})
describe('Online', () => {
it('reads "Unpublish" regardless of dirty state', () => {
const wrapper = mountControl({ online: true, dirty: true })
expect(wrapper.text()).toBe('Unpublish')
})
})
describe('Disabled', () => {
it('disables the button', () => {
const wrapper = mountControl({
online: false,
dirty: false,
disabled: true,
})
expect(
(wrapper.find('button').element as HTMLButtonElement).disabled,
).toBe(true)
})
})
@@ -0,0 +1,87 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import uiPlugin from '@nuxt/ui/vue-plugin'
import SaveStatus from '../SaveStatus.vue'
afterEach(() => {
vi.useRealTimers()
})
describe('Saved within the hour', () => {
it('reads "Saved at HH:MM"', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-09-26T11:10:00Z'))
const wrapper = mount(SaveStatus, {
props: {
state: 'saved',
savedAt: new Date('2026-09-26T11:02:00Z'),
},
global: { plugins: [uiPlugin] },
})
expect(wrapper.text()).toContain('Saved at 11:02')
})
})
describe('Saved over an hour ago', () => {
it('reads "Saved n min ago"', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-09-26T12:32:00Z'))
const wrapper = mount(SaveStatus, {
props: {
state: 'saved',
savedAt: new Date('2026-09-26T11:02:00Z'),
},
global: { plugins: [uiPlugin] },
})
expect(wrapper.text()).toContain('Saved 90 min ago')
})
})
describe('Dirty', () => {
it('reads "Unsaved changes"', () => {
const wrapper = mount(SaveStatus, {
props: { state: 'dirty' },
global: { plugins: [uiPlugin] },
})
expect(wrapper.text()).toContain('Unsaved changes')
})
})
describe('Saving', () => {
it('reads "Saving…"', () => {
const wrapper = mount(SaveStatus, {
props: { state: 'saving' },
global: { plugins: [uiPlugin] },
})
expect(wrapper.text()).toContain('Saving…')
})
})
describe('Error', () => {
it('reads "Save failed" and emits retry from the Retry button', async () => {
const wrapper = mount(SaveStatus, {
props: { state: 'error' },
global: { plugins: [uiPlugin] },
})
expect(wrapper.text()).toContain('Save failed')
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('retry')).toHaveLength(1)
})
})
describe('Offline', () => {
it('reads "Waiting for connection"', () => {
const wrapper = mount(SaveStatus, {
props: { state: 'offline' },
global: { plugins: [uiPlugin] },
})
expect(wrapper.text()).toContain('Waiting for connection')
})
})
+35 -15
View File
@@ -28,7 +28,9 @@ function findMenuItemsByRole(role: string) {
}
function findCheckboxItem(label: string) {
return findMenuItemsByRole('menuitemcheckbox').find((el) => el.textContent?.trim() === label)
return findMenuItemsByRole('menuitemcheckbox').find(
(el) => el.textContent?.trim() === label,
)
}
beforeEach(() => {
@@ -56,8 +58,8 @@ describe("UserMenu's dropdown lists the account label, color-mode checkboxes, th
expect(groups[0].textContent).toContain('marta')
const checkboxLabels = findMenuItemsByRole('menuitemcheckbox').map((el) =>
el.textContent?.trim(),
const checkboxLabels = findMenuItemsByRole('menuitemcheckbox').map(
(el) => el.textContent?.trim(),
)
expect(checkboxLabels).toEqual(['System', 'Light', 'Dark'])
@@ -82,9 +84,15 @@ describe("UserMenu's dropdown lists the account label, color-mode checkboxes, th
const wrapper = mountMenu({ username: 'marta', collapsed: false })
await openMenu()
expect(findCheckboxItem('Dark')?.getAttribute('aria-checked')).toBe('true')
expect(findCheckboxItem('Light')?.getAttribute('aria-checked')).toBe('false')
expect(findCheckboxItem('System')?.getAttribute('aria-checked')).toBe('false')
expect(findCheckboxItem('Dark')?.getAttribute('aria-checked')).toBe(
'true',
)
expect(findCheckboxItem('Light')?.getAttribute('aria-checked')).toBe(
'false',
)
expect(findCheckboxItem('System')?.getAttribute('aria-checked')).toBe(
'false',
)
wrapper.unmount()
})
@@ -105,15 +113,23 @@ describe('Selecting a color-mode item updates the preference and its checked sta
const label = to.charAt(0).toUpperCase() + to.slice(1)
const fromLabel = from.charAt(0).toUpperCase() + from.slice(1)
findCheckboxItem(label)?.dispatchEvent(new MouseEvent('pointerup', { bubbles: true }))
findCheckboxItem(label)?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
findCheckboxItem(label)?.dispatchEvent(
new MouseEvent('pointerup', { bubbles: true }),
)
findCheckboxItem(label)?.dispatchEvent(
new MouseEvent('click', { bubbles: true }),
)
await flushPromises()
expect(colorMode.preference).toBe(to)
await openMenu()
expect(findCheckboxItem(label)?.getAttribute('aria-checked')).toBe('true')
expect(findCheckboxItem(fromLabel)?.getAttribute('aria-checked')).toBe('false')
expect(findCheckboxItem(label)?.getAttribute('aria-checked')).toBe(
'true',
)
expect(findCheckboxItem(fromLabel)?.getAttribute('aria-checked')).toBe(
'false',
)
wrapper.unmount()
})
@@ -124,9 +140,9 @@ describe('A successful logout clears the session and navigates to /login', () =>
const wrapper = mountMenu({ username: 'marta', collapsed: false })
await openMenu()
const logOutItem = Array.from(document.body.querySelectorAll('[role="menuitem"]')).find(
(el) => el.textContent?.trim() === 'Log out',
)
const logOutItem = Array.from(
document.body.querySelectorAll('[role="menuitem"]'),
).find((el) => el.textContent?.trim() === 'Log out')
;(logOutItem as HTMLElement)?.click()
await flushPromises()
@@ -141,8 +157,12 @@ describe('Collapsed sidebar hides the wordmark and username text but keeps an ac
const wrapper = mountMenu({ username: 'marta', collapsed: true })
expect(wrapper.text()).not.toContain('marta')
expect(findTriggerButton()?.getAttribute('aria-label')).toBe('marta — account menu')
expect(document.body.querySelector('[data-slot="fallback"]')?.textContent).toBe('MA')
expect(findTriggerButton()?.getAttribute('aria-label')).toBe(
'marta — account menu',
)
expect(
document.body.querySelector('[data-slot="fallback"]')?.textContent,
).toBe('MA')
await openMenu()
@@ -33,7 +33,8 @@ let currentWrapper: Awaited<ReturnType<typeof mount>> | null = null
async function mountLogin() {
setActivePinia(createPinia())
global.fetch = (() => mockResponse(200, { open: false })) as unknown as typeof fetch
global.fetch = (() =>
mockResponse(200, { open: false })) as unknown as typeof fetch
await router.push('/login')
const wrapper = mount(LoginView, {
attachTo: document.body,
@@ -69,7 +70,9 @@ describe('AppLogo renders the brand mark as inline SVG, and theme-switches', ()
expect(card.element.contains(svg.element)).toBe(true)
const html = wrapper.html()
expect(html.indexOf('<svg')).toBeGreaterThan(html.indexOf('data-testid="auth-card"'))
expect(html.indexOf('<svg')).toBeGreaterThan(
html.indexOf('data-testid="auth-card"'),
)
expect(html.indexOf('<svg')).toBeLessThan(html.indexOf('<h1'))
let centeringAncestor = svg.element.parentElement
@@ -110,7 +113,9 @@ describe("UPageCard's title is the page h1, and its description uses UPageCard's
expect(heading.text()).toBe('Log in')
expect(heading.attributes('tabindex')).toBe('-1')
expect(wrapper.text()).toContain('Welcome back. Log in to manage your articles.')
expect(wrapper.text()).toContain(
'Welcome back. Log in to manage your articles.',
)
expect(wrapper.findAll('p').length).toBe(0)
})
})
@@ -175,8 +180,12 @@ describe('The login form is a UForm', () => {
const form = wrapper.find('form')
expect(form.exists()).toBe(true)
expect(form.attributes('id')).toMatch(/^v-/)
expect(form.find('input[placeholder="Your username"]').exists()).toBe(true)
expect(form.find('input[autocomplete="current-password"]').exists()).toBe(true)
expect(form.find('input[placeholder="Your username"]').exists()).toBe(
true,
)
expect(
form.find('input[autocomplete="current-password"]').exists(),
).toBe(true)
expect(form.find('button[type="submit"]').exists()).toBe(true)
})
})
@@ -185,14 +194,20 @@ describe('"Forgot password?" renders in the Password field\'s hint slot', () =>
it('renders the link through the hint slot, and the label reads only Password', async () => {
const wrapper = await mountLogin()
const passwordInput = wrapper.find('input[autocomplete="current-password"]')
const fieldRoot = passwordInput.element.closest('[data-slot="root"][data-orientation]')
const passwordInput = wrapper.find(
'input[autocomplete="current-password"]',
)
const fieldRoot = passwordInput.element.closest(
'[data-slot="root"][data-orientation]',
)
expect(fieldRoot).toBeTruthy()
const hint = fieldRoot!.querySelector('[data-slot="hint"]')
expect(hint).toBeTruthy()
expect(hint!.textContent?.trim()).toBe('Forgot password?')
expect(hint!.querySelector('a[href="/register/reset/request"]')).toBeTruthy()
expect(
hint!.querySelector('a[href="/register/reset/request"]'),
).toBeTruthy()
const label = fieldRoot!.querySelector('[data-slot="label"]')
expect(label!.textContent?.trim()).toBe('Password')
@@ -204,7 +219,9 @@ describe('Inputs are neutral by default and turn to the error edge only when the
const wrapper = await mountLogin()
const usernameInput = wrapper.find('input[placeholder="Your username"]')
const passwordInput = wrapper.find('input[autocomplete="current-password"]')
const passwordInput = wrapper.find(
'input[autocomplete="current-password"]',
)
expect(usernameInput.classes()).toContain('ring-accented')
expect(usernameInput.classes()).not.toContain('ring-error')
expect(passwordInput.classes()).toContain('ring-accented')
@@ -215,7 +232,9 @@ describe('Inputs are neutral by default and turn to the error edge only when the
await flushPromises()
const usernameAfter = wrapper.find('input[placeholder="Your username"]')
const passwordAfter = wrapper.find('input[autocomplete="current-password"]')
const passwordAfter = wrapper.find(
'input[autocomplete="current-password"]',
)
expect(usernameAfter.classes()).toContain('ring-error')
expect(usernameAfter.classes()).not.toContain('ring-accented')
expect(passwordAfter.classes()).toContain('ring-error')
@@ -33,7 +33,8 @@ let currentWrapper: Awaited<ReturnType<typeof mount>> | null = null
async function mountLogin(path = '/login') {
setActivePinia(createPinia())
global.fetch = (() => mockResponse(200, { open: false })) as unknown as typeof fetch
global.fetch = (() =>
mockResponse(200, { open: false })) as unknown as typeof fetch
await router.push(path)
const wrapper = mount(LoginView, {
attachTo: document.body,
@@ -77,7 +78,9 @@ describe('A field error shows the circle-alert icon with no extra focus halo', (
await wrapper.find('form').trigger('submit')
await flushPromises()
const fieldRoots = wrapper.findAll('[data-slot="root"][data-orientation]')
const fieldRoots = wrapper.findAll(
'[data-slot="root"][data-orientation]',
)
const usernameError = fieldRoots[0]!.find('[data-slot="error"]')
const passwordError = fieldRoots[1]!.find('[data-slot="error"]')
@@ -94,7 +97,9 @@ describe('A field error shows the circle-alert icon with no extra focus halo', (
expect(usernameInput.classes()).toContain('ring-error')
expect(usernameInput.classes()).toContain('focus-visible:ring-error')
expect(usernameInput.classes()).not.toContain('ring-primary')
expect(usernameInput.classes()).not.toContain('focus-visible:ring-primary')
expect(usernameInput.classes()).not.toContain(
'focus-visible:ring-primary',
)
})
})
@@ -141,19 +146,31 @@ describe('The alert renders inside the card, under the description', () => {
expect(card.element.contains(alert.element)).toBe(true)
expect(alert.text()).toContain(text)
const rootsInCard = card.findAll('[data-slot="root"][data-orientation]')
expect(rootsInCard.some((root) => root.classes().includes('bg-primary'))).toBe(false)
const rootsInCard = card.findAll(
'[data-slot="root"][data-orientation]',
)
expect(
rootsInCard.some((root) =>
root.classes().includes('bg-primary'),
),
).toBe(false)
const usernameInput = wrapper.find('input[placeholder="Your username"]')
const usernameInput = wrapper.find(
'input[placeholder="Your username"]',
)
const html = card.html()
expect(html.indexOf('Welcome back')).toBeGreaterThan(-1)
expect(html.indexOf('Welcome back')).toBeLessThan(html.indexOf(text))
expect(html.indexOf('Welcome back')).toBeLessThan(
html.indexOf(text),
)
expect(html.indexOf(text)).toBeLessThan(html.indexOf('Username'))
expect(card.element.contains(usernameInput.element)).toBe(true)
expect(alert.classes().some((c) => /^bg-\(--rc-(info|success)-soft\)$/.test(c))).toBe(
true,
)
expect(
alert
.classes()
.some((c) => /^bg-\(--rc-(info|success)-soft\)$/.test(c)),
).toBe(true)
},
)
})