diff --git a/website/src/i18n.js b/website/src/i18n.js
index 8ce5340..3f57a1e 100644
--- a/website/src/i18n.js
+++ b/website/src/i18n.js
@@ -11,9 +11,6 @@ export function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = i18n.t(el.dataset.i18n)
})
- document.querySelectorAll('[data-i18n-html]').forEach(el => {
- el.innerHTML = i18n.t(el.dataset.i18nHtml)
- })
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = i18n.t(el.dataset.i18nPlaceholder)
})
diff --git a/website/src/main.js b/website/src/main.js
index 91e3c58..0255224 100644
--- a/website/src/main.js
+++ b/website/src/main.js
@@ -6,37 +6,30 @@ import { renderMain } from './components/main-content.js'
import { renderFooter } from './components/footer.js'
import { renderCardGrid, initCardGrid, applyCardFilters, updateAnchorCount } from './components/card-grid.js'
import { fetchData } from './utils/data-loader.js'
-import { createModal, showAnchorDetails } from './components/anchor-modal.js'
-import { buildSearchIndex } from './utils/search-index.js'
+import { buildSearchIndex, isIndexReady, isIndexBuilding } from './utils/search-index.js'
import { initRouter, addRoute } from './utils/router.js'
import { renderDocPage, loadDocContent } from './components/doc-page.js'
const APP_VERSION = '0.4.0'
-// Global function for copying anchor links
-window.copyAnchorLink = async function(anchorId) {
+window.copyAnchorLink = async function copyAnchorLink(anchorId) {
const url = `${window.location.origin}${window.location.pathname}#/anchor/${anchorId}`
try {
await navigator.clipboard.writeText(url)
-
- // Show toast notification
window.showToast(i18n.t('card.linkCopied'))
} catch (err) {
console.error('Failed to copy link:', err)
}
}
-// Global toast notification function
-window.showToast = function(message) {
- // Create toast element
+window.showToast = function showToast(message) {
const toast = document.createElement('div')
toast.className = 'fixed bottom-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg z-50 animate-fade-in'
toast.textContent = message
document.body.appendChild(toast)
- // Remove after 2 seconds
setTimeout(() => {
toast.classList.add('animate-fade-out')
setTimeout(() => toast.remove(), 300)
@@ -44,28 +37,70 @@ window.showToast = function(message) {
}
let appData = null
+let dataLoadingPromise = null
+let searchIndexTriggered = false
+let anchorModalModulePromise = null
+
+function getAnchorModalModule() {
+ if (!anchorModalModulePromise) {
+ anchorModalModulePromise = import('./components/anchor-modal.js')
+ }
+ return anchorModalModulePromise
+}
+
+function ensureDataLoaded() {
+ if (appData) return Promise.resolve(appData)
+
+ if (!dataLoadingPromise) {
+ dataLoadingPromise = fetchData()
+ .then((data) => {
+ appData = data
+ return data
+ })
+ .catch((error) => {
+ dataLoadingPromise = null
+ throw error
+ })
+ }
+
+ return dataLoadingPromise
+}
+
+function triggerSearchIndexBuild() {
+ if (!appData || searchIndexTriggered || isIndexReady() || isIndexBuilding()) return
+
+ searchIndexTriggered = true
+ buildSearchIndex(appData.anchors)
+ .then(() => {
+ const searchInput = document.getElementById('search-input')
+ if (searchInput) {
+ searchInput.placeholder = `${i18n.t('search.placeholder')} (full-text)`
+ }
+ })
+ .catch((err) => {
+ console.warn('Search index build failed:', err)
+ searchIndexTriggered = false
+ })
+}
function initApp() {
i18n.init()
initTheme()
+ getAnchorModalModule().then(({ createModal }) => createModal())
- // Initialize anchor modal
- createModal()
-
- // Setup routes
addRoute('/', renderHomePage)
addRoute('/about', renderAboutPage)
addRoute('/contributing', renderContributingPage)
- // Load initial app structure
const app = document.querySelector('#app')
+ if (!app) return
+
app.innerHTML = `
${renderHeader()}
${renderFooter(APP_VERSION)}
`
- // Initialize i18n and theme
applyTranslations()
updateThemeIcon()
bindThemeToggle()
@@ -73,13 +108,9 @@ function initApp() {
bindMobileMenu()
updateActiveNavLink()
- // Initialize router
initRouter()
- // Load data once for the app
- fetchData().then(data => {
- appData = data
- }).catch(err => {
+ ensureDataLoaded().catch((err) => {
console.error('Failed to load app data:', err)
})
}
@@ -90,20 +121,19 @@ function renderHomePage() {
pageContent.innerHTML = renderMain()
updateActiveNavLink()
-
- // Bind anchor selection
bindAnchorSelection()
- // Initialize card grid
- if (appData) {
- initCardGridVisualization()
- } else {
- // If data not loaded yet, wait for it
- fetchData().then(data => {
- appData = data
+ ensureDataLoaded()
+ .then(() => {
initCardGridVisualization()
})
- }
+ .catch((err) => {
+ console.error('Failed to initialize home page:', err)
+ const container = document.getElementById('main-content')
+ if (container) {
+ container.innerHTML = '
Failed to load anchors. Please try again later.
'
+ }
+ })
}
function renderAboutPage() {
@@ -126,7 +156,7 @@ function renderContributingPage() {
function updateActiveNavLink() {
const currentRoute = window.location.hash.slice(1) || '/'
- document.querySelectorAll('.nav-link').forEach(link => {
+ document.querySelectorAll('.nav-link').forEach((link) => {
const route = link.dataset.route
if (route === currentRoute) {
link.classList.add('font-semibold', 'text-[var(--color-text)]')
@@ -139,89 +169,63 @@ function updateActiveNavLink() {
}
function bindAnchorSelection() {
- // Remove existing listener if any
document.removeEventListener('anchor-selected', handleAnchorSelection)
- // Add listener
document.addEventListener('anchor-selected', handleAnchorSelection)
}
function handleAnchorSelection(event) {
const { anchorId } = event.detail
- showAnchorDetails(anchorId)
+ getAnchorModalModule().then(({ showAnchorDetails }) => showAnchorDetails(anchorId))
}
-async function initCardGridVisualization() {
- try {
- const data = await fetchData()
-
- // Render card grid
- const container = document.getElementById('main-content')
- if (container) {
- container.innerHTML = renderCardGrid(data.categories, data.anchors)
- }
+function initCardGridVisualization() {
+ if (!appData) return
- // Initialize card event handlers
- initCardGrid()
+ const container = document.getElementById('main-content')
+ if (container) {
+ container.innerHTML = renderCardGrid(appData.categories, appData.anchors)
+ }
- // Initialize anchor count
- updateAnchorCount(data.anchors.length, data.anchors.length)
+ initCardGrid()
+ updateAnchorCount(appData.anchors.length, appData.anchors.length)
- // Build search index in background
- buildSearchIndex(data.anchors).then(() => {
- console.log('✓ Full-text search ready')
- // Show indicator that search is ready
- const searchInput = document.getElementById('search-input')
- if (searchInput) {
- searchInput.placeholder = i18n.t('search.placeholder') + ' (full-text)'
- }
- }).catch(err => {
- console.warn('Search index build failed:', err)
- })
+ bindRoleFilter()
+ bindSearchInput()
+}
- // Bind role filter
- const roleFilter = document.getElementById('role-filter')
- if (roleFilter && data.roles) {
- // Clear existing options (except the first "All Roles" option)
- while (roleFilter.options.length > 1) {
- roleFilter.remove(1)
- }
+function bindRoleFilter() {
+ const roleFilter = document.getElementById('role-filter')
+ if (!roleFilter || !appData?.roles) return
- // Add role options
- data.roles.forEach(role => {
- const option = document.createElement('option')
- option.value = role.id
- option.textContent = role.name
- roleFilter.appendChild(option)
- })
+ while (roleFilter.options.length > 1) {
+ roleFilter.remove(1)
+ }
- // Remove existing listener and add new one
- const newRoleFilter = roleFilter.cloneNode(true)
- roleFilter.parentNode.replaceChild(newRoleFilter, roleFilter)
+ appData.roles.forEach((role) => {
+ const option = document.createElement('option')
+ option.value = role.id
+ option.textContent = role.name
+ roleFilter.appendChild(option)
+ })
- newRoleFilter.addEventListener('change', (e) => {
- const searchQuery = document.getElementById('search-input')?.value || ''
- applyCardFilters(e.target.value, searchQuery)
- })
- }
+ roleFilter.onchange = (e) => {
+ const searchQuery = document.getElementById('search-input')?.value || ''
+ applyCardFilters(e.target.value, searchQuery)
+ }
+}
- // Bind search input
- const searchInput = document.getElementById('search-input')
- if (searchInput) {
- // Remove existing listener and add new one
- const newSearchInput = searchInput.cloneNode(true)
- searchInput.parentNode.replaceChild(newSearchInput, searchInput)
+function bindSearchInput() {
+ const searchInput = document.getElementById('search-input')
+ if (!searchInput) return
- newSearchInput.addEventListener('input', (e) => {
- const roleId = document.getElementById('role-filter')?.value || ''
- applyCardFilters(roleId, e.target.value)
- })
- }
- } catch (err) {
- console.error('Failed to initialize card grid:', err)
- const container = document.getElementById('main-content')
- if (container) {
- container.innerHTML = '
Failed to load anchors. Please try again later.
'
+ searchInput.oninput = (e) => {
+ const query = e.target.value
+ if (query.trim()) {
+ triggerSearchIndexBuild()
}
+
+ const roleId = document.getElementById('role-filter')?.value || ''
+ applyCardFilters(roleId, query)
}
}
@@ -261,37 +265,26 @@ function bindLanguageToggle() {
updateThemeIcon()
})
- // Listen for language changes to reload content
document.addEventListener('langchange', handleLanguageChange)
}
function handleLanguageChange() {
const currentRoute = window.location.hash.slice(1) || '/'
- // Reload documentation pages
if (currentRoute === '/about') {
loadDocContent('docs/about.adoc')
} else if (currentRoute === '/contributing') {
loadDocContent('CONTRIBUTING.adoc')
} else if (currentRoute === '/') {
- // Re-render card grid with updated translations
- if (appData) {
- const container = document.getElementById('main-content')
- if (container) {
- container.innerHTML = renderCardGrid(appData.categories, appData.anchors)
- initCardGrid()
- }
- }
+ initCardGridVisualization()
}
- // Reload anchor modal content if it's open
const modal = document.getElementById('anchor-modal')
if (modal && !modal.classList.contains('hidden')) {
- // Get the current anchor ID from the modal title or content
- // For now, we'll close the modal as we don't have a way to track the current anchor ID
- // TODO: Track current anchor ID for reload on language change
- const closeEvent = new Event('click')
- modal.querySelector('#modal-close')?.dispatchEvent(closeEvent)
+ const currentAnchor = modal.dataset.currentAnchor
+ if (currentAnchor) {
+ getAnchorModalModule().then(({ loadAnchorContent }) => loadAnchorContent(currentAnchor))
+ }
}
}
@@ -303,20 +296,18 @@ function bindMobileMenu() {
menuToggle.addEventListener('click', () => {
const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true'
- menuToggle.setAttribute('aria-expanded', !isExpanded)
+ menuToggle.setAttribute('aria-expanded', String(!isExpanded))
mobileMenu.classList.toggle('hidden')
})
- // Close menu when a link is clicked
const mobileNavLinks = document.querySelectorAll('.mobile-nav-link')
- mobileNavLinks.forEach(link => {
+ mobileNavLinks.forEach((link) => {
link.addEventListener('click', () => {
menuToggle.setAttribute('aria-expanded', 'false')
mobileMenu.classList.add('hidden')
})
})
- // Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!menuToggle.contains(e.target) && !mobileMenu.contains(e.target)) {
if (!mobileMenu.classList.contains('hidden')) {
diff --git a/website/src/styles/main.css b/website/src/styles/main.css
index 7824c6a..03cae68 100644
--- a/website/src/styles/main.css
+++ b/website/src/styles/main.css
@@ -26,7 +26,7 @@
body {
margin: 0;
- font-family: Inter, system-ui, -apple-system, sans-serif;
+ font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--color-bg);
color: var(--color-text);
transition: background-color 0.3s ease, color 0.3s ease;
@@ -47,6 +47,27 @@ body {
/* Card Grid Styles */
.card-grid-container {
@apply max-w-7xl mx-auto px-4 py-8;
+ font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+#page-content,
+#main-content,
+.anchor-card,
+.anchor-card-title,
+.anchor-card-meta,
+.anchor-card-proponents,
+.category-heading {
+ font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+}
+
+/* Force sans-serif on all card descendants to override AsciiDoc serif fonts */
+.anchor-card,
+.anchor-card *,
+.card-grid-container,
+.card-grid-container *,
+.anchor-cards-grid,
+.anchor-cards-grid * {
+ font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
.category-section {
diff --git a/website/src/translations/de.json b/website/src/translations/de.json
index 090bf12..31ee409 100644
--- a/website/src/translations/de.json
+++ b/website/src/translations/de.json
@@ -15,7 +15,7 @@
"search.placeholder": "Anchors durchsuchen...",
"filter.allRoles": "Alle Rollen",
"filter.anchors": "Anker",
- "footer.tagline": "Semantic Anchors — Gemeinsames Vokabular für LLM-Kommunikation",
+ "footer.tagline": "Semantic Anchors - Gemeinsames Vokabular für LLM-Kommunikation",
"footer.github": "GitHub",
"footer.leaveStar": "Dir gefallen die semantischen Anker? Hinterlasse einen Stern!",
"categories.communication-presentation": "Kommunikation & Präsentation",
diff --git a/website/src/translations/en.json b/website/src/translations/en.json
index b3acf4f..fc84557 100644
--- a/website/src/translations/en.json
+++ b/website/src/translations/en.json
@@ -15,7 +15,7 @@
"search.placeholder": "Search anchors...",
"filter.allRoles": "All Roles",
"filter.anchors": "anchors",
- "footer.tagline": "Semantic Anchors — Shared vocabulary for LLM communication",
+ "footer.tagline": "Semantic Anchors - Shared vocabulary for LLM communication",
"footer.github": "GitHub",
"footer.leaveStar": "Like Semantic Anchors? Leave a star!",
"categories.communication-presentation": "Communication & Presentation",
diff --git a/website/src/utils/data-loader.js b/website/src/utils/data-loader.js
index 760c594..297b2a3 100644
--- a/website/src/utils/data-loader.js
+++ b/website/src/utils/data-loader.js
@@ -66,16 +66,32 @@ export function getFilteredAnchors(anchors, roleId, searchQuery) {
return filtered
}
+let dataPromise = null
+
+async function fetchJson(path) {
+ const response = await fetch(`${import.meta.env.BASE_URL}${path}`)
+ if (!response.ok) {
+ throw new Error(`Failed to load ${path}: ${response.status}`)
+ }
+ return response.json()
+}
+
export async function fetchData() {
- const [anchorsRes, categoriesRes, rolesRes] = await Promise.all([
- fetch('./data/anchors.json'),
- fetch('./data/categories.json'),
- fetch('./data/roles.json')
- ])
+ if (!dataPromise) {
+ dataPromise = Promise.all([
+ fetchJson('data/anchors.json'),
+ fetchJson('data/categories.json'),
+ fetchJson('data/roles.json')
+ ]).then(([anchors, categories, roles]) => ({ anchors, categories, roles }))
+ .catch((error) => {
+ dataPromise = null
+ throw error
+ })
+ }
- const anchors = await anchorsRes.json()
- const categories = await categoriesRes.json()
- const roles = await rolesRes.json()
+ return dataPromise
+}
- return { anchors, categories, roles }
+export function __resetDataCacheForTests() {
+ dataPromise = null
}
diff --git a/website/src/utils/data-loader.test.js b/website/src/utils/data-loader.test.js
index 01999c4..019a630 100644
--- a/website/src/utils/data-loader.test.js
+++ b/website/src/utils/data-loader.test.js
@@ -1,5 +1,13 @@
-import { describe, it, expect } from 'vitest'
-import { buildTreemapData, getAnchorsByRole, getAnchorsBySearch, getFilteredAnchors, getCategoryColor } from './data-loader.js'
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import {
+ buildTreemapData,
+ getAnchorsByRole,
+ getAnchorsBySearch,
+ getFilteredAnchors,
+ getCategoryColor,
+ fetchData,
+ __resetDataCacheForTests
+} from './data-loader.js'
const mockCategories = [
{
@@ -203,3 +211,39 @@ describe('getFilteredAnchors', () => {
expect(result).toHaveLength(0)
})
})
+
+describe('fetchData', () => {
+ beforeEach(() => {
+ __resetDataCacheForTests()
+ global.fetch = vi.fn()
+ })
+
+ afterEach(() => {
+ delete global.fetch
+ })
+
+ it('loads all datasets once and caches the result', async () => {
+ global.fetch
+ .mockResolvedValueOnce({ ok: true, json: async () => mockAnchors })
+ .mockResolvedValueOnce({ ok: true, json: async () => mockCategories })
+ .mockResolvedValueOnce({ ok: true, json: async () => mockRoles })
+
+ const first = await fetchData()
+ const second = await fetchData()
+
+ expect(first).toEqual(second)
+ expect(global.fetch).toHaveBeenCalledTimes(3)
+ expect(global.fetch).toHaveBeenNthCalledWith(1, expect.stringContaining('data/anchors.json'))
+ expect(global.fetch).toHaveBeenNthCalledWith(2, expect.stringContaining('data/categories.json'))
+ expect(global.fetch).toHaveBeenNthCalledWith(3, expect.stringContaining('data/roles.json'))
+ })
+
+ it('throws on non-success responses', async () => {
+ global.fetch
+ .mockResolvedValueOnce({ ok: false, status: 500 })
+ .mockResolvedValueOnce({ ok: true, json: async () => mockCategories })
+ .mockResolvedValueOnce({ ok: true, json: async () => mockRoles })
+
+ await expect(fetchData()).rejects.toThrow('Failed to load data/anchors.json: 500')
+ })
+})
diff --git a/website/src/utils/router.test.js b/website/src/utils/router.test.js
new file mode 100644
index 0000000..ef19753
--- /dev/null
+++ b/website/src/utils/router.test.js
@@ -0,0 +1,31 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { addRoute, getCurrentRoute, initRouter, navigate } from './router.js'
+
+describe('router', () => {
+ beforeEach(() => {
+ window.location.hash = '#/'
+ })
+
+ it('returns current route from hash', () => {
+ window.location.hash = '#/about'
+ expect(getCurrentRoute()).toBe('/about')
+ })
+
+ it('navigates to a route via hash', () => {
+ navigate('/contributing')
+ expect(window.location.hash).toBe('#/contributing')
+ })
+
+ it('runs registered handler on init and hash changes', async () => {
+ const handler = vi.fn()
+ addRoute('/router-test', handler)
+ window.location.hash = '#/router-test'
+
+ initRouter()
+ expect(handler).toHaveBeenCalledTimes(1)
+
+ window.location.hash = '#/router-test'
+ window.dispatchEvent(new HashChangeEvent('hashchange'))
+ expect(handler).toHaveBeenCalledTimes(2)
+ })
+})
diff --git a/website/src/utils/search-index.js b/website/src/utils/search-index.js
index 0c804fb..588d912 100644
--- a/website/src/utils/search-index.js
+++ b/website/src/utils/search-index.js
@@ -5,43 +5,58 @@
let searchIndex = new Map() // anchorId -> searchable text
let indexReady = false
+let buildingPromise = null
+
+const BATCH_SIZE = 8
/**
* Build search index by loading all anchor .adoc files
*/
export async function buildSearchIndex(anchors) {
- console.log('Building search index for', anchors.length, 'anchors...')
-
- const promises = anchors.map(async (anchor) => {
- try {
- const response = await fetch(`${import.meta.env.BASE_URL}docs/anchors/${anchor.id}.adoc`)
- if (!response.ok) {
- console.warn(`Failed to load ${anchor.id}.adoc for indexing`)
- return
- }
+ if (indexReady) return
+ if (buildingPromise) return buildingPromise
- const content = await response.text()
-
- // Extract searchable text (remove AsciiDoc markup)
- const searchableText = extractSearchableText(content)
+ console.log('Building search index for', anchors.length, 'anchors...')
- // Store in index
- searchIndex.set(anchor.id, {
- title: anchor.title,
- content: searchableText,
- tags: anchor.tags || [],
- proponents: anchor.proponents || [],
- roles: anchor.roles || [],
- categories: anchor.categories || []
+ buildingPromise = (async () => {
+ for (let i = 0; i < anchors.length; i += BATCH_SIZE) {
+ const batch = anchors.slice(i, i + BATCH_SIZE)
+ const tasks = batch.map(async (anchor) => {
+ try {
+ const response = await fetch(`${import.meta.env.BASE_URL}docs/anchors/${anchor.id}.adoc`)
+ if (!response.ok) {
+ console.warn(`Failed to load ${anchor.id}.adoc for indexing`)
+ return
+ }
+
+ const content = await response.text()
+ const searchableText = extractSearchableText(content)
+
+ searchIndex.set(anchor.id, {
+ title: anchor.title,
+ content: searchableText,
+ tags: anchor.tags || [],
+ proponents: anchor.proponents || [],
+ roles: anchor.roles || [],
+ categories: anchor.categories || []
+ })
+ } catch (error) {
+ console.warn(`Error indexing ${anchor.id}:`, error)
+ }
})
- } catch (error) {
- console.warn(`Error indexing ${anchor.id}:`, error)
+
+ await Promise.allSettled(tasks)
}
- })
- await Promise.all(promises)
- indexReady = true
- console.log('Search index built:', searchIndex.size, 'anchors indexed')
+ indexReady = true
+ console.log('Search index built:', searchIndex.size, 'anchors indexed')
+ })()
+
+ try {
+ await buildingPromise
+ } finally {
+ buildingPromise = null
+ }
}
/**
@@ -153,9 +168,19 @@ export function isIndexReady() {
return indexReady
}
+export function isIndexBuilding() {
+ return Boolean(buildingPromise)
+}
+
/**
* Get indexed content for an anchor (for debugging)
*/
export function getIndexedContent(anchorId) {
return searchIndex.get(anchorId)
}
+
+export function __resetSearchIndexForTests() {
+ searchIndex = new Map()
+ indexReady = false
+ buildingPromise = null
+}
diff --git a/website/src/utils/search-index.test.js b/website/src/utils/search-index.test.js
new file mode 100644
index 0000000..15a9fa1
--- /dev/null
+++ b/website/src/utils/search-index.test.js
@@ -0,0 +1,41 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
+import {
+ buildSearchIndex,
+ search,
+ isIndexReady,
+ isIndexBuilding,
+ __resetSearchIndexForTests
+} from './search-index.js'
+
+describe('search-index', () => {
+ beforeEach(() => {
+ __resetSearchIndexForTests()
+ global.fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ text: async () => '= Test Anchor\n\nA practical testing method for teams.'
+ })
+ })
+
+ afterEach(() => {
+ delete global.fetch
+ })
+
+ it('builds index and enables full-text search', async () => {
+ const anchors = [
+ { id: 'tdd-london-school', title: 'TDD London', tags: ['testing'], proponents: ['Steve Freeman'] },
+ { id: 'clean-architecture', title: 'Clean Architecture', tags: ['architecture'], proponents: ['Robert Martin'] }
+ ]
+
+ await buildSearchIndex(anchors)
+
+ expect(isIndexReady()).toBe(true)
+ expect(isIndexBuilding()).toBe(false)
+ expect(search('london')).toContain('tdd-london-school')
+ expect(search('robert')).toContain('clean-architecture')
+ })
+
+ it('returns empty results when index is not ready', () => {
+ expect(isIndexReady()).toBe(false)
+ expect(search('anything')).toEqual([])
+ })
+})
diff --git a/website/tests/e2e/website.spec.js b/website/tests/e2e/website.spec.js
index 81c2e69..3ef039b 100644
--- a/website/tests/e2e/website.spec.js
+++ b/website/tests/e2e/website.spec.js
@@ -1,10 +1,8 @@
import { test, expect } from '@playwright/test'
-const WEBSITE_URL = 'https://raifdmueller.github.io/Semantic-Anchors/'
-
test.describe('Homepage - Card Grid', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
})
test('should load homepage successfully', async ({ page }) => {
@@ -62,7 +60,6 @@ test.describe('Homepage - Card Grid', () => {
// Select a role
await page.selectOption('#role-filter', 'software-developer')
- await page.waitForTimeout(500)
// Some cards should still be visible
const filteredCount = await page.locator('.anchor-card:visible').count()
@@ -74,7 +71,6 @@ test.describe('Homepage - Card Grid', () => {
// Type in search
await page.fill('#search-input', 'TDD')
- await page.waitForTimeout(500)
// Some cards should be visible with TDD
const visibleCards = await page.locator('.anchor-card:visible').count()
@@ -82,7 +78,6 @@ test.describe('Homepage - Card Grid', () => {
// Clear search
await page.fill('#search-input', '')
- await page.waitForTimeout(500)
// All cards should be visible again
const allCards = await page.locator('.anchor-card:visible').count()
@@ -100,7 +95,6 @@ test.describe('Homepage - Card Grid', () => {
await page.locator('.anchor-card').first().click()
// Wait for modal to open
- await page.waitForTimeout(1000)
// Modal should be visible
await expect(modal).not.toHaveClass(/hidden/)
@@ -114,14 +108,12 @@ test.describe('Homepage - Card Grid', () => {
// Open modal
await page.locator('.anchor-card').first().click()
- await page.waitForTimeout(1000)
const modal = page.locator('#anchor-modal')
await expect(modal).not.toHaveClass(/hidden/)
// Click close button
await page.click('#modal-close')
- await page.waitForTimeout(500)
// Modal should be hidden
await expect(modal).toHaveClass(/hidden/)
@@ -132,14 +124,12 @@ test.describe('Homepage - Card Grid', () => {
// Open modal
await page.locator('.anchor-card').first().click()
- await page.waitForTimeout(1000)
const modal = page.locator('#anchor-modal')
await expect(modal).not.toHaveClass(/hidden/)
// Press Escape
await page.keyboard.press('Escape')
- await page.waitForTimeout(500)
// Modal should be hidden
await expect(modal).toHaveClass(/hidden/)
@@ -155,13 +145,10 @@ test.describe('Homepage - Card Grid', () => {
await expect(editBtn).toHaveAttribute('href', /github\.com.*edit/)
})
- test('should display documentation links', async ({ page }) => {
- // Check for Documentation link
- const docLink = page.locator('a[href*="README.adoc"]')
- await expect(docLink).toBeVisible()
- await expect(docLink).toContainText('Documentation')
+ test('should display action links', async ({ page }) => {
+ const aboutLink = page.locator('a[href="#/about"]').first()
+ await expect(aboutLink).toBeVisible()
- // Check for Propose New Anchor link
const proposeLink = page.locator('a[href*="issues/new"]')
await expect(proposeLink).toBeVisible()
await expect(proposeLink).toContainText('Propose New Anchor')
@@ -170,7 +157,7 @@ test.describe('Homepage - Card Grid', () => {
test.describe('Theme and Language', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
})
test('should toggle theme', async ({ page }) => {
@@ -179,7 +166,6 @@ test.describe('Theme and Language', () => {
// Click theme toggle
await page.click('#theme-toggle')
- await page.waitForTimeout(500)
// Theme should have changed
const newClass = await html.getAttribute('class')
@@ -199,7 +185,6 @@ test.describe('Theme and Language', () => {
// Click language toggle
await langToggle.click()
- await page.waitForTimeout(500)
// Language should change
const newLang = await langToggle.textContent()
@@ -209,14 +194,12 @@ test.describe('Theme and Language', () => {
test('should persist theme across page reloads', async ({ page }) => {
// Switch to dark mode
await page.click('#theme-toggle')
- await page.waitForTimeout(500)
const html = page.locator('html')
await expect(html).toHaveClass(/dark/)
// Reload page
await page.reload()
- await page.waitForTimeout(500)
// Should still be dark
await expect(html).toHaveClass(/dark/)
@@ -225,13 +208,12 @@ test.describe('Theme and Language', () => {
test.describe('Routing - Documentation Pages', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
})
test('should navigate to About page', async ({ page }) => {
// Click About link
await page.click('a[data-route="/about"]')
- await page.waitForTimeout(1000)
// URL should update
expect(page.url()).toContain('#/about')
@@ -248,7 +230,6 @@ test.describe('Routing - Documentation Pages', () => {
test('should navigate to Contributing page', async ({ page }) => {
// Click Contributing link
await page.click('a[data-route="/contributing"]')
- await page.waitForTimeout(1000)
// URL should update
expect(page.url()).toContain('#/contributing')
@@ -265,11 +246,9 @@ test.describe('Routing - Documentation Pages', () => {
test('should navigate back to Catalog from About', async ({ page }) => {
// Go to About
await page.click('a[data-route="/about"]')
- await page.waitForTimeout(1000)
// Go back to Catalog
await page.click('a[data-route="/"]')
- await page.waitForTimeout(1000)
// URL should be home
expect(page.url()).toMatch(/#\/$|#$/)
@@ -284,8 +263,7 @@ test.describe('Routing - Documentation Pages', () => {
test('should handle direct URL to About page', async ({ page }) => {
// Navigate directly to About
- await page.goto(WEBSITE_URL + '#/about')
- await page.waitForTimeout(1000)
+ await page.goto('/#/about')
// About content should be visible
await expect(page.locator('#doc-content')).toBeVisible()
@@ -295,11 +273,9 @@ test.describe('Routing - Documentation Pages', () => {
test('should handle browser back button', async ({ page }) => {
// Navigate to About
await page.click('a[data-route="/about"]')
- await page.waitForTimeout(1000)
// Go back
await page.goBack()
- await page.waitForTimeout(1000)
// Should be on home
await expect(page.locator('.anchor-card').first()).toBeVisible()
@@ -308,7 +284,7 @@ test.describe('Routing - Documentation Pages', () => {
test.describe('Responsive Design', () => {
test('should work on mobile viewport', async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
await page.setViewportSize({ width: 375, height: 667 })
// Page should still be visible
@@ -323,7 +299,7 @@ test.describe('Responsive Design', () => {
})
test('should work on tablet viewport', async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
await page.setViewportSize({ width: 768, height: 1024 })
await expect(page.locator('h1')).toBeVisible()
@@ -335,7 +311,7 @@ test.describe('Responsive Design', () => {
})
test('should work on desktop viewport', async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
await page.setViewportSize({ width: 1920, height: 1080 })
await expect(page.locator('h1')).toBeVisible()
@@ -346,7 +322,7 @@ test.describe('Responsive Design', () => {
test.describe('Accessibility', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
})
test('should have accessible navigation', async ({ page }) => {
@@ -371,7 +347,6 @@ test.describe('Accessibility', () => {
// Press Enter to open modal
await page.keyboard.press('Enter')
- await page.waitForTimeout(1000)
// Modal should open
const modal = page.locator('#anchor-modal')
@@ -391,7 +366,7 @@ test.describe('Accessibility', () => {
test.describe('Performance', () => {
test.beforeEach(async ({ page }) => {
- await page.goto(WEBSITE_URL)
+ await page.goto('/')
})
test('should load all required assets', async ({ page }) => {
@@ -418,13 +393,8 @@ test.describe('Performance', () => {
test('should load search index asynchronously', async ({ page }) => {
await page.waitForSelector('.anchor-card', { timeout: 10000 })
-
- // Wait for search to be ready
- await page.waitForTimeout(3000)
-
- // Search input placeholder should indicate full-text search
const searchInput = page.locator('#search-input')
- const placeholder = await searchInput.getAttribute('placeholder')
- expect(placeholder).toContain('full-text')
+ await searchInput.fill('tdd')
+ await expect(searchInput).toHaveAttribute('placeholder', /full-text/, { timeout: 15000 })
})
})
diff --git a/website/vite.config.js b/website/vite.config.js
index a45193a..88360b6 100644
--- a/website/vite.config.js
+++ b/website/vite.config.js
@@ -9,13 +9,20 @@ export default defineConfig({
build: {
outDir: 'dist',
emptyOutDir: true,
+ chunkSizeWarningLimit: 800,
rollupOptions: {
output: {
- manualChunks: {
- echarts: ['echarts']
- }
- }
- }
+ manualChunks(id) {
+ if (id.includes('node_modules/@asciidoctor/core')) {
+ return 'asciidoctor-core'
+ }
+ if (id.includes('node_modules/asciidoctor-opal-runtime')) {
+ return 'asciidoctor-opal'
+ }
+ return undefined
+ },
+ },
+ },
},
server: {
port: 5173,
@@ -23,5 +30,7 @@ export default defineConfig({
},
test: {
environment: 'jsdom',
+ include: ['src/**/*.{test,spec}.js', 'src/**/__tests__/**/*.js'],
+ exclude: ['tests/e2e/**', 'node_modules/**', 'dist/**'],
},
})