Skip to content

Commit 99ca157

Browse files
committed
feature: added new playwright tests
1 parent fcb499b commit 99ca157

45 files changed

Lines changed: 806 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
composer.phar
33
composer.lock
44
.DS_Store
5+
/node_modules
6+
package-lock.json
7+
tests/visual/test-results/
8+
/node_modules

package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "html",
3+
"version": "1.0.0",
4+
"description": "![Grafite HTML](GrafiteHtml-banner.png)",
5+
"main": "index.js",
6+
"directories": {
7+
"test": "tests"
8+
},
9+
"scripts": {
10+
"test:visual": "npx playwright test",
11+
"test:visual:update": "npx playwright test --update-snapshots",
12+
"test:visual:ui": "npx playwright test --ui"
13+
},
14+
"repository": {
15+
"type": "git",
16+
"url": "git+https://github.com/GrafiteInc/Html.git"
17+
},
18+
"keywords": [],
19+
"author": "",
20+
"license": "ISC",
21+
"type": "commonjs",
22+
"bugs": {
23+
"url": "https://github.com/GrafiteInc/Html/issues"
24+
},
25+
"homepage": "https://github.com/GrafiteInc/Html#readme",
26+
"devDependencies": {
27+
"@playwright/test": "^1.59.1"
28+
}
29+
}

playwright.config.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// @ts-check
2+
const { defineConfig } = require('@playwright/test');
3+
4+
module.exports = defineConfig({
5+
testDir: './tests/visual',
6+
outputDir: './tests/visual/test-results',
7+
snapshotDir: './tests/visual/screenshots',
8+
snapshotPathTemplate: '{snapshotDir}/{testName}/{arg}{ext}',
9+
10+
fullyParallel: true,
11+
retries: 0,
12+
workers: 1,
13+
14+
use: {
15+
baseURL: 'http://localhost:8234',
16+
screenshot: 'only-on-failure',
17+
},
18+
19+
expect: {
20+
toHaveScreenshot: {
21+
maxDiffPixelRatio: 0.01,
22+
animations: 'disabled',
23+
},
24+
},
25+
26+
projects: [
27+
{
28+
name: 'chromium',
29+
use: {
30+
browserName: 'chromium',
31+
viewport: { width: 1280, height: 720 },
32+
},
33+
},
34+
],
35+
36+
webServer: {
37+
command: 'php -S localhost:8234 tests/visual/serve.php',
38+
port: 8234,
39+
reuseExistingServer: !process.env.CI,
40+
timeout: 10000,
41+
},
42+
});

src/Tags/Breadcrumbs.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static function processLinks($links)
2727
$active = ' active';
2828
}
2929

30-
$steps .= "<li class=\"breadcrumb-item${active}\"><a href=\"{$link}\">{$title}</a></li>\n";
30+
$steps .= "<li class=\"breadcrumb-item{$active}\"><a href=\"{$link}\">{$title}</a></li>\n";
3131
}
3232

3333
return $steps;

src/Tags/GithubGraph.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,10 @@ function buildGraph() {
559559
document.addEventListener('DOMContentLoaded', function() {
560560
new GithubContributions_{$id}({$jsonOptions});
561561
});
562+
563+
if (document.readyState !== 'loading') {
564+
new GithubContributions_{$id}({$jsonOptions});
565+
}
562566
JS;
563567
}
564568

tests/visual/components.spec.js

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// @ts-check
2+
const { test, expect } = require('@playwright/test');
3+
4+
/**
5+
* Static components that render immediately without external JS dependencies.
6+
* These are the most reliable for pixel-perfect screenshot comparison.
7+
*/
8+
const staticComponents = [
9+
'accordion',
10+
'admonition',
11+
'alert',
12+
'breadcrumbs',
13+
'card',
14+
'divider',
15+
'dropdown-button',
16+
'dropdown-button-action',
17+
'dropdown-button-group',
18+
'feed',
19+
'list-group',
20+
'modal',
21+
'nav',
22+
'nav-bar',
23+
'offcanvas',
24+
'progress',
25+
'spinner',
26+
'status',
27+
'table',
28+
];
29+
30+
for (const component of staticComponents) {
31+
test(`static: ${component}`, async ({ page }) => {
32+
await page.goto(`/${component}`);
33+
await page.waitForLoadState('domcontentloaded');
34+
35+
const container = page.locator('.visual-test-container');
36+
await expect(container).toBeVisible();
37+
await expect(container).toHaveScreenshot(`${component}.png`);
38+
});
39+
}
40+
41+
/**
42+
* Announcement uses position:fixed so it renders outside the container.
43+
* Screenshot the fixed-position element directly.
44+
*/
45+
test('static: announcement', async ({ page }) => {
46+
await page.goto('/announcement');
47+
await page.waitForLoadState('domcontentloaded');
48+
49+
const announcement = page.locator('.html-announcement');
50+
await expect(announcement).toBeVisible();
51+
await expect(announcement).toHaveScreenshot('announcement.png');
52+
});
53+
54+
/**
55+
* Components that rely on JS initialization but render quickly.
56+
* We wait a bit longer for scripts to execute.
57+
*/
58+
const jsComponents = [
59+
'countdown',
60+
'image-compare',
61+
'marquee',
62+
'popover',
63+
'rating',
64+
'word-switcher',
65+
];
66+
67+
for (const component of jsComponents) {
68+
test(`js-init: ${component}`, async ({ page }) => {
69+
await page.goto(`/${component}`);
70+
await page.waitForLoadState('networkidle');
71+
// Allow JS to fully initialize
72+
await page.waitForTimeout(500);
73+
74+
const container = page.locator('.visual-test-container');
75+
await expect(container).toBeVisible();
76+
await expect(container).toHaveScreenshot(`${component}.png`);
77+
});
78+
}
79+
80+
/**
81+
* Components that load images from external URLs.
82+
* We wait for network idle to ensure images have loaded.
83+
*/
84+
const imageComponents = [
85+
'avatar',
86+
'carousel',
87+
'image',
88+
'lightbox',
89+
];
90+
91+
for (const component of imageComponents) {
92+
test(`image: ${component}`, async ({ page }) => {
93+
await page.goto(`/${component}`);
94+
await page.waitForLoadState('networkidle');
95+
// Allow images to fully render
96+
await page.waitForTimeout(1000);
97+
98+
const container = page.locator('.visual-test-container');
99+
await expect(container).toBeVisible();
100+
// Remote images (e.g. Gravatar) can have slight compression differences
101+
await expect(container).toHaveScreenshot(`${component}.png`, {
102+
maxDiffPixelRatio: 0.05,
103+
});
104+
});
105+
}
106+
107+
/**
108+
* Components that load external JS libraries (CDN).
109+
* These need longer timeouts for script loading and initialization.
110+
*/
111+
test('cdn: calendar', async ({ page }) => {
112+
await page.goto('/calendar');
113+
await page.waitForLoadState('networkidle');
114+
await page.waitForTimeout(1000);
115+
116+
const container = page.locator('.visual-test-container');
117+
await expect(container).toBeVisible();
118+
// Calendar dates change daily — use a higher tolerance
119+
await expect(container).toHaveScreenshot('calendar.png', {
120+
maxDiffPixelRatio: 0.05,
121+
});
122+
});
123+
124+
test('cdn: map', async ({ page }) => {
125+
await page.goto('/map');
126+
await page.waitForLoadState('networkidle');
127+
// Map tiles load asynchronously
128+
await page.waitForTimeout(3000);
129+
130+
const container = page.locator('.visual-test-container');
131+
await expect(container).toBeVisible();
132+
// Map tiles can vary slightly
133+
await expect(container).toHaveScreenshot('map.png', {
134+
maxDiffPixelRatio: 0.05,
135+
});
136+
});
137+
138+
test('cdn: parallax', async ({ page }) => {
139+
await page.goto('/parallax');
140+
await page.waitForLoadState('networkidle');
141+
await page.waitForTimeout(1000);
142+
143+
const container = page.locator('.visual-test-container');
144+
await expect(container).toBeVisible();
145+
await expect(container).toHaveScreenshot('parallax.png', {
146+
maxDiffPixelRatio: 0.02,
147+
});
148+
});
149+
150+
test('cdn: text', async ({ page }) => {
151+
await page.goto('/text');
152+
await page.waitForLoadState('networkidle');
153+
await page.waitForTimeout(1000);
154+
155+
const container = page.locator('.visual-test-container');
156+
await expect(container).toBeVisible();
157+
await expect(container).toHaveScreenshot('text.png', {
158+
maxDiffPixelRatio: 0.02,
159+
});
160+
});
161+
162+
test('cdn: tilt', async ({ page }) => {
163+
await page.goto('/tilt');
164+
await page.waitForLoadState('networkidle');
165+
await page.waitForTimeout(500);
166+
167+
const container = page.locator('.visual-test-container');
168+
await expect(container).toBeVisible();
169+
await expect(container).toHaveScreenshot('tilt.png');
170+
});
171+
172+
test('cdn: video', async ({ page }) => {
173+
await page.goto('/video');
174+
await page.waitForLoadState('networkidle');
175+
await page.waitForTimeout(2000);
176+
177+
const container = page.locator('.visual-test-container');
178+
await expect(container).toBeVisible();
179+
// Video player UI can vary slightly
180+
await expect(container).toHaveScreenshot('video.png', {
181+
maxDiffPixelRatio: 0.03,
182+
});
183+
});
184+
185+
/**
186+
* GithubGraph renders entirely via JS using localStorage.
187+
* The grid dates shift daily so we use a higher tolerance.
188+
*/
189+
test('inline-js: github-graph', async ({ page }) => {
190+
await page.goto('/github-graph');
191+
await page.waitForLoadState('domcontentloaded');
192+
193+
// Wait for JS to build the graph and set display:block
194+
const graph = page.locator('.github-contrib');
195+
await expect(graph).toBeVisible({ timeout: 10000 });
196+
197+
// Date-dependent rendering — higher tolerance
198+
await expect(graph).toHaveScreenshot('github-graph.png', {
199+
maxDiffPixelRatio: 0.05,
200+
});
201+
});
202+
203+
/**
204+
* Hero uses WebGL via Vanta.js — GPU-dependent rendering.
205+
* Higher tolerance needed for cross-machine consistency.
206+
*/
207+
test('webgl: hero', async ({ page }) => {
208+
await page.goto('/hero');
209+
await page.waitForLoadState('networkidle');
210+
// Allow Vanta.js WebGL to fully render
211+
await page.waitForTimeout(3000);
212+
213+
const container = page.locator('.visual-test-container');
214+
await expect(container).toBeVisible();
215+
// WebGL rendering varies by GPU — generous threshold
216+
await expect(container).toHaveScreenshot('hero.png', {
217+
maxDiffPixelRatio: 0.10,
218+
});
219+
});
30.6 KB
5.12 KB
13.1 KB
2.91 KB

0 commit comments

Comments
 (0)