Finishes the U2 WIP (37b0891a3) to samples 01, 03 and 08 of the 2026-11-09 unified workspace set: - workspace.js: sidebar Organizations and Projects groups (collapsible, state remembered), the org dashboard, the Agents levels. - workspace-data.js: every read these screens make, GET no-store, redirect manual; a refused read shows unavailable for that part only. - shell.css: the html[data-organizations] .ws-* block from the sample. - bus.js: org-mode sidebar order, pinned Settings, no project tree under Projects, focus kept across repaint. - Honest empty states where the Console has no data (KPIs, metrics, kind). - Tests: unified-workspace-browser (10), picker/delete/switch browser tests follow the sidebar, browser.test script pin.
88 lines
6.5 KiB
JavaScript
88 lines
6.5 KiB
JavaScript
// Walkthrough defect 3 (Jason, 7350): once organizations exist, Quick Setup was reachable only through
|
|
// Settings > Organizations > type an ID > New organization. "New organization" is now offered in the header menu,
|
|
// in the organization picker and in the no-organization empty state. Each opens that form with the ID field focused.
|
|
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';
|
|
|
|
// An owner Console with organization setup, holding the given organizations (the org-switch-browser fixture).
|
|
async function consoleWith(t, ids) {
|
|
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: 'harbor', mode: 'dark' } });
|
|
const docs = new Map(ids.map(id => [id, makeDoc(id)]));
|
|
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: [] }),
|
|
showOrg: async (ctx, id) => ({ id, doc: docs.get(id), revision: 1, root: `/data/${id}`, principal: ctx.principal }),
|
|
notificationShow: missing, 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, label = expression) => b.evaluate(`(async()=>{for(let i=0;i<200;i++){if(${expression})return;await new Promise(r=>setTimeout(r,30))}throw Error('Timed out: '+${JSON.stringify(label)}+'\\n'+document.body.innerText.slice(0,800))})()`);
|
|
const shown = selector => b.evaluate(`(()=>{const e=document.querySelector(${JSON.stringify(selector)});return !!e && getComputedStyle(e).visibility !== 'hidden' && e.getClientRects().length > 0})()`);
|
|
// The form is open, untouched, with the ID field focused, and the ?new=1 flag is gone so the link works again.
|
|
const formFocused = async (prefix, where) => {
|
|
await wait(`document.activeElement?.id === 'org-new-id'`, `${where}: ID field focused`);
|
|
assert.equal(await b.evaluate('location.hash'), `${prefix}/settings/organizations`, `${where}: the flag is dropped`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#org-new-id').value`), '', `${where}: the form is not prefilled`);
|
|
assert.equal(await shown('#org-new-form [data-write]'), true, `${where}: New organization is shown`);
|
|
};
|
|
return { base: `http://127.0.0.1:${server.address().port}/`, b, wait, shown, formFocused };
|
|
}
|
|
|
|
for (const [width, height] of [[1400, 900], [390, 844]]) {
|
|
test(`the header menu offers New organization once organizations exist, at ${width}px`, { timeout: 60000 }, async t => {
|
|
const { base, b, wait, shown, formFocused } = await consoleWith(t, ['alpha']);
|
|
await b.viewport(width, height);
|
|
// From the dashboard, then again from the Organizations page itself.
|
|
for (const route of ['#/orgs/alpha/', '#/orgs/alpha/settings/organizations']) {
|
|
await b.navigate(`${base}?r=${encodeURIComponent(route)}${route}`);
|
|
await wait(`document.querySelector('#avatar-menu a#org-new-link')`, `${route}: menu entry`);
|
|
await b.evaluate(`document.querySelector('#avatar-button').click()`);
|
|
assert.equal(await shown('#org-new-link'), true, `${route}: the entry is visible in the open menu`);
|
|
assert.equal(await b.evaluate(`document.querySelector('#org-new-link').textContent`), 'New organization');
|
|
assert.equal(await b.evaluate(`document.querySelector('#org-new-link').getAttribute('role')`), 'menuitem');
|
|
await b.evaluate(`document.querySelector('#org-new-link').click()`);
|
|
await formFocused('#/orgs/alpha', route);
|
|
assert.equal(await b.evaluate(`document.querySelector('#avatar-menu').hidden`), true, `${route}: the menu closes`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// U2 (#1873): the sidebar's Organizations group replaced the organization picker.
|
|
test('the sidebar Organizations group offers New organization', { timeout: 60000 }, async t => {
|
|
const { base, b, wait, formFocused } = await consoleWith(t, ['alpha', 'beta']);
|
|
await b.viewport(1400, 900);
|
|
await b.navigate(`${base}#/orgs/alpha/settings/business`);
|
|
const link = `document.querySelector('#workspace-nav a.ws-add[href="#/settings/organizations?new=1"]')`;
|
|
await wait(`document.querySelectorAll('[data-workspace-org]').length === 2 && ${link}`, 'sidebar link');
|
|
assert.equal(await b.evaluate(`${link}.getAttribute('aria-label')`), 'New organization');
|
|
await b.evaluate(`${link}.click()`);
|
|
await formFocused('#/orgs/alpha', 'sidebar');
|
|
});
|
|
|
|
test('the no-organization empty state offers New organization next to its Settings link', { timeout: 60000 }, async t => {
|
|
const { base, b, wait, formFocused } = await consoleWith(t, []);
|
|
await b.viewport(1400, 900);
|
|
await b.navigate(`${base}#/`);
|
|
const button = `document.querySelector('#held a.btn[href="#/settings/organizations?new=1"]')`;
|
|
await wait(button, 'empty state button');
|
|
const held = await b.evaluate(`document.querySelector('#held').textContent`);
|
|
assert.match(held, /No organizations yet/);
|
|
assert.equal(await b.evaluate(`!!document.querySelector('#held a[href="#/settings/organizations"]')`), true, 'the Settings link stays');
|
|
assert.equal(await b.evaluate(`${button}.textContent`), 'New organization');
|
|
await b.evaluate(`${button}.click()`);
|
|
await formFocused('#', 'empty state');
|
|
});
|