passmaster/tests/e2e/canonical.spec.ts

161 lines
5.5 KiB
TypeScript

import { test, expect } from '@playwright/test'
test.describe('Canonical Links AEO Tests', () => {
test('should have exactly one canonical link on each page', async ({ page }) => {
const pages = [
'/',
'/offline',
'/client-side',
'/exclude-similar',
'/privacy'
]
for (const pagePath of pages) {
await page.goto(pagePath)
// Find all canonical links
const canonicalLinks = await page.locator('link[rel="canonical"]').all()
// Should have exactly one canonical link
expect(canonicalLinks.length).toBe(1)
const canonicalHref = await canonicalLinks[0].getAttribute('href')
// Should have valid URL
expect(canonicalHref).toBeTruthy()
expect(() => new URL(canonicalHref!)).not.toThrow()
// Should use HTTPS
expect(canonicalHref).toMatch(/^https:\/\//)
// Should contain correct domain
expect(canonicalHref).toContain('passmaster.app')
// Should match expected path
const url = new URL(canonicalHref!)
if (pagePath === '/') {
expect(url.pathname).toBe('/')
} else {
expect(url.pathname).toBe(pagePath)
}
// Should not have trailing slash (except root)
if (pagePath !== '/') {
expect(url.pathname).not.toMatch(/\/$/)
}
// Should not have query parameters
expect(url.search).toBe('')
// Should not have fragment
expect(url.hash).toBe('')
console.log(`${pagePath}: ${canonicalHref}`)
}
})
test('should have canonical in metadata API', async ({ page }) => {
const pages = ['/', '/offline', '/client-side', '/exclude-similar']
for (const pagePath of pages) {
await page.goto(pagePath)
// Check if canonical is properly set in head
const canonical = await page.locator('head link[rel="canonical"]').first()
expect(canonical).toBeTruthy()
const href = await canonical.getAttribute('href')
expect(href).toBeTruthy()
expect(href).toMatch(/^https:\/\/passmaster\.app/)
}
})
test('should handle URL normalization correctly', async ({ page }) => {
// Test with various URL formats
const testCases = [
{ path: '/', expected: 'https://passmaster.app/' },
{ path: '/offline', expected: 'https://passmaster.app/offline' },
{ path: '/offline/', expected: 'https://passmaster.app/offline' }, // Should remove trailing slash
{ path: '/offline?utm_source=test', expected: 'https://passmaster.app/offline' }, // Should remove UTM params
]
for (const testCase of testCases) {
await page.goto(testCase.path)
const canonical = await page.locator('link[rel="canonical"]').first()
const href = await canonical.getAttribute('href')
expect(href).toBe(testCase.expected)
}
})
test('should not have multiple canonical declarations', async ({ page }) => {
const pages = ['/', '/offline', '/client-side', '/exclude-similar', '/privacy']
for (const pagePath of pages) {
await page.goto(pagePath)
// Check for canonical in link tags
const linkCanonicals = await page.locator('link[rel="canonical"]').all()
expect(linkCanonicals.length).toBeLessThanOrEqual(1)
// Check for canonical in HTTP headers (if any)
const response = await page.goto(pagePath)
const linkHeader = response?.headers()['link']
if (linkHeader) {
const canonicalInHeader = linkHeader.includes('rel="canonical"')
// If canonical in header, should not also be in HTML (or vice versa)
if (canonicalInHeader) {
expect(linkCanonicals.length).toBe(0)
}
}
}
})
test('should have consistent canonical URLs across navigation', async ({ page }) => {
// Navigate to page directly
await page.goto('/offline')
const directCanonical = await page.locator('link[rel="canonical"]').getAttribute('href')
// Navigate to page via homepage
await page.goto('/')
await page.click('a[href="/offline"]')
await page.waitForLoadState('networkidle')
const navigatedCanonical = await page.locator('link[rel="canonical"]').getAttribute('href')
// Should have same canonical URL regardless of how we arrived
expect(directCanonical).toBe(navigatedCanonical)
})
test('should handle special characters in URLs', async ({ page }) => {
await page.goto('/exclude-similar')
const canonical = await page.locator('link[rel="canonical"]').first()
const href = await canonical.getAttribute('href')
// Should properly encode URL
expect(href).toBeTruthy()
expect(() => new URL(href!)).not.toThrow()
const url = new URL(href!)
expect(url.pathname).toBe('/exclude-similar')
})
test('should not have self-referential canonical issues', async ({ page }) => {
const pages = ['/', '/offline', '/client-side']
for (const pagePath of pages) {
await page.goto(pagePath)
const canonical = await page.locator('link[rel="canonical"]').getAttribute('href')
const currentUrl = new URL(page.url())
const canonicalUrl = new URL(canonical!)
// Canonical should match current page path (normalized)
expect(canonicalUrl.pathname).toBe(currentUrl.pathname === '/' ? '/' : currentUrl.pathname.replace(/\/$/, ''))
expect(canonicalUrl.hostname).toBe('passmaster.app') // Should use production domain
}
})
})