Verify optional font build cache and allow remote UI acceptance
This commit is contained in:
@@ -15,3 +15,6 @@ build/
|
|||||||
dist/
|
dist/
|
||||||
|
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
build-cache/debs/*
|
||||||
|
!build-cache/debs/.gitkeep
|
||||||
|
|||||||
+10
-2
@@ -7,8 +7,16 @@ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 TZ=Asia/Shanghai \
|
|||||||
DATA_DIR=/data CONFIG_DIR=/config BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME
|
DATA_DIR=/data CONFIG_DIR=/config BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# CJK fonts intentionally retained for readable Chinese rendering.
|
# CJK fonts intentionally retained for readable Chinese rendering.
|
||||||
RUN sed -i "s|http://deb.debian.org|https://${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources \
|
RUN --mount=type=bind,source=build-cache/debs,target=/build-debs,readonly sed -i "s|http://deb.debian.org|https://${DEBIAN_MIRROR}|g" /etc/apt/sources.list.d/debian.sources \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends fonts-noto-cjk tzdata \
|
&& apt-get update \
|
||||||
|
&& if [ -f /build-debs/fonts-noto-cjk.deb ]; then \
|
||||||
|
font_version=$(apt-cache policy fonts-noto-cjk | awk '$1=="Candidate:" {print $2}'); \
|
||||||
|
font_sha=$(apt-cache show "fonts-noto-cjk=$font_version" | awk '$1=="SHA256:" {print $2;exit}'); \
|
||||||
|
test -n "$font_sha"; \
|
||||||
|
echo "$font_sha /build-debs/fonts-noto-cjk.deb" | sha256sum -c -; \
|
||||||
|
cp /build-debs/fonts-noto-cjk.deb "/var/cache/apt/archives/fonts-noto-cjk_${font_version#*:}_all.deb"; \
|
||||||
|
fi \
|
||||||
|
&& apt-get install -y --no-install-recommends fonts-noto-cjk tzdata \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& groupadd --gid 10333 frameflow && useradd --uid 10333 --gid 10333 --no-create-home frameflow \
|
&& groupadd --gid 10333 frameflow && useradd --uid 10333 --gid 10333 --no-create-home frameflow \
|
||||||
&& mkdir -p /data/images /data/generated /data/cache /config \
|
&& mkdir -p /data/images /data/generated /data/cache /config \
|
||||||
|
|||||||
@@ -133,3 +133,7 @@ verification and changing no NAS system settings:
|
|||||||
```sh
|
```sh
|
||||||
sudo /usr/local/bin/docker compose build --build-arg DEBIAN_MIRROR=mirrors.aliyun.com
|
sudo /usr/local/bin/docker compose build --build-arg DEBIAN_MIRROR=mirrors.aliyun.com
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional `build-cache/debs/fonts-noto-cjk.deb` avoids a slow build-network download.
|
||||||
|
Dockerfile verifies it against SHA-256 from the GPG-authenticated APT index before use.
|
||||||
|
The cache is not a runtime mount or committed dependency, and empty-cache builds work.
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
/* Headless test of this app only; never attaches to the user's browser session. */
|
/* Headless test of this app only; never attaches to the user's browser session. */
|
||||||
const fs=require('fs');const path=require('path');const assert=require('assert');const {chromium}=require('playwright');
|
const fs=require('fs');const path=require('path');const assert=require('assert');const {chromium}=require('playwright');
|
||||||
(async()=>{
|
(async()=>{
|
||||||
const root=path.resolve(__dirname,'..');const env=fs.readFileSync(path.join(root,'.env'),'utf8');const token=env.split('\n').find(x=>x.startsWith('FRAME_FLOW_TOKEN=')).slice('FRAME_FLOW_TOKEN='.length);
|
const root=path.resolve(__dirname,'..');const env=fs.readFileSync(process.env.FRAME_FLOW_ENV_FILE||path.join(root,'.env'),'utf8');const token=env.split('\n').find(x=>x.startsWith('FRAME_FLOW_TOKEN=')).slice('FRAME_FLOW_TOKEN='.length);
|
||||||
const base='http://127.0.0.1:18787';const response=await fetch(base+'/api/v1/settings',{headers:{Authorization:'Bearer '+token}});const settings=await response.json();assert(settings.mock===true,'UI smoke requires Mock mode');
|
const base=process.env.FRAME_FLOW_URL||'http://127.0.0.1:18787';const response=await fetch(base+'/api/v1/settings',{headers:{Authorization:'Bearer '+token}});const settings=await response.json();assert(settings.mock===true,'UI smoke requires Mock mode');
|
||||||
const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
|
const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
|
||||||
const page=await browser.newPage({viewport:{width:1440,height:1000}});const errors=[];page.on('pageerror',e=>errors.push(e.message.replaceAll(token,'[REDACTED]')));let checks=0;
|
const page=await browser.newPage({viewport:{width:1440,height:1000}});const errors=[];page.on('pageerror',e=>errors.push(e.message.replaceAll(token,'[REDACTED]')));let checks=0;
|
||||||
await page.goto(base);await page.locator('#api-token').fill(token);await page.locator('#login-form button').click();await page.locator('.hero').waitFor();assert(await page.locator('h2').first().innerText()==='客厅相框');checks++;
|
await page.goto(base);await page.locator('#api-token').fill(token);await page.locator('#login-form button').click();await page.locator('.hero').waitFor();assert(await page.locator('h2').first().innerText()==='客厅相框');checks++;
|
||||||
fs.mkdirSync(path.join(root,'docs/screenshots'),{recursive:true});await page.screenshot({path:path.join(root,'docs/screenshots/dashboard-light-zh.png'),fullPage:true});
|
const shots=process.env.FRAME_FLOW_SCREENSHOT_DIR||path.join(root,'docs/screenshots');fs.mkdirSync(shots,{recursive:true});await page.screenshot({path:path.join(shots,'dashboard-light-zh.png'),fullPage:true});
|
||||||
await page.locator('#lang').click();await page.getByRole('heading',{name:'Dashboard',exact:true}).waitFor();checks++;
|
await page.locator('#lang').click();await page.getByRole('heading',{name:'Dashboard',exact:true}).waitFor();checks++;
|
||||||
await page.locator('#theme').click();assert(await page.locator('html').getAttribute('data-theme')==='dark');checks++;
|
await page.locator('#theme').click();assert(await page.locator('html').getAttribute('data-theme')==='dark');checks++;
|
||||||
await page.screenshot({path:path.join(root,'docs/screenshots/dashboard-dark-en.png'),fullPage:true});
|
await page.screenshot({path:path.join(shots,'dashboard-dark-en.png'),fullPage:true});
|
||||||
await page.locator('[data-page="gallery"]').click();await page.locator('.image-card').first().waitFor();await page.locator('.image-card .thumb').first().evaluate(img=>new Promise((resolve,reject)=>{if(img.complete&&img.naturalWidth)return resolve();img.onload=resolve;img.onerror=reject;}));checks++;
|
await page.locator('[data-page="gallery"]').click();await page.locator('.image-card').first().waitFor();await page.locator('.image-card .thumb').first().evaluate(img=>new Promise((resolve,reject)=>{if(img.complete&&img.naturalWidth)return resolve();img.onload=resolve;img.onerror=reject;}));checks++;
|
||||||
await page.locator('#show-name').click();await page.locator('#image-name').fill('Original Art.jpg');await page.locator('#modal-cancel').click();checks++;
|
await page.locator('#show-name').click();await page.locator('#image-name').fill('Original Art.jpg');await page.locator('#modal-cancel').click();checks++;
|
||||||
await page.locator('[data-page="upload"]').click();await page.locator('#file').setInputFiles({name:'browser-upload.png',mimeType:'image/png',buffer:Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a1OIAAAAASUVORK5CYII=','base64')});checks++;
|
await page.locator('[data-page="upload"]').click();await page.locator('#file').setInputFiles({name:'browser-upload.png',mimeType:'image/png',buffer:Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a1OIAAAAASUVORK5CYII=','base64')});checks++;
|
||||||
@@ -27,7 +27,7 @@ const fs=require('fs');const path=require('path');const assert=require('assert')
|
|||||||
await page.locator('[data-page="settings"]').click();await page.locator('#theme-select').waitFor();await page.locator('#theme-select').selectOption('light');assert(await page.locator('html').getAttribute('data-theme')==='light');checks++;
|
await page.locator('[data-page="settings"]').click();await page.locator('#theme-select').waitFor();await page.locator('#theme-select').selectOption('light');assert(await page.locator('html').getAttribute('data-theme')==='light');checks++;
|
||||||
await page.locator('#language-select').selectOption('zh');await page.getByRole('heading',{name:'设置',exact:true}).waitFor();checks++;
|
await page.locator('#language-select').selectOption('zh');await page.getByRole('heading',{name:'设置',exact:true}).waitFor();checks++;
|
||||||
await page.setViewportSize({width:390,height:844});await page.locator('[data-page="dashboard"]').click();await page.locator('.hero').waitFor();assert(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth));checks++;
|
await page.setViewportSize({width:390,height:844});await page.locator('[data-page="dashboard"]').click();await page.locator('.hero').waitFor();assert(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth));checks++;
|
||||||
await page.screenshot({path:path.join(root,'docs/screenshots/dashboard-mobile-zh.png'),fullPage:true});
|
await page.screenshot({path:path.join(shots,'dashboard-mobile-zh.png'),fullPage:true});
|
||||||
assert(errors.length===0,JSON.stringify(errors));checks++;
|
assert(errors.length===0,JSON.stringify(errors));checks++;
|
||||||
await browser.close();console.log('Browser smoke:',checks,'passed; page errors:',errors.length);
|
await browser.close();console.log('Browser smoke:',checks,'passed; page errors:',errors.length);
|
||||||
})().catch(error=>{console.error(error.message);process.exit(1)});
|
})().catch(error=>{console.error(error.message);process.exit(1)});
|
||||||
|
|||||||
Reference in New Issue
Block a user