Init Commit

This commit is contained in:
2026-05-18 11:46:02 +02:00
commit 2de3502fbc
382 changed files with 19583 additions and 0 deletions
+159
View File
@@ -0,0 +1,159 @@
import { test, expect } from '@playwright/test';
const BASE_URL: string = process.env.TEST_BASE_URL ?? 'http://localhost:1313';
if (!BASE_URL.startsWith('http')) {
throw new Error('TEST_BASE_URL must be a valid URL starting with http:// or https://');
}
console.log(`Running tests against ${BASE_URL}`);
test.describe('Theme basic functionality', () => {
test.beforeAll(async () => {
// Health check
try {
await fetch(BASE_URL);
} catch (error) {
console.error(`Failed to connect to ${BASE_URL}. Is the Hugo server running?`);
throw error;
}
});
test('homepage loads correctly', async ({ page }) => {
await page.goto(BASE_URL);
await expect(page).toHaveTitle(/Adritian/);
});
test('theme switcher works', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// set the browser theme preference to "light"
// Emulate dark color scheme
await page.emulateMedia({ colorScheme: 'dark' });
await page.goto(BASE_URL);
// attribute exists
await expect(page.locator('html')).toHaveAttribute('data-bs-theme');
// click on theme switcher
await page.click('div#footer-color-selector button.bd-theme-selector');
/* click on the html element:
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
☀️ Light
</button>
*/
await page.getByRole('button', { name: '☀️ Light' }).last().click();
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', 'light');
// click on theme switcher
await page.click('div#footer-color-selector button.bd-theme-selector');
/* click on the html element:
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="dark" aria-pressed="true">
🌑 Dark
<span class="theme-icon dark d-none" aria-hidden="true"></span>
</button>
*/
await expect(page.getByText('🌑 Dark').last()).toBeVisible();
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', 'light');
});
test('navigation is visible', async ({ page }) => {
await page.goto(BASE_URL);
await expect(page.locator('nav.navbar')).toBeVisible();
});
test('footer links work', async ({ page }) => {
await page.goto(BASE_URL);
await expect(page.locator('.footer_links')).toBeVisible();
});
test('skip to content link works', async ({ page }) => {
// Navigate to a content page
await page.goto(`${BASE_URL}/blog`);
// The skip link should be initially hidden but in the DOM
const skipLink = page.locator('.skip-to-content-link');
await expect(skipLink).toBeAttached();
// Focus the skip link (simulates tabbing to it)
await skipLink.focus();
// Now it should be visible
await expect(skipLink).toBeVisible();
// Press Enter key on the skip link
await skipLink.press('Enter');
// Verify we jumped to the main content
// Check URL hash
await expect(page).toHaveURL(/#main-content$/);
// Verify focus is moved to main content
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeFocused();
});
});
test('footer_right should contain exactly 2 dropdown element for language and theme', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
await page.goto(`${BASE_URL}/disable-menu/`);
// Verify footer and footer_right exist
await expect(page.locator('footer')).toBeAttached();
await expect(page.locator('footer .footer_right')).toBeAttached();
// Verify exactly 2 dropdown elements exist within footer_right
await expect(page.locator('footer .footer_right .dropdown')).toHaveCount(2);
});
test('should load all homepage images correctly', async ({ page }) => {
await page.goto('http://localhost:1313');
// Wait for network to be idle (most images loaded)
await page.waitForLoadState('networkidle');
// Helper function to check images - handles both regular and lazy-loaded images
const checkImageElements = async (selector: string) => {
const elements = page.locator(selector);
const count = await elements.count();
console.log(`Checking images with selector "${selector}" - found ${count} elements`);
await expect(count).toBeGreaterThan(0);
if (count === 0) throw new Error(`Expected to find at least one element matching "${selector}"`);
for (let i = 0; i < count; i++) {
const element = elements.nth(i);
await expect(element).toBeVisible();
const isLazyLoaded = await element.evaluate(el =>
el.classList.contains('lozad') || el.hasAttribute('data-src'));
if (isLazyLoaded) {
// For lazy-loaded images, check that data attributes exist
const dataSrc = await element.getAttribute('data-src');
expect(dataSrc).toBeTruthy();
} else {
// For regular images, check naturalWidth
const naturalWidth = await element.evaluate(el => (el as HTMLImageElement).naturalWidth);
await expect(naturalWidth).toBeGreaterThan(0);
if (naturalWidth === 0) throw new Error('Image should have loaded (naturalWidth > 0)');
}
}
};
// Check all required image elements
await checkImageElements('.profile-image img');
// .image-left-overflow might be an element with background image or contain an img
const leftOverflowElement = page.locator('.image-left-overflow').first();
await expect(leftOverflowElement).toBeVisible();
// Check if it contains any img elements
const hasImg = await page.locator('.image-left-overflow img').count() > 0;
if (hasImg) {
await checkImageElements('.image-left-overflow img');
}
await checkImageElements('.client-works-container picture img');
await checkImageElements('.testimonial__author .picture img');
});
@@ -0,0 +1,364 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('New Blog Features', () => {
test.describe('Related Posts', () => {
test('should display related posts section with heading, list, and 3 post items including title, excerpt, date, and tags', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Wait for page to load
await page.waitForLoadState('networkidle');
// Check for related posts section
const relatedPostsSection = page.locator('.related-posts');
await expect(relatedPostsSection).toBeVisible();
// Check heading
await expect(relatedPostsSection.locator('h3')).toContainText('Related Posts');
// Check that we have related posts displayed
const relatedPostsList = relatedPostsSection.locator('.related-posts-list');
await expect(relatedPostsList).toBeVisible();
// Check that at least one related post item exists
const relatedPostItems = relatedPostsList.locator('.related-post-item');
await expect(relatedPostItems).toHaveCount(3); // Should show 3 related posts
// Verify each related post has required elements
const firstPost = relatedPostItems.first();
await expect(firstPost.locator('h4')).toBeVisible();
await expect(firstPost.locator('.related-post-excerpt')).toBeVisible();
await expect(firstPost.locator('.related-post-meta .post-date')).toBeVisible();
});
test('should navigate to correct blog post URL when clicking on a related post item', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Click on the first related post
await page.locator('.related-post-item').first().click();
// Should navigate to a different blog post
await expect(page).toHaveURL(/\/blog\/.+/);
await expect(page.locator('h1')).toBeVisible();
});
test('should display at least one tag on related posts items', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check that tags are displayed in related posts
const tagsInRelatedPosts = page.locator('.related-post-item .post-tags .tag');
const tagCount = await tagsInRelatedPosts.count();
// At least some posts should have tags
expect(tagCount).toBeGreaterThan(0);
});
});
test.describe('Social Sharing Buttons', () => {
test('should display social sharing section with "Share this post" heading', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const sharingSection = page.locator('.social-sharing');
await expect(sharingSection).toBeVisible();
// Check heading
await expect(sharingSection.locator('h3')).toContainText('Share this post');
});
test('should display sharing buttons for Twitter, LinkedIn, Facebook, and Email', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const sharingButtons = page.locator('.share-buttons');
await expect(sharingButtons).toBeVisible();
// Check for each platform's button
await expect(sharingButtons.locator('.share-twitter')).toBeVisible();
await expect(sharingButtons.locator('.share-linkedin')).toBeVisible();
await expect(sharingButtons.locator('.share-facebook')).toBeVisible();
await expect(sharingButtons.locator('.share-email')).toBeVisible();
});
test('should have correct share URLs for Twitter, LinkedIn, Facebook, and Email with proper parameters', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check Twitter share link
const twitterButton = page.locator('.share-twitter');
const twitterHref = await twitterButton.getAttribute('href');
expect(twitterHref).toContain('twitter.com/intent/tweet');
expect(twitterHref).toContain('url=');
// Check LinkedIn share link
const linkedinButton = page.locator('.share-linkedin');
const linkedinHref = await linkedinButton.getAttribute('href');
expect(linkedinHref).toContain('linkedin.com/sharing/share-offsite');
// Check Facebook share link
const facebookButton = page.locator('.share-facebook');
const facebookHref = await facebookButton.getAttribute('href');
expect(facebookHref).toContain('facebook.com/sharer');
// Check Email share link
const emailButton = page.locator('.share-email');
const emailHref = await emailButton.getAttribute('href');
expect(emailHref).toContain('mailto:');
});
test('should have accessible ARIA labels on all sharing buttons for screen readers', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check ARIA labels for accessibility
await expect(page.locator('.share-twitter')).toHaveAttribute('aria-label', /Share on Twitter/i);
await expect(page.locator('.share-linkedin')).toHaveAttribute('aria-label', /Share on LinkedIn/i);
await expect(page.locator('.share-facebook')).toHaveAttribute('aria-label', /Share on Facebook/i);
await expect(page.locator('.share-email')).toHaveAttribute('aria-label', /Share via Email/i);
});
});
test.describe('Table of Contents', () => {
test('should display table of contents section with "Table of Contents" heading when enabled in post frontmatter', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const tocSection = page.locator('.table-of-contents');
await expect(tocSection).toBeVisible();
// Check heading
await expect(tocSection.locator('h3')).toContainText('Table of Contents');
});
test('should contain navigation links with anchor href attributes pointing to page sections', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const tocNav = page.locator('.table-of-contents #TableOfContents');
await expect(tocNav).toBeVisible();
// Should have list items with links
const tocLinks = tocNav.locator('a');
const linkCount = await tocLinks.count();
expect(linkCount).toBeGreaterThan(0);
// Check first link
await expect(tocLinks.first()).toBeVisible();
const firstHref = await tocLinks.first().getAttribute('href');
expect(firstHref).toMatch(/^#/); // Should be anchor links
});
test('should scroll to corresponding section and update URL hash when clicking on table of contents links', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Click on a TOC link
const tocLinks = page.locator('.table-of-contents #TableOfContents a');
const firstLink = tocLinks.first();
const linkText = await firstLink.textContent();
await firstLink.click();
// URL should have the hash
await expect(page).toHaveURL(/#.+/);
// Wait a bit for smooth scrolling
await page.waitForTimeout(500);
});
test('should have sticky positioning class when tocSticky is enabled in post frontmatter', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const tocSection = page.locator('.table-of-contents');
// Check if it has the sticky class (post has tocSticky: true)
await expect(tocSection).toHaveClass(/toc-sticky/);
});
});
test.describe('Enhanced Reading Metadata', () => {
test('should display estimated reading time in minutes', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for reading time
const readingTime = page.locator('#reading-time');
await expect(readingTime).toBeVisible();
await expect(readingTime).toContainText('min read');
});
test('should display total word count of the post', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for word count
const wordCount = page.locator('#wordcount');
await expect(wordCount).toBeVisible();
await expect(wordCount).toContainText('Words');
});
test('should display post publish date', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for publish date
const publishDate = page.locator('#date');
await expect(publishDate).toBeVisible();
});
test('should display last modified date with "Updated" label when lastmod is present in frontmatter', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for last modified date
const lastModified = page.locator('#last-modified');
// The demo post has lastmod, so it should be visible
await expect(lastModified).toBeVisible();
await expect(lastModified).toContainText('Updated');
});
test('should have properly structured metadata section with semantic HTML', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const metaSection = page.locator('#meta');
await expect(metaSection).toBeVisible();
// Should be a logical grouping
await expect(metaSection.locator('section')).toBeVisible();
});
});
test.describe('Comments Integration (Structure)', () => {
test('should have main content structure that supports comments section when configured', async ({ page }) => {
// Note: Since comments aren't enabled in demo, this tests the structure
// would exist if configured
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Comments section won't be visible without configuration
// but we can test that the blog single template is properly structured
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeVisible();
});
});
test.describe('Responsive Design', () => {
test('should display related posts, social sharing, and table of contents on mobile viewport (375x667)', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Related posts should adapt to mobile
const relatedPosts = page.locator('.related-posts');
await expect(relatedPosts).toBeVisible();
// Social sharing should be visible
const socialSharing = page.locator('.social-sharing');
await expect(socialSharing).toBeVisible();
// TOC should be visible but may not be sticky
const toc = page.locator('.table-of-contents');
await expect(toc).toBeVisible();
});
test('should adapt related posts grid layout across desktop (1280px), tablet (768px), and mobile (375px) viewports', async ({ page }) => {
// Test desktop width
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const relatedPostsList = page.locator('.related-posts-list');
await expect(relatedPostsList).toBeVisible();
// Test tablet width
await page.setViewportSize({ width: 768, height: 1024 });
await expect(relatedPostsList).toBeVisible();
// Test mobile width
await page.setViewportSize({ width: 375, height: 667 });
await expect(relatedPostsList).toBeVisible();
});
});
test.describe('Multilingual Support', () => {
test('should display "Idioma" button and Spanish language option in language switcher dropdown', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// Navigate to Spanish version
await page.goto(`${BASE_URL}/es/`);
// Open language switcher
const languageButton = page.locator('button', { hasText: 'Idioma' }).first();
await languageButton.click();
// Should see language options
await expect(page.locator('#languages-dropdown-header').getByText('Español')).toBeVisible();
});
});
test.describe('Accessibility', () => {
test('should use proper h3 heading hierarchy for related posts, social sharing, and table of contents sections', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check that section headings are h3
await expect(page.locator('.related-posts h3')).toBeVisible();
await expect(page.locator('.social-sharing h3')).toBeVisible();
await expect(page.locator('.table-of-contents h3')).toBeVisible();
});
test('should allow keyboard navigation and focus on social sharing buttons', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Focus first share button
const shareButtons = page.locator('.share-buttons a');
const firstButton = shareButtons.first();
await firstButton.focus();
// Should be focusable
await expect(firstButton).toBeFocused();
});
test('should allow keyboard navigation and focus on table of contents links', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Focus first TOC link
const tocLinks = page.locator('.table-of-contents a');
const firstLink = tocLinks.first();
await firstLink.focus();
// Should be focusable
await expect(firstLink).toBeFocused();
});
test('should use semantic HTML with unordered list (ul/li) elements for related posts', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Related posts should use list elements
const relatedPostsList = page.locator('.related-posts ul');
await expect(relatedPostsList).toBeVisible();
// Each item should be a list item
const listItems = relatedPostsList.locator('li');
await expect(listItems.first()).toBeVisible();
});
});
test.describe('Performance', () => {
test('should load page with all new features in under 5 seconds including network idle state', async ({ page }) => {
const startTime = Date.now();
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
// Should load in under 5 seconds
expect(loadTime).toBeLessThan(5000);
});
test('should render all feature sections without causing cumulative layout shift after page load', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Wait for page to be fully loaded
await page.waitForLoadState('networkidle');
// All main sections should be visible without scrolling
const relatedPosts = page.locator('.related-posts');
await expect(relatedPosts).toBeAttached();
});
});
});
+23
View File
@@ -0,0 +1,23 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('Blog page content', () => {
test('should show "Demo Blog" heading and "Welcome to the demo blog" text', async ({ page }) => {
await page.goto(`${BASE_URL}/blog`);
// Verify heading is displayed
await expect(page.getByRole('heading', { name: /demo blog/i })).toBeVisible();
// Verify body text is displayed
await expect(page.getByText('Welcome to the demo blog')).toBeVisible();
});
test('should navigate to the sample blog article by clicking on the post link', async ({ page }) => {
await page.goto(`${BASE_URL}/blog`);
await page.click('text=Sample content: formatting styles'); // Adjust text if needed
await expect(page).toHaveURL(`${BASE_URL}/blog/sample/`);
await expect(page.getByRole('heading', { name: /sample content/i })).toBeVisible();
});
});
+167
View File
@@ -0,0 +1,167 @@
import { test, expect } from '@playwright/test';
const BASE_URL: string = process.env.TEST_BASE_URL ?? 'http://localhost:1313';
if (!BASE_URL.startsWith('http')) {
throw new Error('TEST_BASE_URL must be a valid URL starting with http:// or https://');
}
console.log(`Running tests against ${BASE_URL}`);
test.describe('Contact Form Functionality', () => {
test.beforeEach(async ({ page }) => {
// Navigate to the page with the contact form (usually footer)
await page.goto(BASE_URL);
// Scroll to the bottom of the page where the contact form is typically located
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
});
test('contact form placeholders are visible', async ({ page }) => {
// Check that all form fields have visible placeholders
const nameInput = page.locator('form input[name="full_name"]');
const emailInput = page.locator('form input[name="email"]');
const phoneInput = page.locator('form input[name="phone"]');
const messageTextarea = page.locator('form textarea[name="message"]');
// Verify the inputs exist
await expect(nameInput).toBeVisible();
await expect(emailInput).toBeVisible();
await expect(phoneInput).toBeVisible();
await expect(messageTextarea).toBeVisible();
// Verify that placeholders are set
await expect(nameInput).toHaveAttribute('placeholder');
await expect(emailInput).toHaveAttribute('placeholder');
await expect(phoneInput).toHaveAttribute('placeholder');
await expect(messageTextarea).toHaveAttribute('placeholder');
// Verify the placeholders aren't empty
const namePlaceholder = await nameInput.getAttribute('placeholder');
const emailPlaceholder = await emailInput.getAttribute('placeholder');
const phonePlaceholder = await phoneInput.getAttribute('placeholder');
const messagePlaceholder = await messageTextarea.getAttribute('placeholder');
expect(namePlaceholder).toBeTruthy();
expect(emailPlaceholder).toBeTruthy();
expect(phonePlaceholder).toBeTruthy();
expect(messagePlaceholder).toBeTruthy();
});
test('contact form placeholders match values from shortcode', async ({ page }) => {
// Get the form field elements
const nameInput = page.locator('form input[name="full_name"]');
const emailInput = page.locator('form input[name="email"]');
const phoneInput = page.locator('form input[name="phone"]');
const messageTextarea = page.locator('form textarea[name="message"]');
// Get the placeholder values
const namePlaceholder = await nameInput.getAttribute('placeholder');
const emailPlaceholder = await emailInput.getAttribute('placeholder');
const phonePlaceholder = await phoneInput.getAttribute('placeholder');
const messagePlaceholder = await messageTextarea.getAttribute('placeholder');
// Log the placeholder values for debugging
console.log('Placeholder values:', {
name: namePlaceholder,
email: emailPlaceholder,
phone: phonePlaceholder,
message: messagePlaceholder
});
// Verify placeholders have meaningful content
// The specific values will depend on what's set in your shortcode or defaults
// We're checking they're not empty and not just generic defaults
expect(namePlaceholder).toBeTruthy();
expect(namePlaceholder?.length).toBeGreaterThan(2);
expect(emailPlaceholder).toBeTruthy();
expect(emailPlaceholder?.length).toBeGreaterThan(2);
expect(phonePlaceholder).toBeTruthy();
expect(phonePlaceholder?.length).toBeGreaterThan(2);
expect(messagePlaceholder).toBeTruthy();
expect(messagePlaceholder?.length).toBeGreaterThan(2);
// Check that placeholders are not just "placeholder" or generic text
expect(namePlaceholder?.toLowerCase()).not.toBe('placeholder');
expect(emailPlaceholder?.toLowerCase()).not.toBe('placeholder');
expect(phonePlaceholder?.toLowerCase()).not.toBe('placeholder');
expect(messagePlaceholder?.toLowerCase()).not.toBe('placeholder');
});
test('contact form has proper CSS styling', async ({ page }) => {
// Verify that the form elements have the expected CSS classes
await expect(page.locator('section.section--contact')).toBeVisible();
await expect(page.locator('form.contact__form')).toHaveClass(/contact__form/);
// Check specific form field styling
await expect(page.locator('input[name="full_name"]')).toHaveClass(/form-control/);
await expect(page.locator('input[name="email"]')).toHaveClass(/form-control/);
await expect(page.locator('input[name="phone"]')).toHaveClass(/form-control/);
await expect(page.locator('textarea[name="message"]')).toHaveClass(/form-control/);
// Check submit button styling
await expect(page.locator('form.contact__form button[type="submit"]')).toHaveClass(/btn btn-primary/);
});
test('message textarea has the correct number of rows', async ({ page }) => {
// Verify that the textarea has the correct number of rows (default is 2)
const messageTextarea = page.locator('form textarea[name="message"]');
const rowsValue = await messageTextarea.getAttribute('rows');
// The actual value will depend on your implementation, but should be at least "2"
expect(Number(rowsValue)).toBeGreaterThanOrEqual(2);
});
test('contact section displays correct information from shortcode', async ({ page }) => {
// Assuming the contact form has a title
const contactTitle = page.locator('section.section--contact h2');
await expect(contactTitle).toBeVisible();
// Check contact information is displayed correctly
// For example, the phone number, email, and address sections
const contactInfo = page.locator('.contact__info');
await expect(contactInfo).toBeVisible();
// Check headings in contact info section
const infoHeadings = contactInfo.locator('h3');
// Replace with a meaningful assertion if the expected count is known
await expect(infoHeadings).toHaveCount(3); // Example: Replace 3 with the actual expected count
// If your site has email info displayed, check it
const emailSection = contactInfo.locator('h3', { hasText: /mail|email/i }).first();
if (await emailSection.count() > 0) {
await expect(emailSection).toBeVisible();
// Check if there's an email value below the heading
const emailValue = emailSection.locator('+ span');
await expect(emailValue).toBeVisible();
}
// If your site has phone info displayed, check it
const phoneSection = contactInfo.locator('h3', { hasText: /phone/i }).first();
if (await phoneSection.count() > 0) {
await expect(phoneSection).toBeVisible();
// Check if there's a phone value below the heading
const phoneValue = phoneSection.locator('+ span');
await expect(phoneValue).toBeVisible();
}
});
test('submit button displays text from the shortcode', async ({ page }) => {
// Check that the submit button has text content
const submitButton = page.locator('form.contact__form button[type="submit"]');
await expect(submitButton).toBeVisible();
const buttonText = await submitButton.textContent();
expect(buttonText?.trim()).toBeTruthy();
});
test('form action is set correctly', async ({ page }) => {
// Verify that the form has an action attribute
const form = page.locator('form').first();
const action = await form.getAttribute('action');
expect(action).toBeTruthy();
// Also check the method attribute
const method = await form.getAttribute('method');
expect(method).toBeTruthy();
});
});
@@ -0,0 +1,87 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('Experience items functionality', () => {
test('navigates to experience items in English', async ({ page }) => {
// Start from homepage
await page.goto(BASE_URL);
// Click on first experience item
await page.getByText('Chief Intern').first().click();
// Verify URL and content
await expect(page).toHaveURL(/\/experience\/job-2\/?$/);
await expect(page.getByText('Internet Affairs Inc.').first()).toBeVisible();
await expect(page.getByText('Stavanger, Norway').first()).toBeVisible();
await expect(page.getByText('2023-2024').first()).toBeVisible();
});
test('navigates to experience items in Spanish', async ({ page }) => {
// Go to Spanish version
await page.goto(`${BASE_URL}/es/`);
// Click on first experience item
await page.getByText('Becario Jefe').first().click();
// Verify URL and Spanish content
await expect(page).toHaveURL(/\/es\/experience\/job-2\/?$/);
await expect(page.getByText('Internet Affairs Inc.').first()).toBeVisible();
await expect(page.getByText('Stavanger, Noruega').first()).toBeVisible();
await expect(page.getByText('Stavanger, Noruega').first()).toBeVisible();
await expect(page.getByText('2023-2024').first()).toBeVisible();
await expect(page.getByText('Arreglando el mundo, un byte a la vez').first()).toBeVisible();
});
test('navigates to experience items in French', async ({ page }) => {
// Go to French version
await page.goto(`${BASE_URL}/fr/`);
// Click on first experience item
await page.getByText('Stagiaire en Chef').first().click();
await expect(page).toHaveURL(/\/fr\/experience\/job-2\/?$/);
// Remove specific count checks as they might be fragile
await expect(page.getByText('Internet Affairs Inc.').first()).toBeVisible();
await expect(page.getByText('Stavanger').first()).toBeVisible();
await expect(page.getByText('2023-2024').first()).toBeVisible();
await expect(page.getByText('Réparer le monde, un octet à la fois').first()).toBeVisible();
});
test('verifies experience list page shows all items', async ({ page }) => {
// Go to experience list page
await page.goto(`${BASE_URL}/experience`);
// Verify multiple experience items are visible
const experienceItems = page.locator('.experience');
await expect(experienceItems).toHaveCount(4);
// Verify specific job titles are present
await expect(page.locator('.experience__title', { hasText: 'Chief Intern' })).toBeVisible();
await expect(page.locator('.experience__title', { hasText: 'Junior Intern' })).toBeVisible();
});
test('verifies job-card content and structure', async ({ page }) => {
await page.goto(`${BASE_URL}/experience/job-2`);
// Verify the job-card container exists
const jobCard = page.locator('.job-card');
await expect(jobCard).toBeVisible();
// Verify header content
await expect(page.getByRole('heading', { name: 'Chief Intern' }).first()).toBeVisible();
await expect(page.locator('.text-muted', { hasText: 'Internet Affairs Inc.' })).toBeVisible();
// Verify metadata
await expect(page.locator('.job-card__dates')).toHaveText('2023-2024');
await expect(page.locator('.job-card__location')).toContainText('Stavanger, Norway');
});
test('verifies company logo is visible on job page', async ({ page }) => {
await page.goto(`${BASE_URL}/experience/job-1`);
// Verify the company logo is visible
const companyLogo = page.locator('.company-logo');
await expect(companyLogo).toBeVisible();
});
});
@@ -0,0 +1,40 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('HTML validation', () => {
test('homepage has valid HTML structure', async ({ page }) => {
await page.goto(BASE_URL);
// Check basic HTML structure
await expect(page.locator('html')).toHaveAttribute('lang');
await expect(page.locator('head')).toBeAttached();
await expect(page.locator('body')).toBeAttached();
// Verify meta tags
await expect(page.locator('meta[charset]')).toBeAttached();
await expect(page.locator('meta[name="viewport"]')).toBeAttached();
await expect(page.locator('meta[name="description"]')).toBeAttached();
// Check main structural elements
await expect(page.locator('header')).toBeAttached();
await expect(page.locator('footer')).toBeAttached();
});
test('page has valid theme attributes', async ({ page }) => {
await page.goto(BASE_URL);
const html = page.locator('html');
// Check theme attributes are present
await expect(html).toHaveAttribute('data-bs-theme');
// If theme is forced, these attributes should be present
const isForcedTheme = await html.getAttribute('theme-forced');
if (isForcedTheme === 'true') {
await expect(html).toHaveAttribute('theme-auto', 'false');
}
});
});
@@ -0,0 +1,89 @@
import { test, expect } from '@playwright/test';
const BASE_URL = 'http://localhost:1313';
test.describe('Language switching functionality', () => {
test('switches between languages and verifies lang attribute', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// Go to homepage
await page.goto(BASE_URL);
// Verify initial English state
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
await expect(page.getByText('Language').last()).toBeVisible();
await expect(page.getByText('Experience').first()).toBeVisible();
// Switch to Spanish
await page.locator('div#footer-language-selector button').click();
await page.getByText('Español').last().click();
// Verify Spanish
await expect(page.locator('html')).toHaveAttribute('lang', 'es');
await expect(page.getByText('Idioma').last()).toBeVisible();
await expect(page.getByText('Experiencia').first()).toBeVisible();
// Switch to French
await page.locator('div#footer-language-selector button').click();
await page.getByText('Français').last().click();
// Verify French
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(page.getByText('Langue').last()).toBeVisible();
await expect(page.getByText('Expérience').first()).toBeVisible();
});
test('maintains language when navigating to experience pages', async ({ page }) => {
// Start in Spanish
await page.goto(`${BASE_URL}/es/`);
await expect(page.locator('html')).toHaveAttribute('lang', 'es');
// Click on an experience link
await page.getByText('Ver todo').click();
// Verify URL and language maintained
await expect(page).toHaveURL(`${BASE_URL}/es/experience/`);
await expect(page.locator('html')).toHaveAttribute('lang', 'es');
await expect(page.getByText('Experiencia').first()).toBeVisible();
});
test('preserves translations across page types', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// Go to French experience page
await page.goto(`${BASE_URL}/fr/experience`);
// Verify French state
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(page.getByText('Expérience').first()).toBeVisible();
// Navigate to home
await page.getByText('ACCUEIL').first().click();
// Verify language maintained
await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
await expect(page.getByText('Langue').last()).toBeVisible();
});
test('section IDs are translated in Spanish and French', async ({ page }) => {
// Spanish
await page.goto(`${BASE_URL}/es/`);
await expect(page.locator('#sobre-mi')).toBeVisible();
await expect(page.locator('#social')).toBeVisible();
await expect(page.locator('#sobre-mi')).toBeVisible();
await expect(page.locator('#formacion-academica')).toBeVisible();
await expect(page.locator('#experiencia-laboral')).toBeVisible();
await expect(page.locator('#trabajo')).toBeVisible();
await expect(page.locator('#testimonios')).toBeVisible();
// French
await page.goto(`${BASE_URL}/fr/`);
await expect(page.locator('#section-vedette')).toBeVisible();
await expect(page.locator('#liens-plateforme')).toBeVisible();
await expect(page.locator('#a-propos')).toBeVisible();
await expect(page.locator('#formation-academique')).toBeVisible();
await expect(page.locator('#experience-professionnelle')).toBeVisible();
await expect(page.locator('#travail')).toBeVisible();
await expect(page.locator('#temoignages')).toBeVisible();
});
});
@@ -0,0 +1,23 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('List style functionality', () => {
test('summary style shows full post previews', async ({ page }) => {
await page.goto(`${BASE_URL}/tags/sample`);
// Check for summary style elements
const articleSummary = page.locator('article.post.summary');
await expect(articleSummary).toBeVisible();
// Verify summary components are present
await expect(page.locator('article.post.summary h2').first()).toBeVisible();
await expect(page.locator('article.post.summary .post-meta').first()).toBeVisible();
await expect(page.locator('article.post.summary .post-summary').first()).toBeVisible();
await expect(page.locator('article.post.summary .btn-outline-secondary').first()).toBeVisible();
// Verify tags are displayed
await expect(page.locator('ul.tags li')).toHaveCount(2);
});
});
@@ -0,0 +1,364 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('New Blog Features', () => {
test.describe('Related Posts', () => {
test('should display related posts section with heading, list, and 3 post items including title, excerpt, date, and tags', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Wait for page to load
await page.waitForLoadState('networkidle');
// Check for related posts section
const relatedPostsSection = page.locator('.related-posts');
await expect(relatedPostsSection).toBeVisible();
// Check heading
await expect(relatedPostsSection.locator('h3')).toContainText('Related Posts');
// Check that we have related posts displayed
const relatedPostsList = relatedPostsSection.locator('.related-posts-list');
await expect(relatedPostsList).toBeVisible();
// Check that at least one related post item exists
const relatedPostItems = relatedPostsList.locator('.related-post-item');
await expect(relatedPostItems).toHaveCount(3); // Should show 3 related posts
// Verify each related post has required elements
const firstPost = relatedPostItems.first();
await expect(firstPost.locator('h4')).toBeVisible();
await expect(firstPost.locator('.related-post-excerpt')).toBeVisible();
await expect(firstPost.locator('.related-post-meta .post-date')).toBeVisible();
});
test('should navigate to correct blog post URL when clicking on a related post item', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Click on the first related post
await page.locator('.related-post-item').first().click();
// Should navigate to a different blog post
await expect(page).toHaveURL(/\/blog\/.+/);
await expect(page.locator('h1')).toBeVisible();
});
test('should display at least one tag on related posts items', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check that tags are displayed in related posts
const tagsInRelatedPosts = page.locator('.related-post-item .post-tags .tag');
const tagCount = await tagsInRelatedPosts.count();
// At least some posts should have tags
expect(tagCount).toBeGreaterThan(0);
});
});
test.describe('Social Sharing Buttons', () => {
test('should display social sharing section with "Share this post" heading', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const sharingSection = page.locator('.social-sharing');
await expect(sharingSection).toBeVisible();
// Check heading
await expect(sharingSection.locator('h3')).toContainText('Share this post');
});
test('should display sharing buttons for Twitter, LinkedIn, Facebook, and Email', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const sharingButtons = page.locator('.share-buttons');
await expect(sharingButtons).toBeVisible();
// Check for each platform's button
await expect(sharingButtons.locator('.share-twitter')).toBeVisible();
await expect(sharingButtons.locator('.share-linkedin')).toBeVisible();
await expect(sharingButtons.locator('.share-facebook')).toBeVisible();
await expect(sharingButtons.locator('.share-email')).toBeVisible();
});
test('should have correct share URLs for Twitter, LinkedIn, Facebook, and Email with proper parameters', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check Twitter share link
const twitterButton = page.locator('.share-twitter');
const twitterHref = await twitterButton.getAttribute('href');
expect(twitterHref).toContain('twitter.com/intent/tweet');
expect(twitterHref).toContain('url=');
// Check LinkedIn share link
const linkedinButton = page.locator('.share-linkedin');
const linkedinHref = await linkedinButton.getAttribute('href');
expect(linkedinHref).toContain('linkedin.com/sharing/share-offsite');
// Check Facebook share link
const facebookButton = page.locator('.share-facebook');
const facebookHref = await facebookButton.getAttribute('href');
expect(facebookHref).toContain('facebook.com/sharer');
// Check Email share link
const emailButton = page.locator('.share-email');
const emailHref = await emailButton.getAttribute('href');
expect(emailHref).toContain('mailto:');
});
test('should have accessible ARIA labels on all sharing buttons for screen readers', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check ARIA labels for accessibility
await expect(page.locator('.share-twitter')).toHaveAttribute('aria-label', /Share on Twitter/i);
await expect(page.locator('.share-linkedin')).toHaveAttribute('aria-label', /Share on LinkedIn/i);
await expect(page.locator('.share-facebook')).toHaveAttribute('aria-label', /Share on Facebook/i);
await expect(page.locator('.share-email')).toHaveAttribute('aria-label', /Share via Email/i);
});
});
test.describe('Table of Contents', () => {
test('should display table of contents section with "Table of Contents" heading when enabled in post frontmatter', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const tocSection = page.locator('.table-of-contents');
await expect(tocSection).toBeVisible();
// Check heading
await expect(tocSection.locator('h3')).toContainText('Table of Contents');
});
test('should contain navigation links with anchor href attributes pointing to page sections', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const tocNav = page.locator('.table-of-contents #TableOfContents');
await expect(tocNav).toBeVisible();
// Should have list items with links
const tocLinks = tocNav.locator('a');
const linkCount = await tocLinks.count();
expect(linkCount).toBeGreaterThan(0);
// Check first link
await expect(tocLinks.first()).toBeVisible();
const firstHref = await tocLinks.first().getAttribute('href');
expect(firstHref).toMatch(/^#/); // Should be anchor links
});
test('should scroll to corresponding section and update URL hash when clicking on table of contents links', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Click on a TOC link
const tocLinks = page.locator('.table-of-contents #TableOfContents a');
const firstLink = tocLinks.first();
const linkText = await firstLink.textContent();
await firstLink.click();
// URL should have the hash
await expect(page).toHaveURL(/#.+/);
// Wait a bit for smooth scrolling
await page.waitForTimeout(500);
});
test('should have sticky positioning class when tocSticky is enabled in post frontmatter', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const tocSection = page.locator('.table-of-contents');
// Check if it has the sticky class (post has tocSticky: true)
await expect(tocSection).toHaveClass(/toc-sticky/);
});
});
test.describe('Enhanced Reading Metadata', () => {
test('should display estimated reading time in minutes', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for reading time
const readingTime = page.locator('#reading-time');
await expect(readingTime).toBeVisible();
await expect(readingTime).toContainText('min read');
});
test('should display total word count of the post', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for word count
const wordCount = page.locator('#wordcount');
await expect(wordCount).toBeVisible();
await expect(wordCount).toContainText('Words');
});
test('should display post publish date', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for publish date
const publishDate = page.locator('#date');
await expect(publishDate).toBeVisible();
});
test('should display last modified date with "Updated" label when lastmod is present in frontmatter', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check for last modified date
const lastModified = page.locator('#last-modified');
// The demo post has lastmod, so it should be visible
await expect(lastModified).toBeVisible();
await expect(lastModified).toContainText('Updated');
});
test('should have properly structured metadata section with semantic HTML', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const metaSection = page.locator('#meta');
await expect(metaSection).toBeVisible();
// Should be a logical grouping
await expect(metaSection.locator('section')).toBeVisible();
});
});
test.describe('Comments Integration (Structure)', () => {
test('should have main content structure that supports comments section when configured', async ({ page }) => {
// Note: Since comments aren't enabled in demo, this tests the structure
// would exist if configured
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Comments section won't be visible without configuration
// but we can test that the blog single template is properly structured
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeVisible();
});
});
test.describe('Responsive Design', () => {
test('should display related posts, social sharing, and table of contents on mobile viewport (375x667)', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Related posts should adapt to mobile
const relatedPosts = page.locator('.related-posts');
await expect(relatedPosts).toBeVisible();
// Social sharing should be visible
const socialSharing = page.locator('.social-sharing');
await expect(socialSharing).toBeVisible();
// TOC should be visible but may not be sticky
const toc = page.locator('.table-of-contents');
await expect(toc).toBeVisible();
});
test('should adapt related posts grid layout across desktop (1280px), tablet (768px), and mobile (375px) viewports', async ({ page }) => {
// Test desktop width
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
const relatedPostsList = page.locator('.related-posts-list');
await expect(relatedPostsList).toBeVisible();
// Test tablet width
await page.setViewportSize({ width: 768, height: 1024 });
await expect(relatedPostsList).toBeVisible();
// Test mobile width
await page.setViewportSize({ width: 375, height: 667 });
await expect(relatedPostsList).toBeVisible();
});
});
test.describe('Multilingual Support', () => {
test('should display "Idioma" button and Spanish language option in language switcher dropdown', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// Navigate to Spanish version
await page.goto(`${BASE_URL}/es/`);
// Open language switcher
const languageButton = page.locator('button', { hasText: 'Idioma' }).first();
await languageButton.click();
// Should see language options
await expect(page.locator('#languages-dropdown-header').getByText('Español')).toBeVisible();
});
});
test.describe('Accessibility', () => {
test('should use proper h3 heading hierarchy for related posts, social sharing, and table of contents sections', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Check that section headings are h3
await expect(page.locator('.related-posts h3')).toBeVisible();
await expect(page.locator('.social-sharing h3')).toBeVisible();
await expect(page.locator('.table-of-contents h3')).toBeVisible();
});
test('should allow keyboard navigation and focus on social sharing buttons', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Focus first share button
const shareButtons = page.locator('.share-buttons a');
const firstButton = shareButtons.first();
await firstButton.focus();
// Should be focusable
await expect(firstButton).toBeFocused();
});
test('should allow keyboard navigation and focus on table of contents links', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Focus first TOC link
const tocLinks = page.locator('.table-of-contents a');
const firstLink = tocLinks.first();
await firstLink.focus();
// Should be focusable
await expect(firstLink).toBeFocused();
});
test('should use semantic HTML with unordered list (ul/li) elements for related posts', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Related posts should use list elements
const relatedPostsList = page.locator('.related-posts ul');
await expect(relatedPostsList).toBeVisible();
// Each item should be a list item
const listItems = relatedPostsList.locator('li');
await expect(listItems.first()).toBeVisible();
});
});
test.describe('Performance', () => {
test('should load page with all new features in under 5 seconds including network idle state', async ({ page }) => {
const startTime = Date.now();
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
// Should load in under 5 seconds
expect(loadTime).toBeLessThan(5000);
});
test('should render all feature sections without causing cumulative layout shift after page load', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/new-features-demo/`);
// Wait for page to be fully loaded
await page.waitForLoadState('networkidle');
// All main sections should be visible without scrolling
const relatedPosts = page.locator('.related-posts');
await expect(relatedPosts).toBeAttached();
});
});
});
@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('Site without menus', () => {
// Read environment variable TEST_NO_MENUS
test.skip(process.env.TEST_NO_MENUS !== 'true', 'Skipping test because TEST_NO_MENUS is true');
test('page should not have navigation menus when disabled', async ({ page }) => {
// Navigate to the site with disabled menus
await page.goto(`${BASE_URL}/disable-menu/`);
// Verify header still exists but doesn't contain navigation
await expect(page.locator('header')).toBeAttached();
// Verify language selector is not present
await expect(page.locator('#selector-language')).not.toBeAttached();
// Verify main menu elements are not present
await expect(page.locator('#main-menu')).not.toBeAttached();
await expect(page.locator('#main-menu-mobile')).not.toBeAttached();
// Verify the page still has basic required elements
await expect(page.locator('main')).toBeAttached();
await expect(page.locator('footer')).toBeAttached();
});
test('content should still be properly displayed', async ({ page }) => {
await page.goto(`${BASE_URL}`);
// Verify main content area exists and is visible
const mainContent = page.locator('#main-content');
await expect(mainContent).toBeVisible();
await expect(mainContent.locator('.display-1')).toBeVisible();
// Verify page title is still present
await expect(page.locator('h1').first()).toBeVisible();
});
//
});
+134
View File
@@ -0,0 +1,134 @@
import { test, expect } from '@playwright/test';
const BASE_URL: string = process.env.TEST_BASE_URL ?? 'http://localhost:1313';
if (!BASE_URL.startsWith('http')) {
throw new Error('TEST_BASE_URL must be a valid URL starting with http:// or https://');
}
test.describe('Search functionality', () => {
test.beforeAll(async () => {
// Health check
try {
await fetch(BASE_URL);
} catch (error) {
console.error(`Failed to connect to ${BASE_URL}. Is the Hugo server running?`);
throw error;
}
});
test('search page loads correctly', async ({ page }) => {
const response = await page.goto(`${BASE_URL}/search`);
await expect(page).toHaveTitle(/Search/);
await expect(page.locator('h2.mb-4.text-center')).toHaveText('Search the Site');
await expect(page.locator('#search-query')).toBeVisible();
// Check that the search page returns HTTP status 200
});
test('searching for "theme" shows exactly one result', async ({ page }) => {
await page.goto(`${BASE_URL}/search`);
// Wait for the page to be fully loaded
await page.waitForLoadState('networkidle');
// Type "theme" in the search box
await page.locator('#search-query').fill('theme');
// Wait for search results to appear (the debounce is 300ms)
await page.waitForTimeout(500);
// Check URL is updated with search parameter
await expect(page).toHaveURL(/s=theme/);
// Verify exactly one result is shown
await expect(page.locator('#search-results div[id^="summary-"]')).toHaveCount(1);
});
test('clearing search box removes results', async ({ page }) => {
await page.goto(`${BASE_URL}/search`);
// Type something to get results first
await page.locator('#search-query').fill('theme');
// Wait for search results to appear
await page.waitForTimeout(500);
// Verify we have at least one result
await expect(page.locator('#search-results div[id^="summary-"]').first()).toBeVisible();
// Now clear the search box
await page.locator('#search-query').fill('');
// Wait for the UI to update
await page.waitForTimeout(500);
// Check that results are cleared and we have the prompt message
await expect(page.locator('#search-results div[id^="summary-"]')).toHaveCount(0);
await expect(page.locator('#search-results .alert')).toBeVisible();
await expect(page.locator('#search-results .alert')).toHaveText('Please enter at least 2 characters to search');
});
test('searching for "adritian" shows multiple results', async ({ page }) => {
await page.goto(`${BASE_URL}/search`);
// Type "adritian" in the search box
await page.locator('#search-query').fill('adritian');
// Wait for search results to appear
await page.waitForTimeout(500);
// Check URL is updated with search parameter
await expect(page).toHaveURL(/s=adritian/);
// Verify we have multiple results
const resultsCount = await page.locator('#search-results div[id^="summary-"]').count();
expect(resultsCount).toBeGreaterThan(1);
});
test('search handles special characters correctly', async ({ page }) => {
await page.goto(`${BASE_URL}/search`);
// Test with special characters that might break URL encoding
const specialCharQuery = 'test & special';
await page.locator('#search-query').fill(specialCharQuery);
// Wait for search results to appear
await page.waitForTimeout(500);
// Check URL is properly encoded
await expect(page).toHaveURL(`${BASE_URL}/search/?s=test+%26+special`.replace('&', '%26'));
// Verify the search box still contains the original query
await expect(page.locator('#search-query')).toHaveValue(specialCharQuery);
});
test('search updates results in real-time as user types', async ({ page }) => {
await page.goto(`${BASE_URL}/search`);
// Type one character at a time and verify the behavior
await page.locator('#search-query').fill('a');
// With just one character, we should see the minimum character message
await page.waitForTimeout(500);
await expect(page.locator('#search-results .alert')).toContainText('at least 2 characters');
// Type a second character to reach minimum
await page.locator('#search-query').fill('ad');
// Wait for search results to appear
await page.waitForTimeout(500);
// Should now show results
await expect(page.locator('#search-results div[id^="summary-"]').first()).toBeVisible();
// Continue typing to refine search
await page.locator('#search-query').fill('adri');
// Wait for updated results
await page.waitForTimeout(500);
// Should still show results, potentially different count
await expect(page.locator('#search-results div[id^="summary-"]').first()).toBeVisible();
});
});
@@ -0,0 +1,86 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('Hugo sections functionality', () => {
test('articles section loads correctly', async ({ page }) => {
await page.goto(`${BASE_URL}/articles/`);
// Check that section page loads
await expect(page.locator('h1')).toContainText('Articles');
// Verify section content is displayed
await expect(page.getByText('Welcome to the articles section')).toBeVisible();
// Check that article posts are listed
const articleCount = await page.locator('article.post').count();
expect(articleCount).toBeGreaterThan(0);
});
test('individual article pages load correctly', async ({ page }) => {
await page.goto(`${BASE_URL}/articles/first-article/`);
// Check that individual article loads
await expect(page.locator('h1')).toContainText('First Article');
await expect(page.getByText('This is the first article content')).toBeVisible();
});
test('news section loads correctly', async ({ page }) => {
await page.goto(`${BASE_URL}/news/`);
// Check that section page loads
await expect(page.locator('h1')).toContainText('News');
// Verify section content is displayed
await expect(page.getByText('Latest news updates').first()).toBeVisible();
// Check that news posts are listed
const newsCount = await page.locator('article.post').count();
expect(newsCount).toBeGreaterThan(0);
});
test('individual news pages load correctly', async ({ page }) => {
await page.goto(`${BASE_URL}/news/breaking-news/`);
// Check that individual news item loads
await expect(page.locator('h1')).toContainText('Breaking News');
await expect(page.getByText('This is breaking news content')).toBeVisible();
});
test('section pagination works if applicable', async ({ page }) => {
await page.goto(`${BASE_URL}/articles/`);
// Check if pagination exists and works
const paginationExists = await page.locator('.pagination').count() > 0;
if (paginationExists) {
// Test pagination if it exists
await expect(page.locator('.pagination')).toBeVisible();
// Check if there's a next page link and it works
const nextLink = page.locator('.pagination a[rel="next"]');
if (await nextLink.count() > 0) {
await nextLink.click();
await expect(page.locator('h1')).toContainText('Articles');
}
}
});
test('section RSS feeds are accessible', async ({ page }) => {
// Test articles RSS feed
const articlesRssResponse = await page.request.get(`${BASE_URL}/articles/index.xml`);
expect(articlesRssResponse.status()).toBe(200);
// Test news RSS feed
const newsRssResponse = await page.request.get(`${BASE_URL}/news/index.xml`);
expect(newsRssResponse.status()).toBe(200);
});
test('existing blog section still works', async ({ page }) => {
await page.goto(`${BASE_URL}/blog/`);
// Verify existing blog section still works
await expect(page.locator('h1').first()).toContainText('Demo Blog');
await expect(page.getByText('Welcome to the demo blog')).toBeVisible();
});
});
+39
View File
@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
test.describe('Tag functionality', () => {
test('visiting tags list', async ({ page }) => {
await page.goto(`${BASE_URL}/tags`);
// Verify we reached the tags page
await expect(page.getByRole('heading', { name: 'Tags', level: 1 })).toBeVisible();
});
test('verify there are 2 tags, each with 1 article', async ({ page }) => {
await page.goto(`${BASE_URL}/tags`);
await expect(page.locator('ul.list-taxonomy li')).toHaveCount(11);
// Each item shows how many articles
// e.g. "Lorem-Ipsum (1)" or "Sample (1)"
await expect(page.getByText('Sample (1)')).toBeVisible();
});
test('click on a tag -> renders the tag page', async ({ page }) => {
await page.goto(`${BASE_URL}/tags`);
await page.getByRole('link', { name: /Sample/ }).click();
// Verify tag page
await expect(page).toHaveURL(/\/tags\/sample/);
await expect(page.getByRole('heading', { name: 'Sample content: formatting styles' })).toBeVisible();
});
test('tag page content links', async ({ page }) => {
// Go directly to the Sample tag page
await page.goto(`${BASE_URL}/tags/sample`);
// Verify any article link is visible (1 article)
const articleLink = page.locator('a[href*="/blog/sample/"]');
await expect(articleLink.first()).toBeVisible();
await articleLink.first().click();
// Article should list the tags
await expect(page.locator('ul.tags li')).toHaveCount(2);
});
});
@@ -0,0 +1,63 @@
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:1313';
// Extract repeated selectors into constants
const FOOTER_THEME_CONTAINER = 'div#footer-color-selector';
const FOOTER_THEME_SELECTOR = `${FOOTER_THEME_CONTAINER} button.bd-theme-selector`;
const FOOTER_THEME_LIGHT = `${FOOTER_THEME_CONTAINER} .dropdown-item[data-bs-theme-value="light"]`;
const FOOTER_THEME_DARK = `${FOOTER_THEME_CONTAINER} .dropdown-item[data-bs-theme-value="dark"]`;
const FOOTER_THEME_AUTO = `${FOOTER_THEME_CONTAINER} .dropdown-item[data-bs-theme-value="auto"]`;
test.describe('Theme switching functionality', () => {
test('verifies bold styling of selected theme and updates on switch', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// Go to homepage
await page.goto(BASE_URL);
// Open theme dropdown
await page.locator(FOOTER_THEME_SELECTOR).click();
// Verify initial Light theme is bold (font-weight: 700)
const lightButton = page.locator(FOOTER_THEME_LIGHT);
await expect(lightButton).toHaveCSS('font-weight', '700');
// Switch to Dark theme
await page.locator(FOOTER_THEME_DARK).click();
// Verify Dark theme is now bold and Light is not
const darkButton = page.locator(FOOTER_THEME_DARK);
await expect(darkButton).toHaveCSS('font-weight', '700');
await expect(lightButton).toHaveCSS('font-weight', '400');
// Switch to Auto theme
await page.locator(FOOTER_THEME_SELECTOR).click();
await page.locator(FOOTER_THEME_AUTO).click();
// Verify Auto theme is bold and others are not
const autoButton = page.locator(FOOTER_THEME_AUTO);
await expect(autoButton).toHaveCSS('font-weight', '700');
await expect(darkButton).toHaveCSS('font-weight', '400');
await expect(lightButton).toHaveCSS('font-weight', '400');
});
test('theme selection persists after page reload', async ({ page }) => {
test.skip(process.env.TEST_NO_MENUS === 'true', 'Skipping test because TEST_NO_MENUS is true');
// Go to homepage
await page.goto(BASE_URL);
// Switch to Dark theme
await page.locator(FOOTER_THEME_SELECTOR).click();
await page.locator(FOOTER_THEME_DARK).click();
// Reload page
await page.reload();
// Open dropdown and verify Dark theme is still bold
await page.locator(FOOTER_THEME_SELECTOR).click();
const darkButton = page.locator(FOOTER_THEME_DARK);
await expect(darkButton).toHaveCSS('font-weight', '700');
});
});