In organization mode #/ is the cross-organization dashboard (sample 02), #/projects lists every organization's projects with sort, filters and the shared By org / Flat grouping (sample 04), and #/inbox lists decisions across organizations with per-organization decide paths and terminal commands carrying --org and the full id (sample 05). The per-organization board moves to #/board and the organization's own project list to #/chat, which keeps the project surface. Reads go through workspace-data.js; a read an organization could not make says not read, never zero. Polling repaints in place and keeps focus, caret and scroll.
293 lines
25 KiB
JavaScript
293 lines
25 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { startServer } from '../src/serve.mjs';
|
|
import { createDevOwner } from '../src/dev-owner.mjs';
|
|
import { createOrgAdmin } from '../src/org-admin.mjs';
|
|
import { browser } from './browser.mjs';
|
|
import { close } from './fixture.mjs';
|
|
|
|
test('organization header and footer never display null board values', async t => {
|
|
const organizations = { list: async () => [{ id: 'alpha', label: 'alpha', busHealthy: true }], resolve: async () => ({ reads: {}, bus: {} }) };
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, log: () => {} });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
for (const [width, height] of [[1400, 900], [390, 844]]) {
|
|
await b.viewport(width, height);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(/Board:|No board/.test(document.querySelector('#board-url').textContent))return;await new Promise(r=>setTimeout(r,30))}throw Error('footer not loaded')})()`);
|
|
const text = await b.evaluate(`document.querySelector('header').textContent + ' ' + document.querySelector('footer').textContent`);
|
|
assert.doesNotMatch(text, /\b(?:null|undefined)\b/i);
|
|
}
|
|
});
|
|
|
|
test('an invalid organization is explained instead of appearing as an empty registry', async t => {
|
|
const organizations = { list: async () => [{ id: 'alpha', label: 'alpha', invalid: true, reason: 'organization document and registry disagree' }], resolve: async () => { throw Error('unavailable'); } };
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, log: () => {} }); t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
for (const [width, height] of [[1400, 900], [390, 844]]) {
|
|
await b.viewport(width, height);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(document.querySelector('#held [role=alert]'))return;await new Promise(r=>setTimeout(r,30))}throw Error('admission error missing')})()`);
|
|
const body = await b.evaluate('document.body.textContent');
|
|
assert.match(body, /alpha.*invalid.*document and registry disagree/is);
|
|
assert.doesNotMatch(body, /No organizations yet/);
|
|
}
|
|
});
|
|
|
|
test('fresh dashboard points to organization setup while real list errors remain errors', async t => {
|
|
for (let broken of [false, true]) {
|
|
const organizations = { list: async () => { if (broken) throw Error('registry down'); return []; }, resolve: async () => null };
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, board: 'http://127.0.0.1:9', log: () => {} });
|
|
const b = await browser({ machine: false });
|
|
try {
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(document.querySelector('#held .banner, #held a[href="#/settings/organizations"]'))return;await new Promise(r=>setTimeout(r,30))}throw Error('held roles did not settle')})()`);
|
|
const held = await b.evaluate(`({ text: document.querySelector('#held').textContent, error: !!document.querySelector('#held .banner.err, #held .banner.ref'), link: !!document.querySelector('#held a[href="#/settings/organizations"]') })`);
|
|
if (broken) {
|
|
assert.equal(held.error, true, 'a real registry error remains visible'); assert.match(held.text, /^Organization registry could not be readRead again$/, 'the fixed message, once, with no server error text');
|
|
broken = false; await b.evaluate(`document.querySelector('#held [data-retry]').click()`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(document.querySelector('#held a[href="#/settings/organizations"]'))return;await new Promise(r=>setTimeout(r,30))}throw Error('registry retry did not recover')})()`);
|
|
}
|
|
else { assert.match(held.text, /No organizations yet/); assert.equal(held.link, true); assert.equal(held.error, false); }
|
|
} finally { await b.close(); await close(server); }
|
|
}
|
|
});
|
|
|
|
test('creating another organization opens its setup, receipt, header and theme', async t => {
|
|
const makeDoc = id => ({ businessVersion: 1, id, human: 'local-dev-owner', arbiters: { delivery: 'chief', technical: 'chief' }, roles: { chief: { definition: 'chief-of-staff' } }, projects: { work: {} }, appearance: { theme: id === 'beta' ? 'cobalt' : 'harbor', mode: id === 'beta' ? 'light' : 'dark' } });
|
|
const docs = new Map([['alpha', makeDoc('alpha')]]), records = new Map();
|
|
const missing = () => { throw Object.assign(Error('not set up'), { code: 'org-unavailable' }); };
|
|
const service = {
|
|
orgPresets: async () => [{ name: 'fixture', label: 'Test model', description: 'Canned replies', available: { ok: true }, thinking: { levels: ['off'], default: 'off' } }],
|
|
templateRender: async id => makeDoc(id), validateOrg: async () => ({ ok: true, errors: [] }),
|
|
createOrg: async (_ctx, id, doc) => { docs.set(id, doc); const receipt = { op: 'org.create', org: id, notifications: 'none' }; records.set(id, receipt); return receipt; },
|
|
showOrg: async (ctx, id) => ({ id, doc: docs.get(id), revision: 1, root: `/data/${id}`, principal: ctx.principal }),
|
|
notificationShow: async (_ctx, id) => records.get(id), queueShow: missing, projectRepoShow: missing,
|
|
};
|
|
const registry = { listOrgs: async () => [...docs.keys()].map(id => ({ id, state: 'active' })) };
|
|
const organizations = {
|
|
list: async () => [...docs.keys()].map(id => ({ id, label: id, busHealthy: true })),
|
|
resolve: async id => docs.has(id) ? { busHealthy: true, send: async () => ({}), reads: { business: async () => ({ business: { id, projects: ['work'], appearance: docs.get(id).appearance } }), busStatus: async () => ({ live: true }) }, bus: {} } : null,
|
|
};
|
|
const orgAdmin = createOrgAdmin({ service, registry, context: async () => ({ configDir: '/cfg', dataRoot: '/data', instance: 'test' }) });
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, orgAdmin, send: async () => ({}), board: 'http://127.0.0.1:9', log: () => {} }); t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/settings/organizations`);
|
|
await wait('document.querySelector("#org-new-form [data-write]") && !document.querySelector("#org-new-form [data-write]").disabled');
|
|
const shown = selector => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});return !!e && getComputedStyle(e).display !== 'none' && e.getClientRects().length > 0})()`);
|
|
assert.equal(await b.evaluate('!!document.querySelector(".settings-readonly")'), false, 'org setup is not shown as read only');
|
|
assert.equal(await shown('#org-new-form [data-write]'), true, 'New organization is visible, not only clickable');
|
|
await b.evaluate(`(()=>{const e=document.querySelector('#org-new-id');e.value='beta';e.dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('#org-new-form [data-write]').click()})()`);
|
|
await wait('document.querySelector("#org-advanced-open") && !document.querySelector("#org-paths").hidden');
|
|
await b.evaluate(`document.querySelector('#org-advanced-open').click()`);
|
|
await wait('document.querySelector("#org-editor") && !document.querySelector("#org-editor").hidden');
|
|
assert.equal(await shown('#org-save'), true, 'Save organization is visible, not only clickable');
|
|
await b.evaluate(`document.querySelector('#org-save').click()`);
|
|
let switched = false;
|
|
for (let i = 0; i < 150 && !switched; i++) {
|
|
try { switched = await b.evaluate('window.MosaicOrg?.id === "beta" && document.querySelector("#org-setup") && !document.querySelector("#org-setup").hidden'); } catch { /* the organization switch reloads the page */ }
|
|
if (!switched) await new Promise(r => setTimeout(r, 30));
|
|
}
|
|
assert.ok(switched, 'new organization setup loaded after the switch');
|
|
await b.evaluate('window.MosaicAppearance.ready');
|
|
assert.match(await b.evaluate('location.hash'), /^#\/orgs\/beta\/settings\/organizations\?org=beta&step=setup/);
|
|
assert.match(await b.evaluate('document.querySelector("header").textContent'), /beta/);
|
|
assert.equal(await b.evaluate('document.documentElement.dataset.palette'), 'cobalt');
|
|
assert.match(await b.evaluate('document.querySelector("#org-receipt").textContent'), /Organization created.*beta/s);
|
|
});
|
|
|
|
test('global organization admin requests reach the real routes with an organization selected', async t => {
|
|
const organizations = { list: async () => [{ id: 'alpha', label: 'Alpha', busHealthy: true }], resolve: async () => ({ reads: {}, bus: {} }) };
|
|
const service = { validateOrg: async () => ({ ok: true, errors: [] }) };
|
|
const registry = { listOrgs: async () => [{ id: 'alpha', state: 'active' }] };
|
|
const orgAdmin = createOrgAdmin({ service, registry, context: async () => ({ configDir: '/cfg', dataRoot: '/data', instance: 'test' }) });
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, orgAdmin, send: async () => ({}), log: () => {} });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/`);
|
|
const result = await b.evaluate(`(async () => {
|
|
const list = await fetch('/api/org-admin');
|
|
const check = await fetch('/api/org-validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'alpha', doc: {} }) });
|
|
return { listStatus: list.status, list: await list.json(), checkStatus: check.status, check: await check.json() };
|
|
})()`);
|
|
assert.equal(result.listStatus, 200); assert.deepEqual(result.list.orgs, [{ id: 'alpha', state: 'active' }]);
|
|
assert.equal(result.checkStatus, 200); assert.equal(result.check.ok, false);
|
|
});
|
|
|
|
test('project creation offers repositories from the scoped business read', async t => {
|
|
const organizations = {
|
|
list: async () => [{ id: 'alpha', label: 'Alpha', busHealthy: true }],
|
|
resolve: async () => ({ busHealthy: true, send: async () => ({}), reads: { business: async () => ({ business: { id: 'alpha', projects: ['work'] } }), busStatus: async () => ({ live: true }) },
|
|
bus: { inbox: async () => [], agents: async () => [], messages: async () => ({ messages: [] }), tasks: async () => [] }, chat: { projects: async () => [], threads: async () => [] } }),
|
|
};
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, log: () => {} }); t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/chat`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(document.querySelector('#chat-repo option[value=work]'))return;await new Promise(r=>setTimeout(r,30))}throw Error('repository choice missing')})()`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#chat-repo option[value=work]').value`), 'work');
|
|
});
|
|
|
|
test('sidebar lists organizations and navigating changes the route and header', async t => {
|
|
const organizations = {
|
|
list: async () => [{ id: 'alpha', label: 'Alpha', theme: null, busHealthy: true }, { id: 'beta', label: 'Beta', theme: null, busHealthy: false }],
|
|
resolve: async id => ['alpha', 'beta'].includes(id) ? {
|
|
reads: { business: async () => ({ business: { id } }), settings: async () => ({}), busStatus: async () => { await new Promise(r => setTimeout(r, 200)); return { live: id === 'alpha', down: false }; } },
|
|
bus: { inbox: async () => [], agents: async () => [], messages: async () => ({ messages: [] }), tasks: async () => [] },
|
|
} : null,
|
|
};
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, board: 'http://127.0.0.1:9' });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
await b.viewport(390, 900);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/settings/business`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(document.querySelectorAll('[data-workspace-org]').length===2 && document.querySelector('#settings-page-title'))return;await new Promise(r=>setTimeout(r,30))}throw Error('Organizations did not appear')})()`);
|
|
assert.match(await b.evaluate(`document.querySelector('header').textContent`), /Alpha/);
|
|
assert.equal(await b.evaluate(`document.querySelector('#settings-page-title').textContent`), 'Organization');
|
|
assert.equal(await b.evaluate(`!!document.querySelector('#org-picker')`), false);
|
|
assert.deepEqual(await b.evaluate(`[...document.querySelectorAll('[data-workspace-org]')].map(o=>o.dataset.workspaceOrg)`), ['alpha', 'beta']);
|
|
await b.evaluate(`document.querySelector('[data-workspace-org=beta] a').click()`);
|
|
let switched = false;
|
|
for (let i = 0; i < 150 && !switched; i++) {
|
|
try { switched = await b.evaluate(`location.hash.startsWith('#/orgs/beta/') && document.querySelector('header')?.textContent.includes('Beta')`); } catch { /* reload replaced the page */ }
|
|
if (!switched) await new Promise(r => setTimeout(r, 30));
|
|
}
|
|
assert.equal(switched, true, 'switch completed after reload');
|
|
assert.match(await b.evaluate(`document.querySelector('body').textContent`), /Bus unavailable/);
|
|
await new Promise(r => setTimeout(r, 400));
|
|
assert.equal(await b.evaluate(`document.querySelector('#bus-down').hidden`), false, 'offline banner remains visible after the boot-status read');
|
|
assert.match(await b.evaluate(`document.querySelector('#bus-down').textContent`), /Bus unavailable/);
|
|
});
|
|
|
|
test('a chat draft is kept on Stay and discarded on an organization switch', async t => {
|
|
const project = '10000000-0000-4000-8000-000000000001', thread = '20000000-0000-4000-8000-000000000002';
|
|
const organizations = {
|
|
list: async () => [{ id: 'alpha', label: 'Alpha', busHealthy: true }, { id: 'beta', label: 'Beta', busHealthy: true }],
|
|
resolve: async id => ({
|
|
send: async () => ({}),
|
|
reads: { business: async () => ({ business: { id } }), settings: async () => ({}), busStatus: async () => ({ live: true, down: false }) },
|
|
bus: { inbox: async () => [], agents: async () => [], messages: async () => ({ messages: [] }), tasks: async () => [] },
|
|
chat: { projects: async () => [{ id: project, name: 'Project' }], threads: async ({ shared }) => shared ? [] : [{ id: thread, project, name: 'Conversation', instance: 'assistant' }], sessions: async () => [{ id: thread, name: 'default', default: true }], shares: async () => [], messages: async () => [] },
|
|
}),
|
|
};
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, send: async () => ({}), board: 'http://127.0.0.1:9', log: () => {} });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/chat/threads/${thread}`);
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
|
await wait('!!document.querySelector("#chat-body")');
|
|
assert.equal(await b.evaluate(`!!document.querySelector('#chat-session-form, #chat-share-form, #chat-clear')`), false, 'unmapped session and sharing controls are unavailable');
|
|
await b.evaluate(`(()=>{const el=document.querySelector('#chat-body');el.value='unsent alpha';el.dispatchEvent(new Event('input',{bubbles:true}))})()`);
|
|
await b.evaluate(`location.hash='#/settings/business'`);
|
|
await wait('!!document.querySelector("[data-workspace-org=beta] a")');
|
|
await b.evaluate(`document.querySelector('[data-workspace-org=beta] a').click()`);
|
|
await wait('!!document.querySelector("dialog[open] [data-stay]")');
|
|
await b.evaluate(`document.querySelector('[data-stay]').click()`);
|
|
assert.match(await b.evaluate('location.hash'), /^#\/orgs\/alpha\//);
|
|
assert.equal(await b.evaluate(`window.MosaicOrg.id`), 'alpha');
|
|
await b.evaluate(`location.hash='#/chat/threads/${thread}'`);
|
|
await wait('!!document.querySelector("#chat-body")');
|
|
assert.equal(await b.evaluate(`document.querySelector('#chat-body').value`), 'unsent alpha');
|
|
await b.evaluate(`location.hash='#/orgs/beta/settings/business'`);
|
|
await wait('!!document.querySelector("dialog[open] [data-discard]")');
|
|
await b.evaluate(`document.querySelector('[data-discard]').click()`).catch(() => {});
|
|
let switched = false;
|
|
for (let i = 0; i < 150 && !switched; i++) {
|
|
try { switched = await b.evaluate(`location.hash.startsWith('#/orgs/beta/') && document.querySelector('header')?.textContent.includes('Beta')`); } catch {}
|
|
if (!switched) await new Promise(r => setTimeout(r, 30));
|
|
}
|
|
assert.equal(switched, true);
|
|
assert.equal(await b.evaluate(`document.querySelector('#chat-body')?.value ?? ''`), '');
|
|
});
|
|
|
|
test('organization switch isolates browser board preferences', async t => {
|
|
const organizations = {
|
|
list: async () => [{ id: 'alpha', label: 'Alpha', busHealthy: true }, { id: 'beta', label: 'Beta', busHealthy: true }],
|
|
resolve: async id => ({ reads: { business: async () => ({ business: { id } }), settings: async () => ({}), busStatus: async () => ({ live: true, down: false }) }, bus: {} }),
|
|
};
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, board: 'http://127.0.0.1:9', log: () => {} });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
const base = `http://127.0.0.1:${server.address().port}/`;
|
|
await b.navigate(base + '#/orgs/alpha/');
|
|
await b.evaluate(`document.querySelector('#show-machine').click()`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#show-machine').checked`), true);
|
|
// The switch reloads the page. The navigate helper's load checks can be in flight when that
|
|
// reload commits, so set the hash in the page and poll one short check at a time.
|
|
await b.evaluate(`location.hash = '#/orgs/beta/'`);
|
|
let loaded = false;
|
|
for (let i = 0; i < 150 && !loaded; i++) {
|
|
try { loaded = await b.evaluate(`window.MosaicOrg?.id === 'beta' && !!document.querySelector('#show-machine')`); } catch {}
|
|
if (!loaded) await new Promise(r => setTimeout(r, 30));
|
|
}
|
|
assert.equal(loaded, true, 'beta did not load');
|
|
assert.equal(await b.evaluate(`document.querySelector('#show-machine').checked`), false);
|
|
});
|
|
|
|
|
|
test('organization creation draft survives Stay and is discarded on switch', async t => {
|
|
const organizations = {
|
|
list: async () => [{ id: 'alpha', label: 'Alpha', busHealthy: true }, { id: 'beta', label: 'Beta', busHealthy: true }],
|
|
resolve: async id => ({ reads: { business: async () => ({ business: { id } }), settings: async () => ({}), busStatus: async () => ({ live: true, down: false }) }, bus: {} }),
|
|
};
|
|
const service = { orgPresets: async () => [{ name: 'fixture', label: 'Test model', description: 'Canned replies', available: { ok: true }, thinking: { levels: ['off'], default: 'off' } }], templateRender: async id => ({ businessVersion: 1, id, human: 'local-dev-owner', arbiters: { delivery: 'chief', technical: 'chief' }, roles: { chief: { definition: 'chief-of-staff' } }, projects: { work: {} } }) };
|
|
const orgAdmin = createOrgAdmin({ service, registry: { listOrgs: async () => [] }, context: async () => ({ configDir: '/cfg', dataRoot: '/data', instance: 'test' }) });
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, orgAdmin, send: async () => ({}), board: 'http://127.0.0.1:9', log: () => {} });
|
|
t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
const wait = expression => b.evaluate(`(async()=>{for(let i=0;i<150;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expression)})})()`);
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/settings/business`);
|
|
await b.evaluate(`location.hash='#/orgs/alpha/settings/organizations'`);
|
|
await wait('!!document.querySelector("#org-new-id")');
|
|
await wait('!document.querySelector("#org-new-form [data-write]").disabled');
|
|
await b.evaluate(`(()=>{const el=document.querySelector('#org-new-id');el.value='draft';el.dispatchEvent(new Event('input',{bubbles:true}));document.querySelector('#org-new-form [data-write]').click()})()`);
|
|
await wait('document.querySelector("#org-paths") && !document.querySelector("#org-paths").hidden');
|
|
await b.evaluate(`document.querySelector('#org-advanced-open').click()`);
|
|
await wait('!document.querySelector("#org-editor").hidden');
|
|
assert.match(await b.evaluate('location.hash'), /^#\/orgs\/alpha\/settings\/organizations/);
|
|
await b.evaluate(`(()=>{const el=document.querySelector('[data-path="$.human"]');el.value='unsaved';el.dispatchEvent(new Event('input',{bubbles:true}))})()`);
|
|
const before = await b.evaluate(`({ owner: document.querySelector('[data-path="$.human"]').value, json: document.querySelector('#org-json-text').value, hash: location.hash })`);
|
|
await b.evaluate('history.back()');
|
|
await wait('!!document.querySelector("dialog[open] [data-stay]")');
|
|
await b.evaluate(`document.querySelector('[data-stay]').click()`);
|
|
assert.deepEqual(await b.evaluate(`({ owner: document.querySelector('[data-path="$.human"]').value, json: document.querySelector('#org-json-text').value, hash: location.hash })`), before);
|
|
await b.evaluate(`location.hash='#/orgs/beta/settings/organizations'`);
|
|
await wait('!!document.querySelector("dialog[open] [data-stay]")');
|
|
await b.evaluate(`document.querySelector('[data-stay]').click()`);
|
|
assert.equal(await b.evaluate(`document.querySelector('[data-path="$.human"]').value`), 'unsaved');
|
|
assert.match(await b.evaluate('location.hash'), /^#\/orgs\/alpha\//);
|
|
await b.evaluate(`location.hash='#/orgs/beta/settings/organizations'`);
|
|
await wait('!!document.querySelector("dialog[open] [data-discard]")');
|
|
await b.evaluate(`document.querySelector('[data-discard]').click()`).catch(() => {});
|
|
await wait('window.MosaicOrg?.id === "beta"');
|
|
});
|
|
|
|
test('a stopped organization bus has truthful chat guidance', async t => {
|
|
const organizations = {
|
|
list: async () => [{ id: 'alpha', label: 'Alpha', busHealthy: false }],
|
|
resolve: async () => ({ busHealthy: false, reads: { business: async () => ({ business: { id: 'alpha', projects: [] } }) }, bus: {}, chat: { projects: async () => [], threads: async () => [] } }),
|
|
};
|
|
const server = await startServer({ port: 0, sso: createDevOwner(), organizations, send: async () => ({}), log: () => {} }); t.after(() => close(server));
|
|
const b = await browser({ machine: false }); t.after(() => b.close());
|
|
await b.navigate(`http://127.0.0.1:${server.address().port}/#/orgs/alpha/chat`);
|
|
await b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(document.querySelector('.chat-area')?.innerText.includes('The bus is stopped'))return;await new Promise(r=>setTimeout(r,30))}throw Error('stopped-bus guidance missing')})()`);
|
|
const text = await b.evaluate(`document.querySelector('.chat-area').innerText`);
|
|
assert.match(text, /bus is stopped.*Start the bus/i);
|
|
assert.doesNotMatch(text, /This Console only reads/);
|
|
});
|
|
|
|
test('organization conversation recovers send capability and preserves its draft after an outage', async t => {
|
|
const { fixture } = await import('./fixture.mjs');
|
|
const { P, T, stub } = await import('./helpers/projects-review-fixture.mjs');
|
|
const f=await fixture(); t.after(()=>f.close());
|
|
const b=await browser({machine:false}); t.after(()=>b.close());
|
|
await b.call('Page.addScriptToEvaluateOnNewDocument',{source:stub + `\nwindow.MosaicOrg={id:'alpha',route:()=>'/projects/threads/${T}',list:Promise.resolve([]),readList:async()=>[]};window.configUp=false;{const old=fetch;window.fetch=(url,init)=>String(url)==='/api/config'?Promise.resolve(new Response(JSON.stringify({chatSend:window.configUp,orgSetup:true,devOwner:true,sso:true}),{status:200})):old(url,init)}`});
|
|
await b.navigate(f.base+'/#/projects/threads/'+T);
|
|
const wait=expr=>b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expr})return;await new Promise(r=>setTimeout(r,30))}throw Error(${JSON.stringify(expr)})})()`);
|
|
await wait(`document.querySelector('#chat-body')`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#chat-body').disabled`),true);
|
|
await b.evaluate(`const el=document.querySelector('#chat-body');el.value='draft kept';el.dispatchEvent(new Event('input',{bubbles:true}));window.configUp=true;document.querySelector('#refresh').click()`);
|
|
await wait(`!document.querySelector('#chat-body').disabled`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#chat-body').value`),'draft kept');
|
|
});
|