Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/EmbeddableBlueskyPost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const iframeHeight = ref(300)
const { data: embedData, status } = useLazyAsyncData<BlueskyOEmbedResponse>(
`bluesky-embed-${embeddedId}`,
() =>
$fetch('/api/atproto/bluesky-oembed', {
$fetch<BlueskyOEmbedResponse>('/api/atproto/bluesky-oembed', {
Copy link
Contributor Author

@jonathanyeong jonathanyeong Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to need someone with more nuxt/typescript knowledge to explain why adding this type fixes this issue:

Error: app/components/EmbeddableBlueskyPost.vue(16,5): error TS2321: Excessive stack depth comparing types 'Exclude<R extends "/api/atproto/bluesky-author-profiles" ? { key: "/api/atproto/bluesky-author-profiles"; exact: true; score: []; catchAll: false; } : { key: "/api/atproto/bluesky-author-profiles"; exact: false; score: `${R}/` extends `${infer RouteSeg}/${infer RouteRest}` ? `${RouteSeg}?` extends `${infer RouteSegW...' and '{ score: MaxTuple<((R extends "/api/atproto/bluesky-author-profiles" ? { key: "/api/atproto/bluesky-author-profiles"; exact: true; score: []; catchAll: false; } : { key: "/api/atproto/bluesky-author-profiles"; exact: false; score: `${R}/` extends `${infer RouteSeg}/${infer RouteRest}` ? `${RouteSeg}?` extends `${inf...'.

It was an AI suggestion, and I'm unsure why it works 🤷.

query: { url, colorMode: 'system' },
}),
{
Expand Down
2 changes: 0 additions & 2 deletions app/pages/blog/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script setup lang="ts">
const router = useRouter()

import type { BlogPostFrontmatter } from '#shared/schemas/blog'

const blogModules = import.meta.glob<BlogPostFrontmatter>('./*.md', { eager: true })
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@
"@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.1",
"@types/validate-npm-package-name": "4.0.2",
"@valibot/to-json-schema": "^1.5.0",
"@vitest/browser-playwright": "4.0.18",
"@vitest/coverage-v8": "4.0.18",
"@vue/test-utils": "2.4.6",
Expand Down
4 changes: 1 addition & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

130 changes: 130 additions & 0 deletions test/nuxt/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ import {
AppFooter,
AppHeader,
AppLogo,
AuthorAvatar,
AuthorList,
BlogPostListCard,
BlogPostWrapper,
BlueskyComment,
BlueskyComments,
EmbeddableBlueskyPost,
BaseCard,
BuildEnvironment,
ButtonBase,
Expand Down Expand Up @@ -2491,6 +2498,129 @@ describe('component accessibility audits', () => {
expect(results.violations).toEqual([])
})
})

describe('AuthorAvatar', () => {
it('should have no accessibility violations with fallback text', async () => {
const component = await mountSuspended(AuthorAvatar, {
props: {
author: {
name: 'Daniel Roe',
blueskyHandle: 'danielroe.dev',
avatar: null,
profileUrl: 'https://bsky.app/profile/danielroe.dev',
},
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('AuthorList', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(AuthorList, {
props: {
authors: [
{ name: 'Daniel Roe', blueskyHandle: 'danielroe.dev' },
{ name: 'Salma Alam-Naylor' },
],
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('BlogPostWrapper', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(BlogPostWrapper, {
props: {
frontmatter: {
authors: [{ name: 'Daniel Roe', blueskyHandle: 'danielroe.dev' }],
title: 'Building Accessible Vue Components',
date: '2024-06-15',
description: 'A guide to building accessible components in Vue.js applications.',
path: '/blog/building-accessible-vue-components',
slug: 'building-accessible-vue-components',
},
},
slots: { default: '<p>Blog post content here.</p>' },
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('BlueskyComment', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(BlueskyComment, {
props: {
comment: {
uri: 'at://did:plc:2gkh62xvzokhlf6li4ol3b3d/app.bsky.feed.post/3mcg7k75fdc2k',
cid: 'bafyreigincphooxt7zox3blbocf6hnczzv36fkuj2zi5iuzpjgq6gk6pju',
author: {
did: 'did:plc:2gkh62xvzokhlf6li4ol3b3d',
handle: 'patak.dev',
displayName: 'patak',
avatar:
'https://cdn.bsky.app/img/avatar/plain/did:plc:2gkh62xvzokhlf6li4ol3b3d/bafkreifgzl4e5jqlakd77ajvnilsb5tufsv24h2sxfwmitkzxrh3sk6mhq@jpeg',
},
text: 'our kids will need these new stories, thanks for writing this Daniel',
createdAt: '2026-01-14T23:22:05.257Z',
likeCount: 13,
replyCount: 0,
repostCount: 0,
replies: [],
},
depth: 0,
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('BlueskyComments', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(BlueskyComments, {
props: {
postUri: 'at://did:plc:jbeaa5kdaladzwq3r7f5xgwe/app.bsky.feed.post/3mcg6svsgsm2k',
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('EmbeddableBlueskyPost', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(EmbeddableBlueskyPost, {
props: {
url: 'https://bsky.app/profile/patak.dev/post/3mcg7k75fdc2k',
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})

describe('BlogPostListCard', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(BlogPostListCard, {
props: {
authors: [{ name: 'Daniel Roe', blueskyHandle: 'danielroe.dev' }],
title: 'Building Accessible Vue Components',
topics: ['accessibility', 'vue'],
excerpt: 'A guide to building accessible components in Vue.js applications.',
published: '2024-06-15',
path: 'building-accessible-vue-components',
index: 0,
},
})
const results = await runAxe(component)
expect(results.violations).toEqual([])
})
})
})

function applyTheme(colorMode: string, bgTheme: string | null) {
Expand Down
1 change: 1 addition & 0 deletions test/unit/a11y-component-coverage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const SKIPPED_COMPONENTS: Record<string, string> = {
// OgImage components are server-side rendered images, not interactive UI
'OgImage/Default.vue': 'OG Image component - server-rendered image, not interactive UI',
'OgImage/Package.vue': 'OG Image component - server-rendered image, not interactive UI',
'OgImage/BlogPost.vue': 'OG Image component - server-rendered image, not interactive UI',

// Client-only components with complex dependencies
'Header/AuthModal.client.vue': 'Complex auth modal with navigation - requires full app context',
Expand Down
Loading