Remove live-device UI labels and clean persistence acceptance jobs
This commit is contained in:
@@ -22,11 +22,11 @@ async function api(path,options={}){
|
|||||||
function bind(id,fn){const element=$('#'+id);if(element)element.onclick=async event=>{try{element.disabled=true;await fn(event);}catch(e){toast(e.message);}finally{element.disabled=false;}};}
|
function bind(id,fn){const element=$('#'+id);if(element)element.onclick=async event=>{try{element.disabled=true;await fn(event);}catch(e){toast(e.message);}finally{element.disabled=false;}};}
|
||||||
function busy(fn){return async event=>{try{await fn(event);}catch(e){toast(e.message);}};}
|
function busy(fn){return async event=>{try{await fn(event);}catch(e){toast(e.message);}};}
|
||||||
function applyTheme(){const dark=state.theme==='dark'||(state.theme==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);document.documentElement.dataset.theme=dark?'dark':'light';}
|
function applyTheme(){const dark=state.theme==='dark'||(state.theme==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);document.documentElement.dataset.theme=dark?'dark':'light';}
|
||||||
function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).map(([id,icon])=>`<button data-page="${id}" class="${id===state.page?'active':''}"><span class="nav-icon">${icon}</span><span>${t(id)}</span></button>`).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);$('#connection').textContent=state.settings?(state.settings.mock?t('mock'):(!state.settings.write_enabled?t('readOnly'):t('writable'))):t('noToken');$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>`<option value="${esc(d.id)}" ${d.id===state.device?'selected':''}>${esc(d.name)}</option>`).join('');}
|
function shell(){document.documentElement.lang=state.lang==='zh'?'zh-CN':'en';$('#lang').textContent=state.lang==='zh'?'EN':'中文';$('#theme').title=t('theme');$('#nav').innerHTML=Object.entries(icons).map(([id,icon])=>`<button data-page="${id}" class="${id===state.page?'active':''}"><span class="nav-icon">${icon}</span><span>${t(id)}</span></button>`).join('');document.querySelectorAll('[data-page]').forEach(b=>b.onclick=()=>navigate(b.dataset.page));$('#page-title').textContent=t(state.page);$('#device').innerHTML=(state.devices.length?state.devices:[{id:state.device,name:state.device}]).map(d=>`<option value="${esc(d.id)}" ${d.id===state.device?'selected':''}>${esc(d.name)}</option>`).join('');}
|
||||||
function clearBlobs(){for(const url of state.blobs)URL.revokeObjectURL(url);state.blobs=[];}
|
function clearBlobs(){for(const url of state.blobs)URL.revokeObjectURL(url);state.blobs=[];}
|
||||||
async function blobImage(img,path){try{const blob=await api(path,{binary:true});if(!img.isConnected)return;const url=URL.createObjectURL(blob);state.blobs.push(url);img.src=url;}catch{img.alt=t('apiError');}}
|
async function blobImage(img,path){try{const blob=await api(path,{binary:true});if(!img.isConnected)return;const url=URL.createObjectURL(blob);state.blobs.push(url);img.src=url;}catch{img.alt=t('apiError');}}
|
||||||
function hydrateImages(){const observer=new IntersectionObserver(entries=>entries.forEach(entry=>{if(entry.isIntersecting){observer.unobserve(entry.target);blobImage(entry.target,entry.target.dataset.src);}}),{rootMargin:'100px'});document.querySelectorAll('img[data-src]').forEach(img=>observer.observe(img));}
|
function hydrateImages(){const observer=new IntersectionObserver(entries=>entries.forEach(entry=>{if(entry.isIntersecting){observer.unobserve(entry.target);blobImage(entry.target,entry.target.dataset.src);}}),{rootMargin:'100px'});document.querySelectorAll('img[data-src]').forEach(img=>observer.observe(img));}
|
||||||
function notice(){if(!state.settings)return '';return `<div class="notice ${!state.settings.mock&&!state.settings.write_enabled?'warning':''}">${state.settings.mock?t('mock'):(!state.settings.write_enabled?t('readOnlyWarning'):t('writable'))}</div>`;}
|
function notice(){return state.settings?.mock?`<div class="notice">${t('mock')}</div>`:'';}
|
||||||
async function navigate(page=state.page){state.page=icons[page]?page:'dashboard';location.hash=state.page;shell();clearBlobs();state.epoch++;const epoch=state.epoch;
|
async function navigate(page=state.page){state.page=icons[page]?page:'dashboard';location.hash=state.page;shell();clearBlobs();state.epoch++;const epoch=state.epoch;
|
||||||
if(!state.token){login();return;}
|
if(!state.token){login();return;}
|
||||||
$('#main').innerHTML='<div class="spinner"></div>';
|
$('#main').innerHTML='<div class="spinner"></div>';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>Pigway Frame Flow</title><link rel="stylesheet" href="/ui/style.css"></head>
|
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="light dark"><title>Pigway Frame Flow</title><link rel="stylesheet" href="/ui/style.css"></head>
|
||||||
<body><aside class="sidebar"><a class="brand" href="/"><span class="brand-icon">F<span>↗</span></span><span>Frame Flow<small>Pigway · Smart Frame Automation</small></span></a><nav id="nav"></nav><div class="sidebar-bottom"><span class="live-dot"></span><span id="connection">—</span><small>Version 0.1.0</small></div></aside><div class="workspace"><header><div><span class="eyebrow">PIGWAY FRAME FLOW</span><h1 id="page-title"></h1></div><div class="toolbar"><select id="device" aria-label="Device"></select><button id="lang" class="icon-button" aria-label="Change language">EN</button><button id="theme" class="icon-button" aria-label="Change theme">◐</button></div></header><main id="main"></main><footer id="footer"></footer></div><div id="toast" role="status" aria-live="polite"></div><dialog id="modal"><div id="modal-content"></div></dialog><script src="/ui/i18n.js"></script><script src="/ui/features.js"></script><script src="/ui/app.js"></script></body></html>
|
<body><aside class="sidebar"><a class="brand" href="/"><span class="brand-icon">F<span>↗</span></span><span>Frame Flow<small>Pigway · Smart Frame Automation</small></span></a><nav id="nav"></nav><div class="sidebar-bottom"><small>Version 0.1.0</small></div></aside><div class="workspace"><header><div><span class="eyebrow">PIGWAY FRAME FLOW</span><h1 id="page-title"></h1></div><div class="toolbar"><select id="device" aria-label="Device"></select><button id="lang" class="icon-button" aria-label="Change language">EN</button><button id="theme" class="icon-button" aria-label="Change theme">◐</button></div></header><main id="main"></main><footer id="footer"></footer></div><div id="toast" role="status" aria-live="polite"></div><dialog id="modal"><div id="modal-content"></div></dialog><script src="/ui/i18n.js"></script><script src="/ui/features.js"></script><script src="/ui/app.js"></script></body></html>
|
||||||
|
|||||||
@@ -29,12 +29,15 @@ def api(path, method="GET", payload=None):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
with urlopen(request, timeout=10) as response:
|
with urlopen(request, timeout=10) as response:
|
||||||
return json.loads(response.read())
|
body = response.read()
|
||||||
|
return json.loads(body) if body else None
|
||||||
|
|
||||||
|
|
||||||
assert api("/api/v1/settings")["mock"] is True, "Persistence test requires Mock mode"
|
assert api("/api/v1/settings")["mock"] is True, "Persistence test requires Mock mode"
|
||||||
record = Path("data/persistence-acceptance.json")
|
record = Path("data/persistence-acceptance.json")
|
||||||
if args.mode == "record":
|
if args.mode == "record":
|
||||||
|
if record.exists():
|
||||||
|
raise SystemExit("An acceptance record already exists; run check first")
|
||||||
job = api(
|
job = api(
|
||||||
"/api/v1/jobs",
|
"/api/v1/jobs",
|
||||||
"POST",
|
"POST",
|
||||||
@@ -76,4 +79,6 @@ else:
|
|||||||
assert api("/api/v1/operations/" + saved["operation_id"])["status"] == "COMPLETED"
|
assert api("/api/v1/operations/" + saved["operation_id"])["status"] == "COMPLETED"
|
||||||
assert any(a["asset_id"] == saved["asset_id"] for a in api("/api/v1/assets"))
|
assert any(a["asset_id"] == saved["asset_id"] for a in api("/api/v1/assets"))
|
||||||
assert api("/ready")["status"] == "ready"
|
assert api("/ready")["status"] == "ready"
|
||||||
print("Restart persistence: 4 checks passed (job/operation/asset/readiness)")
|
api("/api/v1/jobs/" + saved["job_id"], "DELETE")
|
||||||
|
record.unlink()
|
||||||
|
print("Restart persistence: 4 checks passed; acceptance job removed")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* All-page layout and daily-poetry checks; Mock only, no real device writes. */
|
/* All-page layout and daily-poetry checks; Mock only, no real device writes. */
|
||||||
const assert=require('assert'),{chromium}=require('playwright');
|
const assert=require('assert'),{chromium}=require('playwright');
|
||||||
(async()=>{const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});const page=await browser.newPage();let unintended=0;page.on('request',r=>{if(r.method()==='POST'&&/\/(jobs|playlists|display)$/.test(r.url()))unintended++;});const base='http://127.0.0.1:18787';await page.goto(base);await page.locator('#username').fill(process.env.FRAME_FLOW_TEST_USERNAME);await page.locator('#password').fill(process.env.FRAME_FLOW_TEST_PASSWORD);await page.locator('#login-form button').click();await page.locator('.hero').waitFor();assert(await page.evaluate(()=>state.settings.mock));let checks=0;for(const width of [1440,390])for(const theme of ['light','dark']){await page.setViewportSize({width,height:1000});await page.evaluate(theme=>{localStorage.setItem('ff-theme',theme);document.documentElement.dataset.theme=theme;},theme);for(const name of ['dashboard','gallery','upload','templates','automation','playlists','history','settings']){await page.locator(`[data-page="${name}"]`).click();await page.waitForTimeout(350);if(name==='settings')await page.locator('#image-host-form').waitFor();const result=await page.evaluate(()=>{const children=[...document.querySelector('#main').children].filter(x=>x.getBoundingClientRect().height>0);return {overflow:document.documentElement.scrollWidth>innerWidth,buttonGaps:[...document.querySelectorAll('#main button')].flatMap(b=>{const next=b.nextElementSibling;if(next?.tagName!=='BUTTON')return [];const a=b.getBoundingClientRect(),c=next.getBoundingClientRect();if(!a.width||!c.width)return [];return [Math.abs(a.top-c.top)<4?c.left-a.right:c.top-a.bottom];}),gaps:children.slice(1).map((x,i)=>x.getBoundingClientRect().top-children[i].getBoundingClientRect().bottom)};});const controls=await page.evaluate(()=>{const nodes=[...document.querySelectorAll('#main input:not([type="checkbox"]):not([type="radio"]),#main select,#main textarea,#main button')].filter(x=>{const r=x.getBoundingClientRect();return r.width&&r.height&&getComputedStyle(x).visibility!=='hidden';});const issues=[];for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){const a=nodes[i].getBoundingClientRect(),b=nodes[j].getBoundingClientRect();const ox=Math.min(a.right,b.right)-Math.max(a.left,b.left),oy=Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top);const dy=Math.max(a.top,b.top)-Math.min(a.bottom,b.bottom),dx=Math.max(a.left,b.left)-Math.min(a.right,b.right);if((ox>Math.min(a.width,b.width)/2&&dy>=0&&dy<8)||(oy>Math.min(a.height,b.height)/2&&dx>=0&&dx<8))issues.push({a:nodes[i].id,b:nodes[j].id,gap:ox>0?dy:dx});}return issues;});assert(controls.length===0,`${name} ${width} ${theme} controls ${JSON.stringify(controls)}`);checks++;
|
(async()=>{const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});const page=await browser.newPage();let unintended=0;page.on('request',r=>{if(r.method()==='POST'&&/\/(jobs|playlists|display)$/.test(r.url()))unintended++;});const base='http://127.0.0.1:18787';await page.goto(base);await page.locator('#username').fill(process.env.FRAME_FLOW_TEST_USERNAME);await page.locator('#password').fill(process.env.FRAME_FLOW_TEST_PASSWORD);await page.locator('#login-form button').click();await page.locator('.hero').waitFor();assert(await page.evaluate(()=>state.settings.mock));let checks=0;for(const lang of ['zh','en'])for(const writable of [false,true]){const clean=await page.evaluate(({lang,writable})=>{const previous={lang:state.lang,settings:state.settings};state.lang=lang;state.settings={...state.settings,mock:false,write_enabled:writable};shell();const clean=notice()===''&&!document.querySelector('#connection')&&!document.querySelector('.sidebar-bottom .live-dot');state.lang=previous.lang;state.settings=previous.settings;shell();return clean;},{lang,writable});assert(clean,'Live-device status must not appear in either language');checks++;}for(const width of [1440,390])for(const theme of ['light','dark']){await page.setViewportSize({width,height:1000});await page.evaluate(theme=>{localStorage.setItem('ff-theme',theme);document.documentElement.dataset.theme=theme;},theme);for(const name of ['dashboard','gallery','upload','templates','automation','playlists','history','settings']){await page.locator(`[data-page="${name}"]`).click();await page.waitForTimeout(350);if(name==='settings')await page.locator('#image-host-form').waitFor();const result=await page.evaluate(()=>{const children=[...document.querySelector('#main').children].filter(x=>x.getBoundingClientRect().height>0);return {overflow:document.documentElement.scrollWidth>innerWidth,buttonGaps:[...document.querySelectorAll('#main button')].flatMap(b=>{const next=b.nextElementSibling;if(next?.tagName!=='BUTTON')return [];const a=b.getBoundingClientRect(),c=next.getBoundingClientRect();if(!a.width||!c.width)return [];return [Math.abs(a.top-c.top)<4?c.left-a.right:c.top-a.bottom];}),gaps:children.slice(1).map((x,i)=>x.getBoundingClientRect().top-children[i].getBoundingClientRect().bottom)};});const controls=await page.evaluate(()=>{const nodes=[...document.querySelectorAll('#main input:not([type="checkbox"]):not([type="radio"]),#main select,#main textarea,#main button')].filter(x=>{const r=x.getBoundingClientRect();return r.width&&r.height&&getComputedStyle(x).visibility!=='hidden';});const issues=[];for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){const a=nodes[i].getBoundingClientRect(),b=nodes[j].getBoundingClientRect();const ox=Math.min(a.right,b.right)-Math.max(a.left,b.left),oy=Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top);const dy=Math.max(a.top,b.top)-Math.min(a.bottom,b.bottom),dx=Math.max(a.left,b.left)-Math.min(a.right,b.right);if((ox>Math.min(a.width,b.width)/2&&dy>=0&&dy<8)||(oy>Math.min(a.height,b.height)/2&&dx>=0&&dx<8))issues.push({a:nodes[i].id,b:nodes[j].id,gap:ox>0?dy:dx});}return issues;});assert(controls.length===0,`${name} ${width} ${theme} controls ${JSON.stringify(controls)}`);checks++;
|
||||||
assert(result.buttonGaps.every(x=>x>=8),`${name} ${width} button gaps ${result.buttonGaps}`);assert(!result.overflow,`${name} ${width} overflow`);assert(result.gaps.every(x=>x>=16),`${name} ${width} gaps ${result.gaps}`);checks++;if(name==='templates'||name==='settings')await page.screenshot({path:`docs/screenshots/${name}-${width}-${theme}.png`,fullPage:true});}}
|
assert(result.buttonGaps.every(x=>x>=8),`${name} ${width} button gaps ${result.buttonGaps}`);assert(!result.overflow,`${name} ${width} overflow`);assert(result.gaps.every(x=>x>=16),`${name} ${width} gaps ${result.gaps}`);checks++;if(name==='templates'||name==='settings')await page.screenshot({path:`docs/screenshots/${name}-${width}-${theme}.png`,fullPage:true});}}
|
||||||
await page.locator('[data-page="templates"]').click();await page.locator('[data-template="daily-poetry"]').click();await page.locator('#poem-choice').waitFor();assert(await page.locator('#poem-choice option').count()===21);checks++;await page.locator('#poem-choice').selectOption('jing-ye-si');await page.locator('#render-template').click();await page.locator('#template-image').waitFor();await page.waitForFunction(()=>document.querySelector('#template-image')?.naturalWidth===1200);checks++;await page.locator('#daily-poetry-job').click();assert(!await page.locator('#poetry-job-enabled').isChecked());checks++;const gap=await page.evaluate(()=>{const a=document.querySelector('#daily-poetry-job').getBoundingClientRect(),b=document.querySelector('#template-fields').getBoundingClientRect();return b.top-a.bottom;});assert(gap>=20,'Poetry action touches fields');checks++;await page.locator('#modal-cancel').click();await page.screenshot({path:'docs/screenshots/daily-poetry-controls-mobile-dark.png',fullPage:true});for(const width of [1440,390])for(const theme of ['light','dark']){await page.setViewportSize({width,height:1000});await page.evaluate(theme=>document.documentElement.dataset.theme=theme,theme);await page.locator('[data-page="templates"]').click();for(const id of ['calendar','daily-dashboard','daily-poetry','photo-text','quote','system-alert','weather']){await page.locator(`[data-template="${id}"]`).click();await page.waitForTimeout(250);await page.locator('#download-template').waitFor();if(id==='photo-text'){const photoGap=await page.evaluate(()=>document.querySelector('#template-photo-upload').getBoundingClientRect().top-document.querySelector('#field-photo_id').getBoundingClientRect().bottom);assert(photoGap>=12,`${width} ${theme} photo controls touch`);checks++;}const gap=await page.evaluate(()=>{const a=document.querySelector('#display-template').getBoundingClientRect(),b=document.querySelector('#download-template').getBoundingClientRect();return Math.abs(a.top-b.top)<4?b.left-a.right:b.top-a.bottom;});assert(gap>=8,`${id} ${width} preview buttons touch`);checks++;}for(const [name,button] of [['automation','new-job'],['playlists','new-playlist'],['templates','daily-poetry-job']]){await page.locator(`[data-page="${name}"]`).click();if(name==='templates'){await page.locator('[data-template="daily-poetry"]').click();await page.locator('#poem-choice').waitFor();}await page.locator('#'+button).click();const gap=await page.evaluate(()=>{const [a,b]=[...document.querySelectorAll('#modal .modal-actions button')].map(x=>x.getBoundingClientRect());return Math.abs(a.top-b.top)<4?b.left-a.right:b.top-a.bottom;});assert(gap>=8,`${name} ${width} modal buttons touch`);checks++;await page.locator('#modal-cancel').click();}}assert(unintended===0,`Cancel submitted ${unintended} requests`);checks++;await browser.close();console.log(`Layout/poetry browser: ${checks} passed (32 page/profile combinations)`);})().catch(e=>{console.error(e.message);process.exit(1)});
|
await page.locator('[data-page="templates"]').click();await page.locator('[data-template="daily-poetry"]').click();await page.locator('#poem-choice').waitFor();assert(await page.locator('#poem-choice option').count()===21);checks++;await page.locator('#poem-choice').selectOption('jing-ye-si');await page.locator('#render-template').click();await page.locator('#template-image').waitFor();await page.waitForFunction(()=>document.querySelector('#template-image')?.naturalWidth===1200);checks++;await page.locator('#daily-poetry-job').click();assert(!await page.locator('#poetry-job-enabled').isChecked());checks++;const gap=await page.evaluate(()=>{const a=document.querySelector('#daily-poetry-job').getBoundingClientRect(),b=document.querySelector('#template-fields').getBoundingClientRect();return b.top-a.bottom;});assert(gap>=20,'Poetry action touches fields');checks++;await page.locator('#modal-cancel').click();await page.screenshot({path:'docs/screenshots/daily-poetry-controls-mobile-dark.png',fullPage:true});for(const width of [1440,390])for(const theme of ['light','dark']){await page.setViewportSize({width,height:1000});await page.evaluate(theme=>document.documentElement.dataset.theme=theme,theme);await page.locator('[data-page="templates"]').click();for(const id of ['calendar','daily-dashboard','daily-poetry','photo-text','quote','system-alert','weather']){await page.locator(`[data-template="${id}"]`).click();await page.waitForTimeout(250);await page.locator('#download-template').waitFor();if(id==='photo-text'){const photoGap=await page.evaluate(()=>document.querySelector('#template-photo-upload').getBoundingClientRect().top-document.querySelector('#field-photo_id').getBoundingClientRect().bottom);assert(photoGap>=12,`${width} ${theme} photo controls touch`);checks++;}const gap=await page.evaluate(()=>{const a=document.querySelector('#display-template').getBoundingClientRect(),b=document.querySelector('#download-template').getBoundingClientRect();return Math.abs(a.top-b.top)<4?b.left-a.right:b.top-a.bottom;});assert(gap>=8,`${id} ${width} preview buttons touch`);checks++;}for(const [name,button] of [['automation','new-job'],['playlists','new-playlist'],['templates','daily-poetry-job']]){await page.locator(`[data-page="${name}"]`).click();if(name==='templates'){await page.locator('[data-template="daily-poetry"]').click();await page.locator('#poem-choice').waitFor();}await page.locator('#'+button).click();const gap=await page.evaluate(()=>{const [a,b]=[...document.querySelectorAll('#modal .modal-actions button')].map(x=>x.getBoundingClientRect());return Math.abs(a.top-b.top)<4?b.left-a.right:b.top-a.bottom;});assert(gap>=8,`${name} ${width} modal buttons touch`);checks++;await page.locator('#modal-cancel').click();}}assert(unintended===0,`Cancel submitted ${unintended} requests`);checks++;await browser.close();console.log(`Layout/poetry browser: ${checks} passed (32 page/profile combinations)`);})().catch(e=>{console.error(e.message);process.exit(1)});
|
||||||
|
|||||||
Reference in New Issue
Block a user