Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
40d9b0f
feat(navigation): implement sticky nav, smooth scroll, mobile hamburg…
May 27, 2026
320c49d
feat(accessibility): implement MirDB homepage with WCAG 2.1 AA compli…
May 27, 2026
7487bfb
merge: resolve conflicts with navigation scenario, keep accessibility…
May 27, 2026
2fed53d
feat(responsive): enhance responsive design with full viewport coverage
May 27, 2026
6102ac0
feat(seo): implement comprehensive SEO meta tags and structured data
May 27, 2026
6191246
chore(scenario): backend fallback commit for "SEO and Meta Tags"
May 27, 2026
8e57389
feat(build): fix Zola template inheritance and add build system tests
May 27, 2026
c12e613
feat(seo): add comprehensive SEO meta tags, Open Graph, Twitter Cards…
May 27, 2026
637bd3a
feat(seo): merge remote changes and add WebSite structured data
May 27, 2026
6d0c2ef
feat(build): fix Zola build and add build system validation tests
May 27, 2026
ece4a53
feat(performance): inline critical CSS, async load assets, add defer …
May 27, 2026
2b6ddbf
feat(responsive): enhance responsive design with comprehensive E2E tests
May 27, 2026
df46da3
fix(performance): update critical CSS and tests for new design system
May 27, 2026
a059bfa
feat(performance): optimize homepage for 90+ Lighthouse score
May 27, 2026
59af8ea
fix(tests): update load time tests for rebased codebase
May 27, 2026
f32386b
feat(performance): inline critical CSS, preload non-critical assets, …
May 27, 2026
d6bc092
chore(scenario): backend fallback commit for "Performance and Load Time"
May 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/skills/playwright-responsive-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
name: playwright-responsive-testing
description: E2E responsive layout testing with Playwright across multiple viewports. Validates grid columns, navigation visibility, tap targets, horizontal scroll, and orientation changes.
scope: project
---

See [README.md](references/README.md) for full documentation.
123 changes: 123 additions & 0 deletions .claude/skills/playwright-responsive-testing/references/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Playwright Responsive Testing

## Overview

This skill enables comprehensive responsive design verification using Playwright E2E tests. It validates layout behavior across viewports from 320px mobile to 2560px ultrawide, including grid column counts, navigation state, tap target sizes, horizontal scroll detection, and orientation changes.

## When to Use This Skill

Use this skill when users request:

- Adding responsive design tests to a homepage or web app
- Verifying CSS grid/flexbox layouts across breakpoints
- Testing navigation collapse/expand at viewport thresholds
- Ensuring tap targets meet WCAG accessibility standards on mobile

## Core Capabilities

### 1. Viewport Matrix Testing

Define a set of viewports and run assertions against each:

```js
const VIEWPORTS = {
mobileSE: { width: 320, height: 568 },
mobile8: { width: 375, height: 667 },
tablet: { width: 768, height: 1024 },
smallDesktop: { width: 1024, height: 768 },
standardDesktop: { width: 1920, height: 1080 },
ultrawide: { width: 2560, height: 1440 },
};
```

Use `beforeEach` to set the viewport and reload the page so media queries re-evaluate:

```js
beforeEach(async () => {
await page.setViewportSize(VIEWPORTS.tablet);
await page.reload();
});
```

### 2. Grid Column Detection

Browsers resolve `grid-template-columns: 1fr` to pixel values in computed styles. Use a helper to count columns:

```js
function countGridColumns(gridTemplateColumns) {
if (!gridTemplateColumns || gridTemplateColumns === 'none') return 0;
return gridTemplateColumns.split(/\s+/).filter(s => s && s !== '0px').length;
}

// Usage:
const gridComputed = await page.evaluate(() => {
const el = document.querySelector('.features-grid');
return el ? window.getComputedStyle(el).gridTemplateColumns : '';
});
expect(countGridColumns(gridComputed)).toBe(4);
```

### 3. Visibility via boundingBox

Playwright's Jest integration lacks `toBeHidden()`. Use `boundingBox()` instead:

```js
const box = await page.locator('.mobile-menu-toggle').boundingBox();
const isHidden = !box || box.width === 0 || box.height === 0;
expect(isHidden).toBe(true);
```

Hidden elements return `null` from `boundingBox()`. Elements in the layout with zero dimensions return `{x, y, width: 0, height: 0}`.

### 4. Tap Target Validation

Iterate all interactive elements and assert minimum size:

```js
const tapTargets = await page.locator('button, a, .btn').all();
const failures = [];
for (const target of tapTargets) {
const box = await target.boundingBox();
if (box && box.width > 0 && box.height > 0) {
if (box.width < 44 || box.height < 44) {
failures.push(`${box.width}x${box.height}`);
}
}
}
expect(failures).toHaveLength(0);
```

### 5. Horizontal Scroll Detection

```js
const overflow = await page.evaluate(() => {
return document.documentElement.scrollWidth > window.innerWidth;
});
expect(overflow).toBe(false);
```

### 6. Orientation Change Testing

Change viewport dimensions mid-test and verify layout adaptation:

```js
await page.setViewportSize({ width: 375, height: 667 });
await page.reload();
// ... portrait assertions ...
await page.setViewportSize({ width: 667, height: 375 });
await page.waitForTimeout(500);
// ... landscape assertions ...
```

## Best Practices

- Always call `page.reload()` after `setViewportSize()` so media queries re-evaluate correctly
- Use `page.evaluate()` for computed style checks; `page.locator().evaluate()` works on specific elements
- Batch related assertions in the same `it()` block to minimize browser cycles
- Use `file://` URLs for testing static HTML files without a dev server

## Resources

### references/

- `README.md` - This documentation
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/target
**/*.rs.bk
.something/
node_modules/
test-results/
62 changes: 62 additions & 0 deletions debug_html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');

const HOMEPAGE_DIR = '/workspace/homepage';
const PARTIALS_DIR = path.join(HOMEPAGE_DIR, 'templates/partials');

function preprocessTemplate(html) {
return html
.replace(/\{\{\s*get_url\(path=['"]([^'"]+)['"]\)\s*\}\}/g, (match, p1) => '/' + p1)
.replace(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g, '')
.replace(/\{%\s*block\s+\w+\s*%\}/g, '')
.replace(/\{%\s*endblock\s*%\}/g, '');
}

const basePath = path.join(HOMEPAGE_DIR, 'templates/base.html');
const indexPath = path.join(HOMEPAGE_DIR, 'templates/index.html');
let html = fs.readFileSync(basePath, 'utf-8');
const indexHtml = fs.readFileSync(indexPath, 'utf-8');

const blockRegex = /\{%\s*block\s+(\w+)\s*%\}([\s\S]*?)\{%\s*endblock(?:\s+\w+)?\s*%\}/g;
let blockMatch;
while ((blockMatch = blockRegex.exec(indexHtml)) !== null) {
const blockName = blockMatch[1];
const blockContent = blockMatch[2];
console.log('Replacing block:', blockName, 'with content length:', blockContent.length);
const baseBlockPattern = new RegExp('\\{%\\s*block\\s+' + blockName + '\\s*%\\}([\\s\\S]*?)\\{%\\s*endblock(?:\\s+\\w+)?\\s*%\\}');
html = html.replace(baseBlockPattern, blockContent);
}

console.log('After block replace, includes remaining:');
const includes = [...html.matchAll(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g)];
includes.forEach(m => console.log(' ', m[1]));

let safety = 0;
while (safety++ < 20) {
const matches = [...html.matchAll(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g)];
if (matches.length === 0) break;
for (const m of matches) {
const includePath = m[1];
const partialFile = path.basename(includePath).replace('.html', '') + '.html';
const partialPath = path.join(PARTIALS_DIR, partialFile);
let partialContent = '';
if (fs.existsSync(partialPath)) {
partialContent = fs.readFileSync(partialPath, 'utf-8');
}
html = html.replace(m[0], partialContent);
}
}

html = html.replace(/\{\{\s*get_url\(path=['"]([^'"]+)['"]\)\s*\}\}/g, (m, p1) => '/' + p1);

const dom = new JSDOM(preprocessTemplate(html));
const doc = dom.window.document;

console.log('\nParsed results:');
console.log('Articles:', doc.querySelectorAll('article').length);
console.log('Sections:', doc.querySelectorAll('section').length);
console.log('Headers:', doc.querySelectorAll('header').length);
console.log('Navs:', doc.querySelectorAll('nav').length);
console.log('Footers:', doc.querySelectorAll('footer').length);
console.log('Main:', doc.querySelectorAll('main').length);
2 changes: 2 additions & 0 deletions homepage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
test-results/
17 changes: 17 additions & 0 deletions homepage/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Zola configuration for MirDB Homepage
# Created by the first scenario builder
#
# Expected settings:
# - base_url = "https://mirdb.dev" (or appropriate domain)
# - title = "MirDB - Persistent Key-Value Store"
# - description = "A persistent key-value store with memcached protocol"
# - compile_sass = true
# - build_search_index = false
# - minify_html = true

base_url = "https://mirdb.dev"
title = "MirDB - Persistent Key-Value Store"
description = "A persistent key-value store with memcached protocol"
compile_sass = true
build_search_index = false
minify_html = true
4 changes: 4 additions & 0 deletions homepage/content/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
+++
title = "MirDB - Persistent Key-Value Store"
description = "A persistent key-value store with memcached protocol compatibility, built in Rust."
+++
62 changes: 62 additions & 0 deletions homepage/debug_html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const fs = require('fs');
const path = require('path');
const { JSDOM } = require('jsdom');

const HOMEPAGE_DIR = '/workspace/homepage';
const PARTIALS_DIR = path.join(HOMEPAGE_DIR, 'templates/partials');

function preprocessTemplate(html) {
return html
.replace(/\{\{\s*get_url\(path=['"]([^'"]+)['"]\)\s*\}\}/g, (match, p1) => '/' + p1)
.replace(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g, '')
.replace(/\{%\s*block\s+\w+\s*%\}/g, '')
.replace(/\{%\s*endblock\s*%\}/g, '');
}

const basePath = path.join(HOMEPAGE_DIR, 'templates/base.html');
const indexPath = path.join(HOMEPAGE_DIR, 'templates/index.html');
let html = fs.readFileSync(basePath, 'utf-8');
const indexHtml = fs.readFileSync(indexPath, 'utf-8');

const blockRegex = /\{%\s*block\s+(\w+)\s*%\}([\s\S]*?)\{%\s*endblock(?:\s+\w+)?\s*%\}/g;
let blockMatch;
while ((blockMatch = blockRegex.exec(indexHtml)) !== null) {
const blockName = blockMatch[1];
const blockContent = blockMatch[2];
console.log('Replacing block:', blockName, 'with content length:', blockContent.length);
const baseBlockPattern = new RegExp('\\{%\\s*block\\s+' + blockName + '\\s*%\\}([\\s\\S]*?)\\{%\\s*endblock(?:\\s+\\w+)?\\s*%\\}');
html = html.replace(baseBlockPattern, blockContent);
}

console.log('After block replace, includes remaining:');
const includes = [...html.matchAll(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g)];
includes.forEach(m => console.log(' ', m[1]));

let safety = 0;
while (safety++ < 20) {
const matches = [...html.matchAll(/\{%\s*include\s+["']([^"']+)["']\s*%\}/g)];
if (matches.length === 0) break;
for (const m of matches) {
const includePath = m[1];
const partialFile = path.basename(includePath).replace('.html', '') + '.html';
const partialPath = path.join(PARTIALS_DIR, partialFile);
let partialContent = '';
if (fs.existsSync(partialPath)) {
partialContent = fs.readFileSync(partialPath, 'utf-8');
}
html = html.replace(m[0], partialContent);
}
}

html = html.replace(/\{\{\s*get_url\(path=['"]([^'"]+)['"]\)\s*\}\}/g, (m, p1) => '/' + p1);

const dom = new JSDOM(preprocessTemplate(html));
const doc = dom.window.document;

console.log('\nParsed results:');
console.log('Articles:', doc.querySelectorAll('article').length);
console.log('Sections:', doc.querySelectorAll('section').length);
console.log('Headers:', doc.querySelectorAll('header').length);
console.log('Navs:', doc.querySelectorAll('nav').length);
console.log('Footers:', doc.querySelectorAll('footer').length);
console.log('Main:', doc.querySelectorAll('main').length);
6 changes: 6 additions & 0 deletions homepage/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'jsdom',
testMatch: ['**/tests/integration/*.test.js'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
verbose: true,
};
6 changes: 6 additions & 0 deletions homepage/jest.e2e.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/tests/e2e/*.test.js'],
verbose: true,
testTimeout: 30000,
};
Loading