diff --git a/.gitignore b/.gitignore index 1cb3ad7..c8e2886 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ !*.example test-results/ themes/ +node_modules/ +playwright-report/ diff --git a/Makefile b/Makefile index c326ccd..844d5f6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ SHELL := /bin/bash .DEFAULT_GOAL := help PYTHON := .venv/bin/python -.PHONY: help install build test serve deploy +.PHONY: help install build test test-browser serve deploy help: @echo 'make install | build | test | serve | deploy' @echo '文章: content/posts/*.md;应用: apps.json;样式: assets/site.css' @@ -14,6 +14,9 @@ build: install $(PYTHON) build.py test: install $(PYTHON) -m unittest discover -s tests -v +test-browser: build + npm ci + npm run test:browser serve: build $(PYTHON) -m http.server 4176 --bind 127.0.0.1 --directory public deploy: test diff --git a/README.md b/README.md index 366331b..e6a9d9a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ ## 开发与发布 源码:~/projects/fzc-src/blog(本机);生产:ssh fzc。服务器只接收构建产物。 +构建要求 Python 3.10+ 和 uv;浏览器回归使用 Playwright,可通过 npm ci 安装。 - make install:安装固定版本构建依赖。 - make test:文章完整性、站内链接、RSS、应用清单测试。 diff --git a/apps.json b/apps.json index 1e0df34..20a11a6 100644 --- a/apps.json +++ b/apps.json @@ -2,7 +2,7 @@ {"name":"Music Lab","subtitle":"随身的音乐练习室","description":"找熟悉的旋律,收藏曲谱,用节拍器与鼓机陪你练习。","url":"https://music.famzheng.me/","category":"音乐","icon":"music","featured":true}, {"name":"Hedgehog","subtitle":"一桌人,一起玩","description":"从扫码入座开始,让小骰子成为聚会里的伙伴。","url":"https://hh.famzheng.me/","category":"互动","icon":"dice","featured":true,"access":"内测"}, {"name":"在场","subtitle":"把冷场交给骰子","description":"划一下换个性格,拍一拍聊两句。一个有声音的互动实验。","url":"https://dice.famzheng.me/","category":"互动","icon":"spark","featured":true,"access":"内测"}, - {"name":"Read","description":"把文档带进浏览器,留一段专心阅读的时间。","url":"https://read.famzheng.me/","category":"工具","icon":"book"}, + {"name":"Read","description":"把文档带进浏览器,留一段专心阅读的时间。","url":"https://read.famzheng.me/","category":"工具","icon":"book","access":"私人"}, {"name":"Track","description":"每日轨迹与日记,把走过的路留作自己的记录。","url":"https://track.famzheng.me/","category":"生活","icon":"route","access":"私人"}, {"name":"Notes","description":"随手记下念头,整理值得留下的片段。","url":"https://notes.famzheng.me/","category":"工具","icon":"note"}, {"name":"Write","description":"一个浏览器里的写作空间。","url":"https://write.famzheng.me/","category":"工具","icon":"note"}, @@ -10,7 +10,7 @@ {"name":"Riff","description":"围绕音乐的小站,继续探索旋律与节奏。","url":"https://riff.famzheng.me/","category":"音乐","icon":"music"}, {"name":"Partiverse","description":"多人小游戏的实验场。","url":"https://party.famzheng.me/","category":"互动","icon":"dice"}, {"name":"Werewolf","description":"狼人杀,让推理与演技在桌边碰面。","url":"https://werewolf.famzheng.me/","category":"互动","icon":"spark"}, - {"name":"Cube","description":"浏览器里的立方体实验。","url":"https://cube.famzheng.me/","category":"互动","icon":"cube"}, + {"name":"Cube","description":"小应用平台,把常用的工具和玩具收在一起。","url":"https://cube.famzheng.me/","category":"工具","icon":"cube"}, {"name":"WebGL","description":"光、空间和图形,直接在浏览器里玩。","url":"https://webgl.famzheng.me/","category":"实验","icon":"cube"}, {"name":"Code Space","description":"换个视角,走进一座由代码组成的三维空间。","url":"/demos/code-space-browser.html","category":"实验","icon":"cube"}, {"name":"SimpleASM","description":"一个探索汇编与底层计算的小工具。","url":"https://asm.famzheng.me/","category":"实验","icon":"code"}, diff --git a/build.py b/build.py index a3bd92f..08b452d 100644 --- a/build.py +++ b/build.py @@ -128,7 +128,12 @@ def build(): write('sitemap.xml', ET.tostring(site,encoding='unicode')) write('robots.txt', f'User-agent: *\nAllow: /\nSitemap: {BASE}/sitemap.xml\n') write('index.json', json.dumps([{'title':p['title'],'permalink':BASE+p['url'],'summary':p.get('summary',''),'content':p['raw']} for p in posts],ensure_ascii=False)) - write('release.json', json.dumps({'site':'famzheng.me','posts':len(posts),'apps':len(apps),'content_sha256':hashlib.sha256(''.join(p['raw'] for p in posts).encode()).hexdigest()})) + inputs = [ROOT/'build.py', ROOT/'apps.json', ROOT/'requirements.txt'] + sorted((ROOT/'assets').rglob('*')) + sorted((ROOT/'content').rglob('*')) + sorted((ROOT/'static').rglob('*')) + digest = hashlib.sha256() + for path in inputs: + if path.is_file(): + digest.update(str(path.relative_to(ROOT)).encode()); digest.update(path.read_bytes()) + write('release.json', json.dumps({'site':'famzheng.me','posts':len(posts),'apps':len(apps),'build_sha256':digest.hexdigest()})) print(f'Built {len(posts)} posts and {len(apps)} applications → {OUT}') if __name__ == '__main__': build() diff --git a/content/about.md b/content/about.md index 33187f6..907f680 100644 --- a/content/about.md +++ b/content/about.md @@ -10,4 +10,4 @@ showToc: false **找到我:** - [GitHub](https://github.com/famz) -- [Gitea](https://git.famzheng.com) +- [Gitea](https://git.euphon.cloud/) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4b3b004 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,71 @@ +{ + "name": "blog", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@playwright/test": "1.61.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fba8224 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"private":true,"type":"module","scripts":{"test:browser":"playwright test"},"devDependencies":{"@playwright/test":"1.61.1"}} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..303f46e --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,2 @@ +import { defineConfig } from '@playwright/test'; +export default defineConfig({testDir:'tests/browser',workers:1,webServer:{command:'make serve',url:'http://127.0.0.1:4176',reuseExistingServer:true},use:{baseURL:'http://127.0.0.1:4176'},projects:[{name:'desktop',use:{viewport:{width:1440,height:1000}}},{name:'mobile',use:{viewport:{width:390,height:844}}},{name:'small',use:{viewport:{width:320,height:700}}}]}); diff --git a/tests/browser/site.spec.js b/tests/browser/site.spec.js new file mode 100644 index 0000000..47fef84 --- /dev/null +++ b/tests/browser/site.spec.js @@ -0,0 +1,24 @@ +import {test,expect} from '@playwright/test'; +test('应用导航、搜索、旧文章与手机布局', async ({page})=>{ + const errors=[]; page.on('pageerror',e=>errors.push(e.message)); + await page.goto('/'); + await expect(page.locator('#app-grid .app-card')).toHaveCount(23); + expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true); + await page.getByRole('button',{name:'音乐',exact:true}).click(); + await expect(page.locator('#app-grid .app-card:visible')).toHaveCount(3); + await page.getByRole('button',{name:'全部',exact:true}).click(); + await page.getByRole('searchbox').fill('hedgehog'); + await expect(page.locator('#app-grid .app-card:visible')).toHaveCount(1); + await page.getByRole('searchbox').fill('nothing-to-match'); + await expect(page.locator('#empty')).toBeVisible(); + await page.goto('/posts/claude-code-goal/'); + await expect(page.locator('h1')).toContainText('Claude Code'); + expect(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth)).toBe(true); + expect(errors).toEqual([]); +}); +test('没有 JavaScript 仍能访问所有入口',async({browser})=>{ + const context=await browser.newContext({javaScriptEnabled:false}); + const page=await context.newPage();await page.goto('http://127.0.0.1:4176/'); + await expect(page.locator('#app-grid .app-card:visible')).toHaveCount(23); + await context.close(); +}); diff --git a/tests/test_site.py b/tests/test_site.py index cde4bf5..a9cc935 100644 --- a/tests/test_site.py +++ b/tests/test_site.py @@ -38,11 +38,12 @@ class SiteTest(unittest.TestCase): self.assertNotIn('PaperMod', (build.OUT/'index.html').read_text()) self.assertNotIn('Hugo', (build.OUT/'index.html').read_text()) def test_feeds(self): - self.assertEqual(len(ET.parse(build.OUT/'index.xml').findall('./channel/item')), 11) - self.assertEqual(len(ET.parse(build.OUT/'sitemap.xml').getroot()), 14) + count = sum(not build.load_post(p).get('draft') for p in (build.ROOT/'content/posts').glob('*.md')) + self.assertEqual(len(ET.parse(build.OUT/'index.xml').findall('./channel/item')), count) + self.assertEqual(len(ET.parse(build.OUT/'sitemap.xml').getroot()), count + 3) def test_app_registry(self): apps = json.loads((build.ROOT/'apps.json').read_text()) - self.assertEqual(len(apps), 23) + self.assertGreaterEqual(len(apps), 23) self.assertTrue(all(not urlsplit(a['url']).query and not urlsplit(a['url']).fragment for a in apps)) self.assertTrue(all(a['name'] and a['description'] and a['icon'] in build.ICONS for a in apps))