fix(webui): sp6 review round, a stopped organization's pane stops reading and keeps its unsent message, an open pane picks up new replies on its own, a pasted thread id in any case shows its pane (#1873, lane sp6-threads)
A pane whose organization's bus is stopped or unavailable no longer polls or offers Refresh; it says nothing is read until the bus starts again, holds any typed draft, and restores it untouched when the bus returns. Unsent pane drafts now count for the organization-switch and Console-restart guards. The Assistant sidebar item drops the Messages unread badge. "Start thread" is the primary action. Tests cover the stopped pane's draft, a reply arriving with no refresh, and an uppercase show id.
This commit is contained in:
1 parent
5dd940165d
commit
b982404963
7 files changed
+69
-24
No files matched your search
@@ -23,6 +23,8 @@ html[data-organizations] .assist-new-fields{display:grid;grid-template-columns:r
|
||||
html[data-organizations] .assist-new-fields label{display:grid;gap:4px;font-size:13px;color:var(--muted);min-width:0}
|
||||
html[data-organizations] .assist-new-fields select,html[data-organizations] .assist-new-fields input{font:500 14px/1.4 var(--font);color:var(--text);background:var(--canvas);border:1px solid var(--line);border-radius:var(--r);padding:6px 8px;min-width:0}
|
||||
html[data-organizations] .assist-new-actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
html[data-organizations] .assist-new .ws-btn.primary{background:var(--action);color:var(--onAction);border-color:var(--action)}
|
||||
html[data-organizations] .assist-new .ws-btn.primary:hover:not(:disabled){background:color-mix(in srgb,var(--action) 88%,var(--strong))}
|
||||
html[data-organizations] .assist-new [data-assist-new-status]{margin:0;font-size:13px}
|
||||
html[data-organizations] .assist-new [data-assist-new-status]:empty{display:none}
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
const key = `${org}:${id}`;
|
||||
if (!keys.includes(key) && keys.length < MAX) keys.push(key);
|
||||
}
|
||||
const show = q.get('show');
|
||||
const want = q.get('show') || '', at = want.indexOf(':');
|
||||
const show = at > 0 ? `${want.slice(0, at)}:${want.slice(at + 1).toLowerCase()}` : null;
|
||||
return { keys, show: keys.includes(show) ? show : null, named: q.has('open') };
|
||||
}
|
||||
const split = key => { const cut = key.indexOf(':'); return { org: key.slice(0, cut), id: key.slice(cut + 1) }; };
|
||||
@@ -185,7 +186,7 @@
|
||||
function paintTop() {
|
||||
const { root, list, o, panes } = mounted;
|
||||
setHtml(root.querySelector('[data-assist-unread]'), list ? ws().unreadNote(o, [], 'assistant') : '<p class="note ws-unread-note">The organization list could not be read. Open panes only read.</p>');
|
||||
setText(root.querySelector('[data-assist-note]'), panes.length ? `${panes.length} of ${MAX} open` : '');
|
||||
setText(root.querySelector('[data-assist-note]'), panes.length ? `${panes.length} open, up to ${MAX}` : '');
|
||||
const pick = root.querySelector('[data-assist-open]');
|
||||
if (document.activeElement !== pick) { setHtml(pick, pickerHtml(list, mounted.got)); pick.value = ''; }
|
||||
const full = panes.length >= MAX;
|
||||
@@ -213,21 +214,34 @@
|
||||
if (org) p.el.setAttribute('style', ws().colours(org)); else p.el.removeAttribute('style');
|
||||
const project = thread && i.projects?.find(x => x.id === thread.project)?.name;
|
||||
setHtml(e.chips, `${ws().orgChip(o, p.org)}${ws().projChip(project || '')}`);
|
||||
let line = '', readable = true;
|
||||
let line = '', readable = true, why = 'Nothing is read for this thread.';
|
||||
if (!list) line = 'The organization list could not be read, so sending is off here until it reads again.';
|
||||
else if (!org) { line = `${p.org} is not on your organization list.`; readable = false; }
|
||||
else if (org.invalid) { line = `${name}: organization invalid. Fix its file under Settings, Organizations.`; readable = false; }
|
||||
else if (org.busHealthy === false) line = `${name}: bus is ${org.busStopped ? 'stopped' : 'unavailable'}. This pane cannot read or send until it starts again; the other panes are not affected.`;
|
||||
else if (org.busHealthy === false) {
|
||||
const how = org.busStopped ? 'stopped' : 'unavailable';
|
||||
line = `${name}: bus is ${how}. This pane cannot read or send until it starts again; the other panes are not affected.`;
|
||||
why = `Nothing is read for this thread while ${name}'s bus is ${how}.`; readable = false;
|
||||
}
|
||||
else if (i?.threads && !thread) line = `This thread is not in ${name}'s thread list.`;
|
||||
setText(e.line, line); e.line.hidden = !line;
|
||||
if (!readable) {
|
||||
// An unsent message outlives the chat view: it is held on the pane and put back when the
|
||||
// thread reads again.
|
||||
const typed = p.chat && e.body.querySelector('form textarea')?.value;
|
||||
if (typed?.trim()) p.draft = typed;
|
||||
p.chat?.dispose(); p.chat = null;
|
||||
setHtml(e.body, '<p class="ws-line">Nothing is read for this thread.</p>');
|
||||
setHtml(e.body, `<p class="ws-line">${esc(why)}${p.draft ? ' Your unsent message is kept and comes back here when it reads again.' : ''}</p>`);
|
||||
return;
|
||||
}
|
||||
if (!p.chat) { p.chat = window.MosaicChat.pane(p.org); p.first = true; }
|
||||
const first = p.first; p.first = false;
|
||||
void p.chat.render({ path: `/chat/threads/${p.id}`, container: e.body, canSend: !!list && org?.busHealthy !== false && i?.chatSend === true, stopped: org?.busHealthy === false, navigated: first });
|
||||
const first = p.first, chat = p.chat; p.first = false;
|
||||
void chat.render({ path: `/chat/threads/${p.id}`, container: e.body, canSend: !!list && i?.chatSend === true, navigated: first }).then(() => {
|
||||
const field = p.draft && p.chat === chat ? e.body.querySelector('form textarea') : null;
|
||||
if (!field) return;
|
||||
if (!field.value) { field.value = p.draft; field.dispatchEvent(new Event('input', { bubbles: true })); }
|
||||
p.draft = '';
|
||||
}, () => {});
|
||||
}
|
||||
function paintTabs() {
|
||||
const { root, panes, show } = mounted;
|
||||
@@ -259,7 +273,7 @@
|
||||
// ---------- panes ----------
|
||||
function makePane(key) {
|
||||
const { org, id } = split(key);
|
||||
const p = { key, org, id, n: ++paneSeq, el: null, chat: null, first: true, title: null };
|
||||
const p = { key, org, id, n: ++paneSeq, el: null, chat: null, first: true, title: null, draft: '' };
|
||||
const box = mounted.root.querySelector('[data-assist-panes]');
|
||||
box.insertAdjacentHTML('beforeend', paneShell(p));
|
||||
p.el = box.lastElementChild;
|
||||
@@ -418,7 +432,7 @@
|
||||
infos.clear();
|
||||
void draw({ fresh: true });
|
||||
}
|
||||
const hasDraft = () => !!mounted && mounted.panes.some(p => p.chat?.hasDraft());
|
||||
const hasDraft = () => !!mounted && mounted.panes.some(p => !!p.draft.trim() || !!p.chat?.hasDraft());
|
||||
// A write that changed an organization reads it again.
|
||||
document.addEventListener('mosaic-org-changed', e => { if (mounted && mounted.panes.some(p => p.org === e.detail?.org)) { infos.delete(e.detail.org); void draw(); } });
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
const focused = $('sections').contains(active) || foot.contains(active) ? active.getAttribute('href') : null;
|
||||
const icon = id => window.MosaicWorkspace?.icon?.(id) ?? `<svg class="i" aria-hidden="true"><use href="/icons.svg#i-${id}"/></svg>`;
|
||||
const n = (count, what) => count > 0 ? `<span class="ws-n hot" aria-label="${count} ${what}">${count}</span>` : '';
|
||||
const items = [['/dashboard', 'Dashboard', '', 'board'], ['/inbox', 'Inbox', n(inboxCount?.open, 'open for you'), 'inbox'], ['/tasks', 'Tasks', '', 'tasks'], ['/projects', 'Projects', '', 'folder'], ['/agents', 'Agents', '', 'agents'], ['/assistant', 'Assistant', n(messageCount, 'unread messages'), 'talk']];
|
||||
const items = [['/dashboard', 'Dashboard', '', 'board'], ['/inbox', 'Inbox', n(inboxCount?.open, 'open for you'), 'inbox'], ['/tasks', 'Tasks', '', 'tasks'], ['/projects', 'Projects', '', 'folder'], ['/agents', 'Agents', '', 'agents'], ['/assistant', 'Assistant', '', 'talk']];
|
||||
const link = ([h, l, extra, id]) => {
|
||||
// #1873 SP1: the Dashboard item is the system dashboard, `#/dashboard`, outside any organization.
|
||||
const cur = h === '/dashboard' ? (route === '/dashboard' || route === '/' || route === '/board') : route.startsWith(h) || (h === '/projects' && route.startsWith('/chat')) || (h === '/tasks' && route.startsWith('/trail/task')) || (h === '/inbox' && (route.startsWith('/trail/decision') || route.startsWith('/trail/message')));
|
||||
|
||||
@@ -657,7 +657,7 @@
|
||||
} else {
|
||||
html += heading('Projects') + '<p>Projects and their conversations.</p><div class="chat-projects">' + c.projects.map(p => `<section class="panel"><h2>${link('projects', p.id, p.name)}</h2>${p.repo ? `<p class="muted">Repository: ${txt(p.repo)}</p>` : ''}${threads(c.threads.filter(t => t.project === p.id && !t.shared))}</section>`).join('') + (c.projects.length ? '' : c.readFailed ? '<p>Projects unavailable.</p>' : '<p>No projects yet.</p>') + '</div><h2>Shared with me</h2>' + threads(c.threads.filter(t => t.shared)) + creation('project', 'projects');
|
||||
}
|
||||
if (!c.canSend && pane) html += c.stopped ? '<p class="muted">This organization\'s bus is stopped. Start it on Settings → Organizations to send messages here.</p>' : '<p class="muted">Sending is unavailable in this organization. This thread only reads.</p>';
|
||||
if (!c.canSend && pane) html += '<p class="muted">Sending is unavailable in this organization. This thread only reads.</p>';
|
||||
else if (!c.canSend) html += window.MosaicOrgBusStopped ? '<p class="muted">The bus is stopped. Use Start the bus on Settings → Organizations to send messages.</p>' : '<p class="muted">Creating and sending require signed-in send mode. This Console only reads.</p>';
|
||||
html += '</div>';
|
||||
if (pane) html = html.replace(/\b(id|for|aria-controls|aria-describedby|aria-labelledby)="chat-/g, `$1="${env.prefix}`);
|
||||
@@ -768,7 +768,7 @@
|
||||
err.code === 'thread-not-found' ? 'Thread not found.' :
|
||||
err.code === 'session-not-found' ? 'Session not found in this thread.' : 'Chat could not be read. Refresh to try again; your draft is kept.';
|
||||
}
|
||||
async function render({ path, container, canSend, user = false, tester = false, navigated = false, next = false, stopped = false }) {
|
||||
async function render({ path, container, canSend, user = false, tester = false, navigated = false, next = false }) {
|
||||
if (out()) return;
|
||||
const originalPath = path;
|
||||
const sectionRoute = /^\/projects\/([0-9a-f-]+)\/(settings|conversations|tasks|requirements|files)$/i.exec(path);
|
||||
@@ -789,7 +789,7 @@
|
||||
paint(); return;
|
||||
}
|
||||
const old = current;
|
||||
current = { container, kind, id, stopped, orgArchived: old?.orgArchived ?? false, loading: true, archiveUnknown: kind === 'thread' && !user && !tester, settings: !!settings, section, 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 = { container, kind, id, orgArchived: old?.orgArchived ?? false, loading: true, archiveUnknown: kind === 'thread' && !user && !tester, settings: !!settings, section, 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 (kind === 'thread' && old?.kind === 'thread' && old.id === id && !navigated && old.archiveUnknown === false && old.projects.some(p => p.id === old.thread?.project)) current.archiveUnknown = false;
|
||||
if (current.projectArchived) current.archiveUnknown = false;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.then(r => (r.type === 'opaqueredirect' ? null : r.ok ? r.json() : null));
|
||||
const ready = readConfig().then(c => { config = c; boot = typeof c?.boot === 'string' ? c.boot : null; return c; }).catch(() => null);
|
||||
|
||||
const dirty = () => !!(window.MosaicChat?.hasDraft?.() || window.MosaicOrgCreate?.hasDraft?.()
|
||||
const dirty = () => !!(window.MosaicChat?.hasDraft?.() || window.MosaicAssistant?.hasDraft?.() || window.MosaicOrgCreate?.hasDraft?.()
|
||||
|| document.querySelector('.settings-dot')
|
||||
|| [...document.querySelectorAll('#reply, #conv-reply, #doc-source, #send-body')].some(el => !!el.value?.trim()));
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ window.MosaicLocalTime = value => Number.isFinite(Date.parse(value)) ? new Date(
|
||||
return read;
|
||||
};
|
||||
let previous = location.hash, skipGuard = false;
|
||||
const dirty = () => window.MosaicChat?.hasDraft?.() || window.MosaicOrgCreate?.hasDraft?.() || [...document.querySelectorAll('#reply, #conv-reply, #doc-source')].some(el => !!el.value?.trim());
|
||||
const dirty = () => window.MosaicChat?.hasDraft?.() || window.MosaicAssistant?.hasDraft?.() || window.MosaicOrgCreate?.hasDraft?.() || [...document.querySelectorAll('#reply, #conv-reply, #doc-source')].some(el => !!el.value?.trim());
|
||||
// A switch scopes later reads to the new organization; listeners drop what they held for the old one.
|
||||
function rescope(seg) {
|
||||
const from = current;
|
||||
|
||||
@@ -52,6 +52,7 @@ function orgs(state) {
|
||||
messages: async ({ thread }) => [
|
||||
...(MESSAGES[thread] ? [{ id: thread.replace(/^2/, '3'), from_role: 'assistant', body: MESSAGES[thread], at: '2026-11-15T10:00:00.000Z' }] : []),
|
||||
...state.sent.filter(s => s.org === id && s.thread === thread).map(s => ({ id: s.id, from_role: 'human', body: s.body, at: '2026-11-15T10:01:00.000Z' })),
|
||||
...(state.replies || []).filter(r => r.org === id && r.thread === thread).map((r, n) => ({ id: `50000000-0000-4000-8000-${String(n + 1).padStart(12, '0')}`, from_role: 'assistant', body: r.body, at: '2026-11-15T10:02:00.000Z' })),
|
||||
],
|
||||
send: async ({ thread, body }) => {
|
||||
const sent = { org: id, thread, body, id: `40000000-0000-4000-8000-${String(state.sent.length + 1).padStart(12, '0')}` };
|
||||
@@ -76,7 +77,7 @@ async function open(t, { width = 1440, height = 900, hash, state = { down: new S
|
||||
await b.viewport(width, height);
|
||||
const base = `http://127.0.0.1:${server.address().port}/`;
|
||||
await b.navigate(base + hash);
|
||||
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
||||
const wait = (expression, tries = 200) => b.evaluate(`(async()=>{for(let i=0;i<${tries};i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
||||
return { b, base, wait, state };
|
||||
}
|
||||
|
||||
@@ -85,6 +86,7 @@ const panes = `[...document.querySelectorAll('[data-assist-pane]')].map(p => p.d
|
||||
const shown = `[...document.querySelectorAll('[data-assist-pane]')].filter(p => p.getClientRects().length).map(p => p.dataset.assistPane)`;
|
||||
const query = `new URLSearchParams(location.hash.split('?')[1] || '').get('open')`;
|
||||
const ready = sel => `!!${PANE(sel)}?.querySelector('.chat-log li') && !!${PANE(sel)}?.querySelector('[data-ws-orgchip]')`;
|
||||
const SENT = '/Accepted message|Message shown in this conversation/';
|
||||
const pick = (b, value) => b.evaluate(`(() => { const s = document.querySelector('[data-assist-open]'); s.value = ${JSON.stringify(value)}; if (s.value !== ${JSON.stringify(value)}) throw Error('no option ' + ${JSON.stringify(value)}); s.dispatchEvent(new Event('change', { bubbles: true })); })()`);
|
||||
|
||||
test('the sidebar Assistant opens one system page, and two threads from two organizations open side by side with their chips', async t => {
|
||||
@@ -148,14 +150,14 @@ test('a send in organization B\'s pane reaches B\'s thread while organization A
|
||||
const field = `${PANE(`beta:${T.b1}`)}.querySelector('form textarea')`;
|
||||
await wait(`!${field}.disabled`);
|
||||
await b.evaluate(`(() => { const el = ${field}; el.value = 'rotate the beta key'; el.dispatchEvent(new Event('input', { bubbles: true })); el.form.requestSubmit(); })()`);
|
||||
await wait(`/Accepted message/.test(${PANE(`beta:${T.b1}`)}.innerText)`);
|
||||
await wait(`${SENT}.test(${PANE(`beta:${T.b1}`)}.innerText)`);
|
||||
assert.deepEqual(state.sent.map(({ org, thread, body }) => ({ org, thread, body })), [{ org: 'beta', thread: T.b1, body: 'rotate the beta key' }]);
|
||||
assert.doesNotMatch(await b.evaluate(`${PANE(`alpha:${T.a1}`)}.innerText`), /Accepted message/, 'the other pane shows no receipt');
|
||||
assert.doesNotMatch(await b.evaluate(`${PANE(`alpha:${T.a1}`)}.innerText`), /Accepted message|Message shown in this conversation/, 'the other pane shows no receipt');
|
||||
assert.equal(await b.evaluate(`${PANE(`alpha:${T.a1}`)}.querySelector('form textarea').value`), '', 'the other pane keeps its own draft');
|
||||
// A send in A's pane reaches A.
|
||||
const other = `${PANE(`alpha:${T.a1}`)}.querySelector('form textarea')`;
|
||||
await b.evaluate(`(() => { const el = ${other}; el.value = 'alpha copy'; el.dispatchEvent(new Event('input', { bubbles: true })); el.form.requestSubmit(); })()`);
|
||||
await wait(`/Accepted message/.test(${PANE(`alpha:${T.a1}`)}.innerText)`);
|
||||
await wait(`${SENT}.test(${PANE(`alpha:${T.a1}`)}.innerText)`);
|
||||
assert.deepEqual(state.sent.map(({ org, thread }) => `${org}:${thread}`), [`beta:${T.b1}`, `alpha:${T.a1}`]);
|
||||
// Live updates: a refresh shows the sent message in its own pane only.
|
||||
await b.evaluate('window.MosaicAssistant.refresh()');
|
||||
@@ -185,18 +187,41 @@ test('a stopped organization is named at the top, its pane says so, and the othe
|
||||
await wait(`/bus is stopped/i.test(${PANE(`home:${T.h1}`)}?.querySelector('[data-assist-line]')?.textContent || '')`);
|
||||
assert.equal(await b.evaluate(`${PANE(`home:${T.h1}`)}.querySelector('[data-ws-orgchip]').textContent`), 'Home');
|
||||
assert.equal(await b.evaluate(`${PANE(`alpha:${T.a1}`)}.querySelector('form textarea').disabled`), false);
|
||||
// Beta's bus stops while its pane is open: that pane says so; Alpha's still sends.
|
||||
// Beta's bus stops while its pane is open, with an unsent message in it: that pane says so and
|
||||
// only so, and holds the message; Alpha's still sends.
|
||||
const betaField = `${PANE(`beta:${T.b1}`)}.querySelector('form textarea')`;
|
||||
await wait(`!!${betaField} && !${betaField}.disabled`);
|
||||
await b.evaluate(`(() => { const el = ${betaField}; el.value = 'beta draft'; el.dispatchEvent(new Event('input', { bubbles: true })); })()`);
|
||||
state.down.add('beta');
|
||||
await b.evaluate('window.MosaicAssistant.refresh()');
|
||||
await wait(`/bus is stopped/i.test(${PANE(`beta:${T.b1}`)}.querySelector('[data-assist-line]')?.textContent || '') && /Beta/.test(document.querySelector('#bus-view [data-ws-unread]')?.textContent || '')`);
|
||||
await wait(`!${PANE(`beta:${T.b1}`)}.querySelector('form textarea') || ${PANE(`beta:${T.b1}`)}.querySelector('form textarea').disabled`);
|
||||
await wait(`!${betaField}`);
|
||||
const betaBody = await b.evaluate(`${PANE(`beta:${T.b1}`)}.querySelector('.assist-body').innerText`);
|
||||
assert.match(betaBody, /Nothing is read for this thread while Beta's bus is stopped\. Your unsent message is kept/);
|
||||
assert.doesNotMatch(betaBody, /Refresh to try again|Read again|Conversation unavailable/, 'no line that says a refresh would help');
|
||||
assert.equal(await b.evaluate('window.MosaicAssistant.hasDraft()'), true, 'the held message counts as a draft');
|
||||
const field = `${PANE(`alpha:${T.a1}`)}.querySelector('form textarea')`;
|
||||
assert.equal(await b.evaluate(`${field}.disabled`), false);
|
||||
await b.evaluate(`(() => { const el = ${field}; el.value = 'still here'; el.dispatchEvent(new Event('input', { bubbles: true })); el.form.requestSubmit(); })()`);
|
||||
await wait(`/Accepted message/.test(${PANE(`alpha:${T.a1}`)}.innerText)`);
|
||||
await wait(`${SENT}.test(${PANE(`alpha:${T.a1}`)}.innerText)`);
|
||||
assert.deepEqual(state.sent.map(({ org, body }) => `${org}:${body}`), ['alpha:still here']);
|
||||
// The picker names the stopped organizations and offers none of their threads.
|
||||
assert.match(await b.evaluate(`[...document.querySelectorAll('[data-assist-open] optgroup')].map(g => g.label).join('|')`), /Home · bus stopped/);
|
||||
// Beta's bus starts again: its pane reads again and the held message is back in its composer.
|
||||
state.down.delete('beta');
|
||||
await b.evaluate('window.MosaicAssistant.refresh()');
|
||||
await wait(`${ready(`beta:${T.b1}`)} && ${betaField}?.value === 'beta draft'`);
|
||||
assert.deepEqual(state.sent.map(({ org, body }) => `${org}:${body}`), ['alpha:still here'], 'the held message is not sent by itself');
|
||||
});
|
||||
|
||||
test('an open pane shows a new reply on its own, with no refresh', async t => {
|
||||
const state = { down: new Set(['home']), sent: [], created: [], replies: [] };
|
||||
const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}`, state });
|
||||
await wait(`${ready(`alpha:${T.a1}`)} && ${ready(`beta:${T.b1}`)}`);
|
||||
state.replies.push({ org: 'beta', thread: T.b1, body: 'Rotation finished.' });
|
||||
// The page polls every 10 s; wait up to 15 s without calling refresh.
|
||||
await wait(`/Rotation finished\./.test(${PANE(`beta:${T.b1}`)}.querySelector('.chat-log').innerText)`, 500);
|
||||
assert.doesNotMatch(await b.evaluate(`${PANE(`alpha:${T.a1}`)}.querySelector('.chat-log').innerText`), /Rotation finished/);
|
||||
});
|
||||
|
||||
test('the old Side by side address lands on the Assistant page', async t => {
|
||||
@@ -211,7 +236,7 @@ test('the old Side by side address lands on the Assistant page', async t => {
|
||||
});
|
||||
|
||||
test('at 390 one pane shows at a time with a tab strip to switch', async t => {
|
||||
const { b, wait } = await open(t, { width: 390, height: 844, hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}` });
|
||||
const { b, base, wait } = await open(t, { width: 390, height: 844, hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}` });
|
||||
await wait(`${ready(`alpha:${T.a1}`)} && document.querySelectorAll('[data-assist-tabs] [data-assist-show]').length === 2`);
|
||||
assert.deepEqual(await b.evaluate(shown), [`alpha:${T.a1}`]);
|
||||
assert.deepEqual(await b.evaluate(`[...document.querySelectorAll('[data-assist-tabs] [data-assist-show]')].map(x => [x.textContent.trim(), x.getAttribute('aria-pressed')])`), [['Launch email', 'true'], ['Gateway auth', 'false']]);
|
||||
@@ -219,7 +244,11 @@ test('at 390 one pane shows at a time with a tab strip to switch', async t => {
|
||||
await wait(`${ready(`beta:${T.b1}`)}`);
|
||||
assert.deepEqual(await b.evaluate(shown), [`beta:${T.b1}`]);
|
||||
assert.equal(await b.evaluate(`new URLSearchParams(location.hash.split('?')[1]).get('show')`), `beta:${T.b1}`);
|
||||
assert.equal(await b.evaluate('document.documentElement.scrollWidth <= innerWidth'), true, 'no horizontal page scroll');
|
||||
assert.equal(await b.evaluate('document.documentElement.scrollWidth <= innerWidth'), true, 'no horizontal page scroll'); // A pasted address names the shown pane however its id is cased.
|
||||
await b.navigate('about:blank');
|
||||
await b.navigate(base + `#/assistant?open=alpha:${T.a1},beta:${T.b1}&show=beta:${T.b1.toUpperCase()}`);
|
||||
await wait(`${ready(`beta:${T.b1}`)}`);
|
||||
assert.deepEqual(await b.evaluate(shown), [`beta:${T.b1}`]);
|
||||
});
|
||||
|
||||
test('"Open thread" starts a new thread in a chosen organization through that organization\'s own route', async t => {
|
||||
|
||||
Reference in new issue
Block a user