补齐浏览器回归和发布指纹,更新应用访问说明
This commit is contained in:
@@ -5,3 +5,5 @@ __pycache__/
|
||||
!*.example
|
||||
test-results/
|
||||
themes/
|
||||
node_modules/
|
||||
playwright-report/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## 开发与发布
|
||||
|
||||
源码:~/projects/fzc-src/blog(本机);生产:ssh fzc。服务器只接收构建产物。
|
||||
构建要求 Python 3.10+ 和 uv;浏览器回归使用 Playwright,可通过 npm ci 安装。
|
||||
|
||||
- make install:安装固定版本构建依赖。
|
||||
- make test:文章完整性、站内链接、RSS、应用清单测试。
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@ showToc: false
|
||||
|
||||
**找到我:**
|
||||
- [GitHub](https://github.com/famz)
|
||||
- [Gitea](https://git.famzheng.com)
|
||||
- [Gitea](https://git.euphon.cloud/)
|
||||
|
||||
Generated
+71
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"private":true,"type":"module","scripts":{"test:browser":"playwright test"},"devDependencies":{"@playwright/test":"1.61.1"}}
|
||||
@@ -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}}}]});
|
||||
@@ -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();
|
||||
});
|
||||
+4
-3
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user