Compare commits
2
Commits
a0fefc5041
...
41114eb0ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41114eb0ad | ||
|
|
acfa02f96d |
@@ -5,3 +5,5 @@ __pycache__/
|
|||||||
!*.example
|
!*.example
|
||||||
test-results/
|
test-results/
|
||||||
themes/
|
themes/
|
||||||
|
node_modules/
|
||||||
|
playwright-report/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
PYTHON := .venv/bin/python
|
PYTHON := .venv/bin/python
|
||||||
.PHONY: help install build test serve deploy
|
.PHONY: help install build test test-browser serve deploy
|
||||||
help:
|
help:
|
||||||
@echo 'make install | build | test | serve | deploy'
|
@echo 'make install | build | test | serve | deploy'
|
||||||
@echo '文章: content/posts/*.md;应用: apps.json;样式: assets/site.css'
|
@echo '文章: content/posts/*.md;应用: apps.json;样式: assets/site.css'
|
||||||
@@ -14,6 +14,9 @@ build: install
|
|||||||
$(PYTHON) build.py
|
$(PYTHON) build.py
|
||||||
test: install
|
test: install
|
||||||
$(PYTHON) -m unittest discover -s tests -v
|
$(PYTHON) -m unittest discover -s tests -v
|
||||||
|
test-browser: build
|
||||||
|
npm ci
|
||||||
|
npm run test:browser
|
||||||
serve: build
|
serve: build
|
||||||
$(PYTHON) -m http.server 4176 --bind 127.0.0.1 --directory public
|
$(PYTHON) -m http.server 4176 --bind 127.0.0.1 --directory public
|
||||||
deploy: test
|
deploy: test
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
## 开发与发布
|
## 开发与发布
|
||||||
|
|
||||||
源码:~/projects/fzc-src/blog(本机);生产:ssh fzc。服务器只接收构建产物。
|
源码:~/projects/fzc-src/blog(本机);生产:ssh fzc。服务器只接收构建产物。
|
||||||
|
构建要求 Python 3.10+ 和 uv;浏览器回归使用 Playwright,可通过 npm ci 安装。
|
||||||
|
|
||||||
- make install:安装固定版本构建依赖。
|
- make install:安装固定版本构建依赖。
|
||||||
- make test:文章完整性、站内链接、RSS、应用清单测试。
|
- make test:文章完整性、站内链接、RSS、应用清单测试。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{"name":"Music Lab","subtitle":"随身的音乐练习室","description":"找熟悉的旋律,收藏曲谱,用节拍器与鼓机陪你练习。","url":"https://music.famzheng.me/","category":"音乐","icon":"music","featured":true},
|
{"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":"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":"在场","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":"Track","description":"每日轨迹与日记,把走过的路留作自己的记录。","url":"https://track.famzheng.me/","category":"生活","icon":"route","access":"私人"},
|
||||||
{"name":"Notes","description":"随手记下念头,整理值得留下的片段。","url":"https://notes.famzheng.me/","category":"工具","icon":"note"},
|
{"name":"Notes","description":"随手记下念头,整理值得留下的片段。","url":"https://notes.famzheng.me/","category":"工具","icon":"note"},
|
||||||
{"name":"Write","description":"一个浏览器里的写作空间。","url":"https://write.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":"Riff","description":"围绕音乐的小站,继续探索旋律与节奏。","url":"https://riff.famzheng.me/","category":"音乐","icon":"music"},
|
||||||
{"name":"Partiverse","description":"多人小游戏的实验场。","url":"https://party.famzheng.me/","category":"互动","icon":"dice"},
|
{"name":"Partiverse","description":"多人小游戏的实验场。","url":"https://party.famzheng.me/","category":"互动","icon":"dice"},
|
||||||
{"name":"Werewolf","description":"狼人杀,让推理与演技在桌边碰面。","url":"https://werewolf.famzheng.me/","category":"互动","icon":"spark"},
|
{"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":"WebGL","description":"光、空间和图形,直接在浏览器里玩。","url":"https://webgl.famzheng.me/","category":"实验","icon":"cube"},
|
||||||
{"name":"Code Space","description":"换个视角,走进一座由代码组成的三维空间。","url":"/demos/code-space-browser.html","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"},
|
{"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('sitemap.xml', ET.tostring(site,encoding='unicode'))
|
||||||
write('robots.txt', f'User-agent: *\nAllow: /\nSitemap: {BASE}/sitemap.xml\n')
|
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('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}')
|
print(f'Built {len(posts)} posts and {len(apps)} applications → {OUT}')
|
||||||
|
|
||||||
if __name__ == '__main__': build()
|
if __name__ == '__main__': build()
|
||||||
|
|||||||
+1
-1
@@ -10,4 +10,4 @@ showToc: false
|
|||||||
|
|
||||||
**找到我:**
|
**找到我:**
|
||||||
- [GitHub](https://github.com/famz)
|
- [GitHub](https://github.com/famz)
|
||||||
- [Gitea](https://git.famzheng.com)
|
- [Gitea](https://git.euphon.cloud/)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def main():
|
|||||||
ssh('mkdir','-p',target)
|
ssh('mkdir','-p',target)
|
||||||
run('rsync','-az','--chmod=D755,F644',str(ROOT/'public')+'/',f'{HOST}:{target}/')
|
run('rsync','-az','--chmod=D755,F644',str(ROOT/'public')+'/',f'{HOST}:{target}/')
|
||||||
def point(path):
|
def point(path):
|
||||||
patch={'spec':{'template':{'spec':{'volumes':[{'name':'site','hostPath':{'path':path,'type':'Directory'}}]}}}}
|
patch={'spec':{'strategy':{'type':'RollingUpdate','rollingUpdate':{'maxUnavailable':0,'maxSurge':1}},'template':{'spec':{'volumes':[{'name':'site','hostPath':{'path':path,'type':'Directory'}}]}}}}
|
||||||
kube('patch','deployment','hugo','--type=strategic','-p',json.dumps(patch))
|
kube('patch','deployment','hugo','--type=strategic','-p',json.dumps(patch))
|
||||||
kube('rollout','status','deployment/hugo','--timeout=120s')
|
kube('rollout','status','deployment/hugo','--timeout=120s')
|
||||||
try:
|
try:
|
||||||
|
|||||||
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('PaperMod', (build.OUT/'index.html').read_text())
|
||||||
self.assertNotIn('Hugo', (build.OUT/'index.html').read_text())
|
self.assertNotIn('Hugo', (build.OUT/'index.html').read_text())
|
||||||
def test_feeds(self):
|
def test_feeds(self):
|
||||||
self.assertEqual(len(ET.parse(build.OUT/'index.xml').findall('./channel/item')), 11)
|
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/'sitemap.xml').getroot()), 14)
|
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):
|
def test_app_registry(self):
|
||||||
apps = json.loads((build.ROOT/'apps.json').read_text())
|
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(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))
|
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