import { CHART_ANNOTATOR_SLOTS, type AnnotatorSlotName } from '~/utils/charts' import { DEFAULT_FILTERS, type ColumnConfig, type FilterChip } from '#shared/types/preferences' import { mountSuspended, registerEndpoint, mockNuxtImport } from '@nuxt/test-utils/runtime' import { flushPromises, type VueWrapper } from '@vue/test-utils' import 'axe-core' import type { AxeResults, RunOptions } from 'axe-core' import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from 'vite-plus/test' import { createLikesLeaderboardEntry } from '~~/test/fixtures/likes-leaderboard' // axe-core is a UMD module that exposes itself as window.axe in the browser declare const axe: { run: (context: Element, options?: RunOptions) => Promise } mockNuxtImport('useCharts', original => vi.fn(original)) // Track mounted containers for cleanup const mountedContainers: HTMLElement[] = [] const axeRunOptions: RunOptions = { // Only compute violations to reduce work per run resultTypes: ['violations'], // Disable rules that don't apply to isolated component testing rules: { // These rules check page-level concerns that don't apply to isolated components 'landmark-one-main': { enabled: false }, 'region': { enabled: false }, 'page-has-heading-one': { enabled: false }, // Duplicate landmarks are expected when testing multiple header/footer components 'landmark-no-duplicate-banner': { enabled: false }, 'landmark-no-duplicate-contentinfo': { enabled: false }, 'landmark-no-duplicate-main': { enabled: false }, }, } /** * Run axe accessibility audit on a mounted component. * Mounts the component in an isolated container to avoid cross-test pollution. */ async function runAxe(wrapper: VueWrapper): Promise { // Create an isolated container for this test const container = document.createElement('div') container.id = `test-container-${Date.now()}` document.body.appendChild(container) mountedContainers.push(container) // Clone the element into our isolated container const el = wrapper.element.cloneNode(true) as HTMLElement container.appendChild(el) // Run axe only on the isolated container return axe.run(container, axeRunOptions) } async function runAxeElements(elements: Array): Promise { const container = document.createElement('div') container.id = `test-container-${Date.now()}` document.body.appendChild(container) mountedContainers.push(container) for (const element of elements) { if (!element) continue container.appendChild(element.cloneNode(true)) } return axe.run(container, axeRunOptions) } // --- Console warning assertion -------------------------------------------------- // Fail any test that emits unexpected console.warn calls. This catches issues // like missing/invalid props that would otherwise silently pass. let warnSpy: MockInstance // Patterns that are expected and safe to ignore in the test environment. const allowedWarnings: RegExp[] = [ // vue-i18n logs this when is used outside a component-scoped i18n; // it falls back to the global scope and still renders correctly. /\[intlify\] Not found parent scope/, // mountSuspended wraps each component instance and calls expose() after // setup. For recursive components (e.g. DiffFileTree rendering child // DiffFileTree instances), this triggers a duplicate expose() call on the // inner wrapper. The warning does not affect test correctness. /expose\(\) should be called only once/, ] beforeEach(() => { warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) }) afterEach(() => { // Collect unexpected warnings const unexpected = warnSpy.mock.calls.filter( args => !allowedWarnings.some(re => re.test(String(args[0]))), ) warnSpy.mockRestore() if (unexpected.length > 0) { const msgs = unexpected.map(args => args.map(String).join(' ')).join('\n') throw new Error(`Test emitted unexpected console.warn:\n${msgs}`) } }) // Clean up mounted containers after each test afterEach(() => { for (const container of mountedContainers) { container.remove() } mountedContainers.length = 0 }) // VueUiXy is imported directly in