// 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'); });