Fix radio persistence and verify published cabinet behavior
OpenClaw Engineer committed 2026-09-09 18:46:06 +0000

Analyzing diff...

README.md+17 −0
index c7f41cc..e768222 100644
--- a/README.md
+++ b/README.md
@@ -6,3 +6,20 @@
 - Фото хранятся только локально в IndexedDB.
 - Авторизации, аналитики и серверной отправки данных нет.
 
+
+## Публикация
+
+- URL: https://pages.sekta.work/ravefox/nastenka-sekta/
+- Репозиторий: https://git.sekta.work/ravefox/nastenka-sekta
+- Ветка `main`, Pages `source=ci`, workflow `.oxygit/ci.yml`. Сборка не требуется: CI копирует статические файлы.
+- Для этого URL настроено отдельное правило CSP на Pages: `img-src` включает `blob:`, `frame-src` разрешает `https://kinescope.io`. Без него IndexedDB-фото сохраняются, но не отображаются, а iframe блокируются.
+
+## Проверка браузером
+
+Node.js с WebSocket и запущенный Chromium с CDP; тест создаёт отдельный временный browser context и удаляет его, не меняя пользовательские данные:
+
+```sh
+NODE_USE_ENV_PROXY=0 node tests/browser.mjs https://pages.sekta.work/ravefox/nastenka-sekta 18800
+```
+
+Проверяет четыре страницы на мобильной ширине, плеер Kinescope, прогресс, баннер, шкалы, смену radio-ответа, заметку, рефлексию и IndexedDB-фото после перезагрузки, удаление фото и отсутствие JS-исключений.
app.js+1 −1
index 8c65398..dbdda15 100644
--- a/app.js
+++ b/app.js
@@ -9,7 +9,7 @@ function save(id,val){localStorage.setItem(storageKey()+id,JSON.stringify(val))}
 function load(id,fallback){try{return JSON.parse(localStorage.getItem(storageKey()+id))??fallback}catch{return fallback}}
 function updateProgress(){const all=[...document.querySelectorAll('.done')],done=all.filter(x=>x.checked).length,p=all.length?Math.round(done/all.length*100):0;document.querySelectorAll('.bar span').forEach(x=>x.style.width=p+'%');document.querySelectorAll('.progress-value').forEach(x=>x.textContent=p+'%')}
 function celebrate(){const b=document.querySelector('.celebrate');b.classList.remove('show');void b.offsetWidth;b.classList.add('show')}
-function initWeek(){document.querySelectorAll('[data-save]').forEach(el=>{const id=el.dataset.save;if(el.type==='checkbox'||el.type==='radio')el.checked=load(id,false);else el.value=load(id,'');el.addEventListener('change',()=>{save(id,el.type==='checkbox'||el.type==='radio'?el.checked:el.value);flash(el.closest('label,fieldset,.checks')||el);if(el.classList.contains('done'))updateProgress();if(el.classList.contains('main-done')&&el.checked)celebrate()});if(el.tagName==='TEXTAREA')el.addEventListener('input',()=>save(id,el.value))});document.querySelectorAll('.rating').forEach(group=>{const id=group.dataset.save,value=load(id,0);paintRating(group,value);group.querySelectorAll('button').forEach(b=>b.addEventListener('click',()=>{save(id,+b.dataset.value);paintRating(group,+b.dataset.value);flash(group)}))});updateProgress()}
+function initWeek(){document.querySelectorAll('[data-save]').forEach(el=>{const id=el.dataset.save;if(el.type==='checkbox'||el.type==='radio')el.checked=load(id,false);else el.value=load(id,'');el.addEventListener('change',()=>{save(id,el.type==='checkbox'||el.type==='radio'?el.checked:el.value);if(el.type==='radio')document.querySelectorAll('input[type=radio][data-save]').forEach(other=>{if(other.name===el.name)save(other.dataset.save,other.checked)});flash(el.closest('label,fieldset,.checks')||el);if(el.classList.contains('done'))updateProgress();if(el.classList.contains('main-done')&&el.checked)celebrate()});if(el.tagName==='TEXTAREA')el.addEventListener('input',()=>save(id,el.value))});document.querySelectorAll('.rating').forEach(group=>{const id=group.dataset.save,value=load(id,0);paintRating(group,value);group.querySelectorAll('button').forEach(b=>b.addEventListener('click',()=>{save(id,+b.dataset.value);paintRating(group,+b.dataset.value);flash(group)}))});updateProgress()}
 function paintRating(g,v){g.querySelectorAll('button').forEach(b=>b.classList.toggle('on',+b.dataset.value<=v))}
 async function initPhotos(){for(const box of document.querySelectorAll('.photo')){const k=box.dataset.photo,input=box.querySelector('input'),img=box.querySelector('img'),empty=box.querySelector('.empty');const show=blob=>{if(img.src.startsWith('blob:'))URL.revokeObjectURL(img.src);if(blob){img.src=URL.createObjectURL(blob);img.hidden=false;empty.hidden=true}else{img.removeAttribute('src');img.hidden=true;empty.hidden=false}};show(await photoGet(k));box.querySelector('.choose').onclick=()=>input.click();box.querySelector('.delete').onclick=async()=>{await photoDel(k);show(null);flash(box)};input.onchange=async()=>{if(input.files[0]){await photoPut(k,input.files[0]);show(input.files[0]);flash(box)}}}}
 document.addEventListener('DOMContentLoaded',()=>{if(document.body.dataset.page==='goals')initPhotos();else initWeek()});
tests/browser.mjs+26 −0
new file mode 100644
index 0000000..8b01719
--- /dev/null
+++ b/tests/browser.mjs
@@ -0,0 +1,26 @@
+import assert from 'node:assert/strict';
+const base=process.argv[2]||'http://127.0.0.1:8765';
+const meta=await(await fetch(`http://127.0.0.1:${process.argv[3]||18800}/json/version`)).json();
+const ws=new WebSocket(meta.webSocketDebuggerUrl);await new Promise(r=>ws.onopen=r);let id=0;const pending=new Map();let errors=[];
+ws.onmessage=e=>{let m=JSON.parse(e.data);if(m.id){pending.get(m.id)(m);pending.delete(m.id)}else if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.text)};
+const send=(method,params={},sessionId)=>new Promise((ok,no)=>{let n=++id;pending.set(n,m=>m.error?no(m.error):ok(m.result));ws.send(JSON.stringify({id:n,method,params,sessionId}))});
+const {browserContextId}=await send('Target.createBrowserContext');const {targetId}=await send('Target.createTarget',{url:'about:blank',browserContextId});const {sessionId}=await send('Target.attachToTarget',{targetId,flatten:true});const cmd=(m,p)=>send(m,p,sessionId);const ev=async expression=>{const r=await cmd('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw r.exceptionDetails;return r.result.value};const pause=()=>new Promise(r=>setTimeout(r,1000));const nav=async p=>{await cmd('Page.navigate',{url:base+'/'+p});await pause()};
+await cmd('Page.enable');await cmd('Runtime.enable');await cmd('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true});let report={pages:[]};
+try{
+for(const p of ['index.html','week1.html','week2.html','week3.html']){await nav(p);report.pages.push({page:p,...await ev(`({title:document.title,overflow:document.documentElement.scrollWidth>innerWidth,iframes:document.querySelectorAll('iframe').length,days:document.querySelectorAll('.day').length})`)});}
+await nav('week1.html');report.before=await ev(`(()=>{document.querySelector('.main-done').click();document.querySelector('.rating button[data-value="7"]').click();let e=document.querySelector('.energy button[data-value="4"]');e.click();for(const s of ['.note','.reflection textarea']){let t=document.querySelector(s);t.value='Проверка сохранения';t.dispatchEvent(new Event('input',{bubbles:true}));}let r=document.querySelectorAll('input[name="d1-tech"]');r[2].click();r[0].click();return {banner:document.querySelector('.celebrate').textContent,shown:document.querySelector('.celebrate').classList.contains('show'),radio:[...r].map(x=>x.checked)}})()`);await cmd('Page.reload');await pause();report.restored=await ev(`({checked:document.querySelector('.main-done').checked,load:document.querySelectorAll('.rating:not(.energy) button.on').length,energy:document.querySelectorAll('.energy button.on').length,note:document.querySelector('.note').value,reflection:document.querySelector('.reflection textarea').value,radio:[...document.querySelectorAll('input[name="d1-tech"]')].map(x=>x.checked),progress:document.querySelector('.progress-value').textContent})`);
+await ev(`document.querySelector('details.player').open=true`);await pause();report.frames=await cmd('Page.getFrameTree');report.iframeTargets=(await send('Target.getTargets')).targetInfos.filter(t=>t.browserContextId===browserContextId&&t.type==='iframe').map(t=>({url:t.url,title:t.title}));
+await nav('index.html');report.photo=await ev(`(async()=>{let canvas=document.createElement('canvas');canvas.width=2;canvas.height=2;const blob=await new Promise(r=>canvas.toBlob(r));const f=new File([blob],'test.png',{type:'image/png'});const dt=new DataTransfer();dt.items.add(f);const i=document.querySelector('.photo input');i.files=dt.files;await i.onchange();return {visible:!document.querySelector('.photo img').hidden,stored:(await photoGet('front')).size}})()`);await cmd('Page.reload');await pause();report.photoRestored=await ev(`({visible:!document.querySelector('.photo img').hidden,loaded:document.querySelector('.photo img').naturalWidth})`);await ev(`document.querySelector('.photo .delete').click()`);await pause();report.photoDeleted=await ev(`(async()=>({empty:!(await photoGet('front')),hidden:document.querySelector('.photo img').hidden}))()`);report.errors=errors;
+console.log(JSON.stringify(report,null,2));
+assert(report.pages.every(p=>p.title&&!p.overflow));
+assert(report.pages.slice(1).every(p=>p.iframes===6&&p.days===3));
+assert(report.before.shown&&report.before.banner.includes('чудо чудесное'));
+assert.deepEqual(report.restored.radio,[true,false,false]);
+assert.equal(report.restored.load,7);assert.equal(report.restored.energy,4);
+assert(report.restored.checked);assert.equal(report.restored.progress,'17%');
+assert.equal(report.restored.note,'Проверка сохранения');assert.equal(report.restored.reflection,'Проверка сохранения');
+assert(report.photo.stored>0&&report.photoRestored.visible&&report.photoRestored.loaded>0);
+assert(report.photoDeleted.empty&&report.photoDeleted.hidden);
+assert.equal(report.errors.length,0);
+assert(report.iframeTargets.some(f=>f.url.startsWith('https://kinescope.io/embed/'))||report.frames.frameTree.childFrames.some(f=>f.frame.url.startsWith('https://kinescope.io/embed/')&&!f.frame.unreachableUrl));
+}finally{await send('Target.disposeBrowserContext',{browserContextId});ws.close()}