154 lines
4.6 KiB
TypeScript
154 lines
4.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { config, flushPromises } from '@vue/test-utils'
|
|
import { MOBILE_WIDTH } from '@/components/__tests__/viewport'
|
|
import { META_SECTION_OPEN_KEY } from '@/lib/metaSectionMemory'
|
|
import { SEO_SECTION_OPEN_KEY } from '@/lib/seoSectionMemory'
|
|
import { click, unmountMeta } from './metaTestKit'
|
|
import {
|
|
FOUR_SUGGESTIONS,
|
|
answerChecks,
|
|
checkAnswer,
|
|
mountSeo,
|
|
openPanel,
|
|
openSection,
|
|
section,
|
|
sectionBody,
|
|
sectionToggle,
|
|
seoBackend,
|
|
} from './seoTestKit'
|
|
|
|
vi.mock('@/composables/useApi', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@/composables/useApi')>()
|
|
return { ...actual, useApi: vi.fn() }
|
|
})
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
sessionStorage.clear()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
config.global.config.errorHandler = undefined
|
|
vi.restoreAllMocks()
|
|
await unmountMeta()
|
|
})
|
|
|
|
function securityError() {
|
|
return new DOMException('Access denied', 'SecurityError')
|
|
}
|
|
|
|
function collectVueErrors() {
|
|
const uncaught: unknown[] = []
|
|
const previous = config.global.config.errorHandler
|
|
config.global.config.errorHandler = (error) => {
|
|
uncaught.push(error)
|
|
}
|
|
return {
|
|
uncaught,
|
|
stop: () => {
|
|
config.global.config.errorHandler = previous
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('The choice is remembered per browser, with its own key', () => {
|
|
it('uses a key of its own, independent of the Site and authors key', () => {
|
|
expect(SEO_SECTION_OPEN_KEY).not.toBe(META_SECTION_OPEN_KEY)
|
|
})
|
|
|
|
it('keeps an opening for the next article and a collapse after a reload, sending nothing to the API', async () => {
|
|
const backend = seoBackend()
|
|
await mountSeo(backend)
|
|
const callsBefore = backend.calls.length
|
|
await openSection()
|
|
expect(localStorage.getItem(SEO_SECTION_OPEN_KEY)).toBe('true')
|
|
expect(localStorage.getItem(META_SECTION_OPEN_KEY)).toBeNull()
|
|
expect(backend.calls).toHaveLength(callsBefore)
|
|
await unmountMeta()
|
|
|
|
await mountSeo(seoBackend())
|
|
expect(sectionBody()).not.toBeNull()
|
|
sectionToggle().click()
|
|
await flushPromises()
|
|
expect(localStorage.getItem(SEO_SECTION_OPEN_KEY)).toBe('false')
|
|
await unmountMeta()
|
|
|
|
await mountSeo(seoBackend())
|
|
expect(sectionBody()).toBeNull()
|
|
})
|
|
|
|
it('shares the choice between the sidebar and the Details sheet', async () => {
|
|
await mountSeo(seoBackend(), { width: MOBILE_WIDTH })
|
|
await click('Details')
|
|
const sheet = document.body.querySelector<HTMLElement>('[role="dialog"]')!
|
|
sheet.querySelector<HTMLElement>('[data-testid="seo-toggle"]')!.click()
|
|
await flushPromises()
|
|
|
|
expect(localStorage.getItem(SEO_SECTION_OPEN_KEY)).toBe('true')
|
|
const sidebar = document.body.querySelector('[data-testid="article-details"]')!
|
|
expect(sidebar.querySelector('[data-testid="seo-body"]')).not.toBeNull()
|
|
})
|
|
|
|
it('does not change the stored choice when the panel or the dialog is opened from the section', async () => {
|
|
const backend = seoBackend()
|
|
answerChecks(backend, checkAnswer(FOUR_SUGGESTIONS))
|
|
await mountSeo(backend)
|
|
await openSection()
|
|
const writes = vi.spyOn(Storage.prototype, 'setItem')
|
|
|
|
const panel = await openPanel()
|
|
expect(panel).not.toBeNull()
|
|
expect(writes).not.toHaveBeenCalled()
|
|
expect(localStorage.getItem(SEO_SECTION_OPEN_KEY)).toBe('true')
|
|
expect(section()).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('A storage that throws never breaks the editor', () => {
|
|
it.each(['getItem', 'setItem'] as const)(
|
|
'renders the editor and toggles the section for this page view when localStorage.%s throws',
|
|
async (call) => {
|
|
const errors = collectVueErrors()
|
|
vi.spyOn(Storage.prototype, call).mockImplementation(() => {
|
|
throw securityError()
|
|
})
|
|
|
|
await mountSeo(seoBackend())
|
|
|
|
expect(section()).not.toBeNull()
|
|
expect(sectionBody()).toBeNull()
|
|
sectionToggle().click()
|
|
await flushPromises()
|
|
expect(sectionBody()).not.toBeNull()
|
|
sectionToggle().click()
|
|
await flushPromises()
|
|
expect(sectionBody()).toBeNull()
|
|
errors.stop()
|
|
expect(errors.uncaught).toEqual([])
|
|
},
|
|
)
|
|
|
|
it('falls back to collapsed when reading throws, even if a choice was stored', async () => {
|
|
localStorage.setItem(SEO_SECTION_OPEN_KEY, 'true')
|
|
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
throw securityError()
|
|
})
|
|
|
|
await mountSeo(seoBackend())
|
|
|
|
expect(sectionBody()).toBeNull()
|
|
})
|
|
|
|
it('silently skips the write when setItem throws', async () => {
|
|
const setItem = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw securityError()
|
|
})
|
|
|
|
await mountSeo(seoBackend())
|
|
await openSection()
|
|
|
|
expect(setItem).toHaveBeenCalled()
|
|
expect(localStorage.getItem(SEO_SECTION_OPEN_KEY)).toBeNull()
|
|
})
|
|
})
|