fix(webui): retain archive state on failed refresh
This commit is contained in:
1 parent
51892cf95a
commit
5bf07f35f0
4 files changed
+160
-5
No files matched your search
@@ -6291,3 +6291,5 @@ R5 quote fallback room (#797 D3, 2026-11-06, MVP3 lead): r4 review P2 found that
|
||||
2026-11-06 | #797 MVP3 project archive r5 — WebUI implementation | Before: r4 head 37f38d8e1 exposed unguarded request and session writes when archive lookup failed, and mapped guarded attach's project-archived refusal to 503 outcome-unknown. After: the unknown-state thread keeps reads and guarded writes available with a warning, suppresses unguarded controls and handlers including library revert, and reports guarded attach as 409/refused; live and archived recovery are tested. Node 26: final WebUI 649 pass / 0 fail / 0 cancelled / 0 skipped; exact-r4 negative 5 behavioral failures, real authenticated archived-origin attach 409. An initial diagnostic full run had 647 pass / 2 fail while source was changing (session catalog recovery and known-archived clear cancellation); both pass on exact r4 and after correction. All iterations, comparisons, inventory and commands: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r5-report.md and project-archive-r5-evidence/. Bus/harness/CLI not run for the WebUI-only change. Queue refused this linked worktree; no queue move, push, merge or deployment. Local implementation candidate for independent review.
|
||||
|
||||
2026-11-06 | #797 MVP3 project archive r6 — WebUI implementation | Before: r5 head 8c3f48113 exposed server-unguarded request, accepted-clear cancel, library Revert and project settings Save controls on a known-archived project. After: known archived state suppresses those controls and fences stale handlers while preserving readable history, settings, Unarchive and the guarded attach picker. Node 26 frozen WebUI 654 pass / 0 fail / 0 cancelled / 0 skipped; five new tests plus the changed navigation assertion fail on exact r5 production for expected behavior; real authenticated Store/Broker probes verify guarded send, resume, clear and attach refusals. The first whole-suite run was diagnostic because the final disposed-form guard was added during it; it also reported 654/0/0/0. Detailed inventory, iterations, commands and evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r6-report.md and project-archive-r6-evidence/. Bus/harness/CLI suites not run for WebUI-only change. Queue refused this linked worktree; no queue move, push, merge, deployment or server guard change. Local candidate for independent review.
|
||||
|
||||
2026-11-06 | #797 MVP3 project archive r7 — WebUI implementation | Before: r6 head 51892cf95 lost known archived state during failed refresh, reopening repository Save and room Open and exposing guarded thread controls. After: settings, thread and room derive archive state from the displayed metadata through pending and failed reads; successful live reads restore controls, and refusal clearing remains intact. Node 26 final WebUI 658 pass / 0 fail / 0 cancelled / 0 skipped; four browser regressions fail on exact r6 for expected behavior. An earlier diagnostic whole suite had one 1440px remote-fixture timeout; candidate and r6 isolated reruns each passed 4/4, and the final whole suite passed that case. Room Send remains disabled on failed membership reads as before. Full iterations and comparison: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r7-report.md and project-archive-r7-evidence/. Bus/harness/CLI not run. Queue refused this linked worktree; no push, merge, deployment or server guard change. Local implementation candidate for independent review.
|
||||
@@ -2093,3 +2093,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 MVP3 project archive r4 | Applied reviewed merge 6a655a5cb and two WebUI fixes; Node 26 WebUI 644/644, bus without socket file 1642/1642, harness 277/277, CLI 1198 pass / 1 skip. Socket timeout reproduced on 5255e4e; original failures and comparisons in /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r4-report.md. Local candidate for independent delta review; no push or deployment.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 project archive r5 WebUI fixes on 37f38d8e1 | Unknown archive state suppresses unguarded writes, guarded attach maps to 409; final Node 26 WebUI 649/0/0/0, five exact-baseline behavioral negatives, real authenticated attach refusal. Initial diagnostic regressions corrected; inventory, all evidence and limits in /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r5-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 project archive r6 WebUI fixes on 8c3f48113 | Known-archived unguarded writes suppressed in controls and handlers; final Node 26 WebUI 654/0/0/0, five new tests and changed navigation assertion fail on baseline as expected, real Store/Broker guarded-refusal probes 4/4. Full inventory, diagnostic iterations and evidence: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r6-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
|
||||
2026-11-06 | Codex implementation sub-agent | #797 project archive r7 WebUI correction on 51892cf95 | Retained archived state fences settings, thread and room writes across failed refresh; final Node 26 WebUI 658/0/0/0, four exact-baseline behavioral negatives, 1440px diagnostic timeout and candidate/base isolated reruns documented. Room Send still requires a successful membership read. Evidence and handoff: /mnt/storage/scratch/mosaic-lead/mvp3/project-archive-r7-report.md. Local candidate for independent review; queue refused linked worktree; no push, merge or deployment.
|
||||
@@ -292,8 +292,10 @@
|
||||
controller?.abort(); controller = new AbortController();
|
||||
const signal = controller.signal, g = ++generation;
|
||||
const match = /^\/chat\/rooms\/([0-9a-f-]+)$/i.exec(path), id = match?.[1];
|
||||
const old = current;
|
||||
current = { kind: 'room', id, key: 'room:' + id, path, container, canSend, user, tester,
|
||||
projects: [], threads: [], members: [], roomReady: false, channelDisabled: current?.channelDisabled ?? false, refused: false, error: '' };
|
||||
projects: old?.kind === 'room' && old.id === id ? old.projects : [], threads: [], members: [], roomReady: false, channelDisabled: old?.channelDisabled ?? false, refused: false, error: '' };
|
||||
current.projectArchived = current.projects.some(p => p.id === id && p.archived);
|
||||
if (!UUID.test(id ?? '') || !roomOwner()) {
|
||||
current.error = UUID.test(id ?? '') ? 'Rooms require the business owner.' : 'Invalid room route.';
|
||||
current.refused = true; paint(navigated); return;
|
||||
@@ -311,9 +313,10 @@
|
||||
if (resolved) projects.rows.push(resolved);
|
||||
}
|
||||
if (!projects.rows.some(p => p.id === id)) throw new ChatError('project-not-found', 404);
|
||||
current.projectArchived = projects.rows.some(p => p.id === id && p.archived === true);
|
||||
current.projects = projects.rows;
|
||||
current.projectArchived = current.projects.some(p => p.id === id && p.archived);
|
||||
if (!catalog.rows.every(m => typeof m?.instance === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,159}$/.test(m.instance) && typeof m.name === 'string')) throw new ChatError('invalid-response');
|
||||
current.projects = projects.rows; current.threads = threads.rows;
|
||||
current.threads = threads.rows;
|
||||
current.members = catalog.rows.filter(m => threads.rows.some(t => t.project === id && t.instance === m.instance && t.name === m.instance));
|
||||
const after = s.loaded && (!s.more || next) ? s.after : null;
|
||||
const page = await request(`rooms/${id}/messages?limit=100${after ? '&after=' + after : ''}`, undefined, signal);
|
||||
@@ -331,7 +334,8 @@
|
||||
current.error = roomError(err);
|
||||
current.readFailed = true;
|
||||
if (err.code === 'channel-disabled') current.channelDisabled = true;
|
||||
if (err.status === 403 || err.status === 404) { s.rows = []; current.members = []; }
|
||||
if (err.status === 403 || err.status === 404) { s.rows = []; current.members = []; current.projects = []; }
|
||||
current.projectArchived = current.projects.some(p => p.id === id && p.archived);
|
||||
}
|
||||
if (g === generation && !out()) { paint(navigated); void loadLibrary(libraryOf(current), g, signal); }
|
||||
}
|
||||
@@ -482,6 +486,8 @@
|
||||
}
|
||||
const old = current;
|
||||
current = { container, kind, id, archiveUnknown: kind === 'thread' && !user && !tester, settings: !!settings, path: originalPath, canSend, user, tester, chatGrants: [], thread: old && old.id === id ? old.thread : null, shared: old && old.id === id ? old.shared : false, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', readFailed: false, channelDisabled: old?.channelDisabled ?? false, refused: false, legacyDefault: old && old.id === id ? old.legacyDefault : false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
|
||||
current.projectArchived = current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project) && p.archived);
|
||||
if (current.projectArchived) current.archiveUnknown = false;
|
||||
if (navigated || !old) container.innerHTML = '<p class="chat-loading" role="status">Reading chat…</p>';
|
||||
else if (current.archiveUnknown) {
|
||||
// Keep the previous conversation in place while its archive lookup is pending,
|
||||
@@ -537,7 +543,7 @@
|
||||
}
|
||||
}
|
||||
else if (projectId) current.archiveUnknown = false;
|
||||
current.projectArchived = current.projects.some(p => p.id === projectId && p.archived === true);
|
||||
current.projectArchived = current.projects.some(p => p.id === projectId && p.archived);
|
||||
const catalog = kind === 'thread' && !current.shared ? await request(`threads/${id}/sessions`, undefined, signal).catch(err => {
|
||||
if (err.code === 'access-refused' && !match[3]) return null;
|
||||
throw err;
|
||||
@@ -655,6 +661,8 @@
|
||||
if (!err.sharedRead && (err.status === 403 || err.status === 404)) {
|
||||
current.projects = []; current.threads = []; current.thread = null; current.shared = false; current.refused = true;
|
||||
}
|
||||
current.projectArchived = current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project) && p.archived);
|
||||
if (current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project))) current.archiveUnknown = false;
|
||||
paint(navigated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setup, state, REQUEST, THREAD, question, wait, draft, candidate } from './helpers/w15-requests.mjs';
|
||||
|
||||
const archived = { id: THREAD, name: 'Archived', repo: 'library', archived: true };
|
||||
const live = { id: THREAD, name: 'Live', repo: 'library' };
|
||||
const active = state({ question, candidate, snapshot: 'snapshot-1', wait: wait('question', 'question', 'q-1') });
|
||||
const material = { ...draft, snapshot: { id: 'snapshot-1', revision: 2 } };
|
||||
const url = 'https://git.example/owner/draft';
|
||||
const until = (b, expression) => b.evaluate(`(async()=>{const end=Date.now()+10000;while(!(${expression})){if(Date.now()>end)throw Error('timed out: '+${JSON.stringify(expression)});await new Promise(r=>setTimeout(r,25))}return true})()`);
|
||||
const refresh = (b, path) => b.evaluate(`window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#bus-view'),canSend:true,navigated:false}).then(()=>true)`);
|
||||
|
||||
async function boot(t) {
|
||||
const x = await setup(t, { states: { [REQUEST]: active }, draft: material, shares: [{ to: 'user:reader' }] });
|
||||
await x.b.call('Page.addScriptToEvaluateOnNewDocument', { source: `(()=>{
|
||||
window.__trace=[]; window.__failProjects=false; window.__pauseProjects=false; window.__liveProjects=[${JSON.stringify(live)}];
|
||||
const original=window.fetch;
|
||||
window.fetch=async(url,init={})=>{
|
||||
const path=String(url),method=(init.method||'GET').toUpperCase();window.__trace.push({path,method});
|
||||
if(path==='/api/chat/projects' && window.__pauseProjects) await new Promise(resolve=>window.__releaseProjects=resolve);
|
||||
if(path==='/api/chat/projects' && window.__failProjects) return new Response(JSON.stringify({error:'unavailable'}),{status:window.__failProjects===true?503:window.__failProjects});
|
||||
if(path==='/api/config'){const r=await original(url,init),v=await r.json();return new Response(JSON.stringify({...v,projectRemotes:true,projectRemoteWrites:true}));}
|
||||
if(path==='/api/project-remotes/library')return new Response(JSON.stringify({project:'library',remote:null,revision:'${'a'.repeat(64)}',firstSaveAllowed:true}));
|
||||
if(path==='/api/chat/rooms/${THREAD}/messages?limit=100')return new Response(JSON.stringify({rows:[],after:null,hasMore:false}));
|
||||
if(path==='/api/chat/threads?project=${THREAD}') { const r=await original(url,init),data=await r.json();data.rows=data.rows.map(row=>({...row,name:'assistant'}));return new Response(JSON.stringify(data)); }
|
||||
if(method==='GET' && path.includes('/api/chat/threads/') && path.includes('/messages')) {
|
||||
const r=await original(url,init),data=await r.json();data.rows.push({id:'70000000-0000-4000-8000-000000000007',from_role:'assistant',at:'2026-11-05T12:00:00Z',body:JSON.stringify({notice:'library-write',op:'write',path:'notes.md',commit:'${'a'.repeat(40)}'})});return new Response(JSON.stringify(data));
|
||||
}
|
||||
if(path.startsWith('/api/library/status'))return new Response(JSON.stringify({served:'${'b'.repeat(40)}',problems:[]}));
|
||||
return original(url,init);
|
||||
};window.confirm=()=>true;
|
||||
})()` });
|
||||
await x.b.call('Page.addScriptToEvaluateOnNewDocument', { source: `sessionStorage.setItem('mosaic-request-answer:${REQUEST}',JSON.stringify({op:'answer-retained',text:'answer',question:'q-1',revision:7}));` });
|
||||
await x.navigate(1000);
|
||||
return x;
|
||||
}
|
||||
async function setArchived(b) {
|
||||
await b.evaluate(`window.__liveProjects=[];window.__archiveRows=[${JSON.stringify(archived)}];true`);
|
||||
}
|
||||
async function setLive(b) {
|
||||
await b.evaluate(`window.__failProjects=false;window.__archiveRows=[];window.__liveProjects=[${JSON.stringify(live)}];true`);
|
||||
}
|
||||
async function noWrites(b) {
|
||||
assert.deepEqual(await b.evaluate(`window.__trace.filter(x=>x.method!=='GET')`), []);
|
||||
}
|
||||
async function force(b, markup, selector, submit = false) {
|
||||
await b.evaluate(`(()=>{const root=document.querySelector('.request-panel')||document.querySelector('.chat-area');root.insertAdjacentHTML('beforeend',${JSON.stringify(markup)});const e=root.querySelector(${JSON.stringify(selector)});${submit ? "e.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}))" : 'e.click()'};return true})()`);
|
||||
}
|
||||
|
||||
test('failed settings refresh keeps archived save fenced and live recovery preserves draft', async t => {
|
||||
const { b } = await boot(t), path = `/projects/${THREAD}/settings`;
|
||||
await b.evaluate(`location.hash=${JSON.stringify('#' + path)};true`);
|
||||
await until(b, `document.querySelector('#project-remote-url')`);
|
||||
await b.evaluate(`window.__oldForm=document.querySelector('.project-remote form');const e=document.querySelector('#project-remote-url');e.value=${JSON.stringify(url)};e.dispatchEvent(new Event('input',{bubbles:true}));true`);
|
||||
await setArchived(b); await refresh(b, path);
|
||||
await until(b, `document.querySelector('#project-archive-button')?.textContent==='Unarchive'`);
|
||||
await b.evaluate(`window.__failProjects=true;window.__pauseProjects=true;window.__trace=[];window.__refresh=window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#bus-view'),canSend:true,navigated:false});true`);
|
||||
await until(b, `window.__releaseProjects`);
|
||||
await b.evaluate(`window.__oldForm.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));true`);
|
||||
await noWrites(b);
|
||||
await b.evaluate(`window.__pauseProjects=false;window.__releaseProjects();window.__refresh.then(()=>true)`);
|
||||
assert.equal(await b.evaluate(`document.querySelector('#project-archive-button')?.textContent`), 'Unarchive');
|
||||
assert.match(await b.evaluate(`document.querySelector('.project-remote').textContent`), /archived/i);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('.project-remote form')`), false, 'archived settings must not offer Save');
|
||||
await b.evaluate(`(()=>{window.__oldForm.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));document.querySelector('.project-remote').append(window.__oldForm);const e=window.__oldForm.querySelector('input');e.disabled=false;window.__oldForm.querySelector('button').disabled=false;e.focus();return true})()`);
|
||||
await b.key('Enter');
|
||||
await b.evaluate(`window.__oldForm.requestSubmit();window.__oldForm.remove();true`);
|
||||
await noWrites(b);
|
||||
await setLive(b); await refresh(b, path); await until(b, `document.querySelector('#project-remote-url')`);
|
||||
assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), url);
|
||||
assert.equal(await b.evaluate(`document.querySelector('.project-remote button[type=submit]').disabled`), false);
|
||||
await b.evaluate(`window.__failProjects=true;true`); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`document.querySelector('#project-archive-button')?.textContent`), 'Archive project');
|
||||
assert.equal(await b.evaluate(`document.querySelector('.project-remote button[type=submit]').disabled`), false);
|
||||
await b.evaluate(`window.__failProjects=403;true`); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('#project-archive-button, .project-remote form')`), false);
|
||||
});
|
||||
|
||||
test('settings write guard agrees with displayed archived metadata', async t => {
|
||||
const { b } = await boot(t), path = `/projects/${THREAD}/settings`;
|
||||
await b.evaluate(`window.__liveProjects=[${JSON.stringify({ ...archived, archived: 'yes' })}];location.hash=${JSON.stringify('#' + path)};true`);
|
||||
await until(b, `document.querySelector('#project-archive-button')?.textContent==='Unarchive' && document.querySelector('.project-remote')`);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('.project-remote form')`), false);
|
||||
await b.evaluate(`window.__failProjects=true;true`); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`document.querySelector('#project-archive-button')?.textContent`), 'Unarchive');
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('.project-remote form')`), false);
|
||||
});
|
||||
|
||||
test('failed thread refresh keeps known archived writes fenced and live recovery restores them', async t => {
|
||||
const { b } = await boot(t), path = `/projects/threads/${THREAD}`;
|
||||
await setArchived(b); await refresh(b, path);
|
||||
await until(b, `document.querySelector('.chat-area')?.textContent.includes('This project is archived') && document.querySelector('.request-panel')`);
|
||||
await b.evaluate(`window.__failProjects=true;window.__pauseProjects=true;window.__trace=[];window.__refresh=window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#bus-view'),canSend:true,navigated:false});true`);
|
||||
await until(b, `window.__releaseProjects`);
|
||||
await b.evaluate(`(()=>{const f=document.querySelector('#chat-session-form');f.querySelector('input').disabled=false;f.querySelector('input').value='new';f.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));return true})()`);
|
||||
await noWrites(b);
|
||||
await b.evaluate(`window.__pauseProjects=false;window.__releaseProjects();window.__refresh.then(()=>true)`);
|
||||
assert.match(await b.evaluate(`document.querySelector('.chat-area').textContent`), /This project is archived/);
|
||||
for (const selector of ['#chat-send-form', '#chat-resume', '#chat-share-form', '[data-unshare]', '[data-revert]', '.request-answer', '[data-request-resend]', '[data-request-mode]', '[data-request-cancel]', '[data-request-revoke]']) assert.equal(await b.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`), false, selector);
|
||||
for (const selector of ['#chat-session-form button', '#chat-clear', '#chat-cancel']) assert.equal(await b.evaluate(`document.querySelector(${JSON.stringify(selector)}).disabled`), true, selector);
|
||||
await b.evaluate(`(()=>{const e=document.querySelector('#chat-operation');e.value='clear-1';e.dispatchEvent(new Event('input',{bubbles:true}));const button=document.querySelector('#chat-cancel');button.disabled=false;button.click();const f=document.querySelector('#chat-session-form');f.querySelector('input').disabled=false;f.querySelector('input').value='new';f.querySelector('button').disabled=false;f.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));f.querySelector('input').focus();return true})()`);
|
||||
await b.key('Enter');
|
||||
await force(b, `<button data-revert="${'a'.repeat(40)}">Revert</button>`, '[data-revert]');
|
||||
await force(b, '<form id="chat-share-form"><input id="chat-share-target" value="user:reader"><button>Share</button></form>', '#chat-share-form', true);
|
||||
await force(b, '<button data-unshare="user:reader">Unshare</button>', '[data-unshare]');
|
||||
await force(b, '<form class="request-answer"><textarea>answer</textarea><button>Send</button></form>', '.request-answer', true);
|
||||
for (const [attr, value] of [['resend',''],['mode','autonomous'],['cancel',''],['cancel-confirm',''],['revoke','']]) await force(b, `<button data-request-${attr}="${value}">Go</button>`, `[data-request-${attr}]`);
|
||||
await force(b, '<form class="request-revoke-form"><textarea>reason</textarea><button>Revoke</button></form>', '.request-revoke-form', true);
|
||||
await noWrites(b);
|
||||
await setLive(b); await refresh(b, path);
|
||||
await until(b, `document.querySelector('.request-answer') && document.querySelector('[data-revert]')`);
|
||||
for (const selector of ['#chat-session-form button', '#chat-clear', '#chat-resume', '#chat-send-form button', '#chat-share-form button', '[data-unshare]', '[data-revert]', '.request-answer button', '[data-request-resend]', '[data-request-mode]', '[data-request-cancel]', '[data-request-revoke]']) assert.equal(await b.evaluate(`!document.querySelector(${JSON.stringify(selector)}).disabled`), true, selector);
|
||||
await b.evaluate(`(()=>{const e=document.querySelector('#chat-operation');e.value='clear-1';e.dispatchEvent(new Event('input',{bubbles:true}));return true})()`);
|
||||
assert.equal(await b.evaluate(`document.querySelector('#chat-cancel').disabled`), false);
|
||||
await b.evaluate(`window.__failProjects=true;true`); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`!document.querySelector('#chat-session-form button').disabled`), true);
|
||||
await b.evaluate(`window.__failProjects=404;true`); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('#chat-session-form, #chat-send-form')`), false);
|
||||
});
|
||||
|
||||
test('failed archived room refresh never offers open or send and live recovery restores room', async t => {
|
||||
const { b } = await boot(t), path = `/projects/rooms/${THREAD}`;
|
||||
await setArchived(b); await b.evaluate(`location.hash=${JSON.stringify('#' + path)};true`);
|
||||
await until(b, `document.querySelector('.chat-room')?.textContent.includes('This project is archived')`);
|
||||
await b.evaluate(`window.__failProjects=true;window.__pauseProjects=true;window.__trace=[];window.__refresh=window.MosaicChat.render({path:${JSON.stringify(path)},container:document.querySelector('#bus-view'),canSend:true,navigated:false});true`);
|
||||
await until(b, `window.__releaseProjects`);
|
||||
await force(b, '<button id="chat-room-open">Open</button>', '#chat-room-open');
|
||||
await noWrites(b);
|
||||
await b.evaluate(`window.__pauseProjects=false;window.__releaseProjects();window.__refresh.then(()=>true)`);
|
||||
assert.match(await b.evaluate(`document.querySelector('.chat-room').textContent`), /This project is archived/);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('#chat-room-open, #chat-room-send-form')`), false);
|
||||
await force(b, '<button id="chat-room-open">Open</button>', '#chat-room-open');
|
||||
await force(b, '<form id="chat-room-send-form"><textarea id="chat-room-body">hello</textarea><input value="hello"><button>Send</button></form>', '#chat-room-send-form', true);
|
||||
await b.evaluate(`document.querySelector('#chat-room-send-form input').focus();true`); await b.key('Enter');
|
||||
await noWrites(b);
|
||||
await setLive(b); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('#chat-room-open')`), true);
|
||||
assert.equal(await b.evaluate(`!document.querySelector('#chat-room-send-form button[type=submit]').disabled`), true);
|
||||
await b.evaluate(`window.__failProjects=true;true`); await refresh(b, path);
|
||||
assert.equal(await b.evaluate(`!!document.querySelector('#chat-room-open')`), true);
|
||||
await b.evaluate(`window.__failProjects=false;true`); await setArchived(b); await refresh(b, path);
|
||||
await b.evaluate(`window.__failProjects=404;true`); await refresh(b, path);
|
||||
assert.doesNotMatch(await b.evaluate(`document.querySelector('.chat-room').textContent`), /This project is archived/);
|
||||
});
|
||||
Reference in new issue
Block a user