feat: SEO SPA editor integration
This commit is contained in:
1 parent
eb9876fde3
commit
470584400f
62 files changed
+7471
-58
No files matched your search
@@ -0,0 +1,153 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user