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() 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('[role="dialog"]')! sheet.querySelector('[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() }) })