ravefoxnastenka-sektacommits8758524
Analyzing diff...
README.md+17 −0
index c7f41cc..e768222 100644 @@ -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 @@ -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 @@ -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()}