fix(webui): keep model proxy on the root hash route

This commit is contained in:
jason.woltje committed 2026-11-06 17:08:15 -06:00
1 parent fcc0f01aa0
commit ad9b2dee42
7 files changed
+51 -21

No files matched your search

+2 -2
View File
@@ -992,7 +992,7 @@
const label=pages.find(([id])=>id===page)?.[1] || 'Appearance';
const isDirty=id=>(id==='routing'&&routesDraft)||(id==='limits'&&settingsInputs)||(id==='assistant'&&(Object.keys(assistantDrafts).length||Object.keys(nameDrafts).length))||(id==='agents'&&(instanceDrafts.add||instanceDrafts.change));
const dot='<span class="settings-dot"><span class="sr-only">unsaved changes</span></span>';
const links=pages.map(([id,name],i)=>`${i===0?'<div class="settings-group">Your browser</div>':i===1?'<div class="settings-group">This instance</div>':''}<a href="${id==='model-proxy'?'/settings/model-proxy':`#/settings/${id}`}"${id===page?' aria-current="page"':''}>${name}${isDirty(id)?dot:''}</a>`).join('');
const links=pages.map(([id,name],i)=>`${i===0?'<div class="settings-group">Your browser</div>':i===1?'<div class="settings-group">This instance</div>':''}<a href="#/settings/${id}"${id===page?' aria-current="page"':''}>${name}${isDirty(id)?dot:''}</a>`).join('');
return `${head('Settings','Your browser and this instance.')}<div class="settings-layout"><div><button type="button" id="settings-disclosure" aria-expanded="false" aria-controls="settings-nav"><small>Section</small><strong>${label}${isDirty(page)?dot:''}</strong><span aria-hidden="true">⌄</span></button><nav id="settings-nav" aria-label="Settings sections">${links}</nav></div><div class="settings-content settings-${page}${page!=='appearance'&&!canSend&&!isTester?' readonly':''}"><h2 id="settings-page-title" tabindex="-1">${label}</h2><p class="settings-helper">${helper}</p>${page!=='appearance'&&!canSend&&!isTester?'<p class="settings-readonly">Read only. Saving requires signed-in send mode.</p>':''}${body}</div></div>`;
}
async function settingsView(get, page='appearance') {
@@ -1006,7 +1006,7 @@
if (page==='routing') { try { [routes] = await get('routes'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; } }
if (page==='tokens' && s.launch && !s.launch.refused) takeSnapshot(s.launch);
const notify = typeof n.binding==='string' ? `<code>${txt(n.binding)}</code>` : n.refused ? `Could not be read: <code>${txt(n.refused)}</code>` : '<span class="muted">None set</span>';
const contents={limits:()=>launchForm(s.launch),routing:()=>routesSection(routes,instances),agents:()=>agentsSection(instances),assistant:()=>assistantSection(s.launch,names),tokens:()=>tokenLimits(limitRows,limitsRefused)+tokenUsage(usage),names:()=>testerNames(names,instances),about:()=>`<section class="panel"><dl class="kv"><dt>Address</dt><dd><code>${txt(s.address)||'unknown'}</code></dd><dt>Release</dt><dd>${txt(s.release)||'unknown'}</dd><dt>Business</dt><dd>${txt(s.business)||none}</dd><dt>Board</dt><dd><code>${txt(s.board)||'unknown'}</code></dd><dt>Notifier binding</dt><dd>${notify}</dd></dl></section>`, 'model-proxy':()=>'<section class="panel"><a href="/settings/model-proxy">Open model proxy settings</a></section>'};
const contents={limits:()=>launchForm(s.launch),routing:()=>routesSection(routes,instances),agents:()=>agentsSection(instances),assistant:()=>assistantSection(s.launch,names),tokens:()=>tokenLimits(limitRows,limitsRefused)+tokenUsage(usage),names:()=>testerNames(names,instances),about:()=>`<section class="panel"><dl class="kv"><dt>Address</dt><dd><code>${txt(s.address)||'unknown'}</code></dd><dt>Release</dt><dd>${txt(s.release)||'unknown'}</dd><dt>Business</dt><dd>${txt(s.business)||none}</dd><dt>Board</dt><dd><code>${txt(s.board)||'unknown'}</code></dd><dt>Notifier binding</dt><dd>${notify}</dd></dl></section>`, 'model-proxy':()=>'<section class="panel"><a href="#/settings/model-proxy">Open model proxy settings</a></section>'};
const helpers={limits:'Leave a limit blank for no limit. Zero stops new launches. Running sessions keep running when limits or models change.',routing:"Each agent’s harness, model and thinking, and the fallback a launch takes when the primary harness fails its preflight. A fallback sets all three or none. Running sessions keep their route. The next launch reads the change.",agents:'Each agent with its definition, launch membership and runs. A launch refuses until the agent has a route in Routing.',assistant:'The definition stays an assistant one. Holder and credentials stay hand edits.',tokens:'Token limits the bus checks, and token use read from session transcripts.'};
return settingsShell(page,contents[page](),`${get.oldest()?`Read ${when(get.oldest())}.`: 'Reading…'} ${helpers[page]||''}`);
}
+2 -3
View File
@@ -33,7 +33,6 @@ export function boardURL(value) {
const root = resolve(import.meta.dirname, 'public');
const files = new Map([
['/', ['index.html', 'text/html; charset=utf-8']],
['/settings/model-proxy', ['proxy.html', 'text/html; charset=utf-8']],
['/proxy.js', ['proxy.js', 'text/javascript; charset=utf-8']],
['/project-remote.js', ['project-remote.js', 'text/javascript; charset=utf-8']],
...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'project-archive.js', 'processing.js', 'agents.js', 'federation.js', 'requests.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
@@ -788,8 +787,8 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
// tester writes or static-file serving. No localhost preview grants access.
if (path === '/proxy' || path === '/settings/model-proxy' || path === '/proxy.js' || path.startsWith('/api/proxy/')) {
if (tester || granted || !named) return json(res, 403, { error: 'human-required' });
if (path === '/proxy' && (req.method === 'GET' || req.method === 'HEAD')) {
res.writeHead(303, { location: '/settings/model-proxy', 'cache-control': 'no-store' });
if ((path === '/proxy' || path === '/settings/model-proxy') && (req.method === 'GET' || req.method === 'HEAD')) {
res.writeHead(303, { location: '/#/settings/model-proxy', 'cache-control': 'no-store' });
return res.end();
}
if (path.startsWith('/api/proxy/')) return proxyHTTP(req, res, { path, call: proxyCall, named, tester, granted,
+3 -3
View File
@@ -10,7 +10,7 @@ async function setup(t, readonly = false, refusal = null) {
const state = { routes: [], loginStatus: 'wait', receivedKey: false, release: null, priority: 0 };
const server = http.createServer(async (req, res) => {
const send = value => { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)); };
if (req.url === '/api/config') return send({ send: !readonly });
if (req.url === '/api/config') return send({ send: !readonly, devOwner: true });
if (req.url.startsWith('/api/proxy/')) {
state.routes.push(req.url);
if (refusal && req.url.endsWith(refusal)) { res.statusCode = 503; return send({ error: 'not-configured' }); }
@@ -28,7 +28,7 @@ async function setup(t, readonly = false, refusal = null) {
if (req.url.endsWith('/login/cancel')) return send({ status: 'cancelled' });
if (req.url.endsWith('/priority/set')) { state.priority = args.priority; return send({ priority: args.priority }); }
}
if (req.url === '/settings/model-proxy') { res.setHeader('content-type', 'text/html'); return res.end(readFileSync(new URL('../src/public/proxy.html', import.meta.url))); }
if (req.url === '/') { res.setHeader('content-type', 'text/html'); return res.end(readFileSync(new URL('../src/public/index.html', import.meta.url))); }
if (req.url.startsWith('/api/')) return send({rows:[],unread:0,sessions:[]});
const root=resolve(import.meta.dirname,'../src/public'), file=resolve(root,'.'+req.url.split('?')[0]);
if(file.startsWith(root+'/') && existsSync(file)) { const type={'.js':'text/javascript','.css':'text/css','.svg':'image/svg+xml','.woff2':'font/woff2'}[extname(file)] || 'application/octet-stream';res.setHeader('content-type',type);return res.end(readFileSync(file)); }
@@ -38,7 +38,7 @@ async function setup(t, readonly = false, refusal = null) {
t.after(() => { state.release?.(); return new Promise(r => server.close(r)); });
const b = await browser({ machine: false }); t.after(() => b.close());
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: "window.__stubReady=true;window.openFeatures=null;window.open=(url,target,features)=>{window.openFeatures=features;return null;};" });
await b.navigate(`http://127.0.0.1:${server.address().port}/settings/model-proxy`);
await b.navigate(`http://127.0.0.1:${server.address().port}/#/settings/model-proxy`);
const wait = expression => b.evaluate(`(async()=>{for(let n=0;n<600;n++){if(${expression})return true;await new Promise(r=>setTimeout(r,25));}throw Error('proxy UI condition timed out');})()`);
await wait('window.__stubReady === true && document.readyState === "complete" && document.querySelector("#proxy-message")');
if (!refusal) await wait('document.querySelector("#proxy-message").textContent.includes("loaded")');
+23 -6
View File
@@ -32,6 +32,15 @@ async function setup(t, options = {}) {
s.headers = auth.writeHeaders(session);
return { s, calls, logs };
}
test('owner proxy entry paths redirect GET and HEAD to the root hash route', async t => {
const { s } = await setup(t);
for (const path of ['/proxy', '/settings/model-proxy']) for (const method of ['GET', 'HEAD']) {
const r = await call(s, path, method);
assert.equal(r.status, 303, `${method} ${path}`);
assert.equal(r.headers.location, '/#/settings/model-proxy', `${method} ${path}`);
assert.equal(r.headers['cache-control'], 'no-store');
}
});
test('owner proxy routes each make exactly one allowlisted call', async t => {
const { s, calls, logs } = await setup(t);
for (const [path, method, verb, args] of routes) {
@@ -70,7 +79,7 @@ test('granted user never reaches owner page or any proxy route', async t => {
t.after(() => new Promise(resolve => s.close(resolve)));
const session = await auth.signIn(s.address().port, { sub: 'alice-sub' });
s.headers = auth.writeHeaders(session);
for (const path of ['/proxy', '/settings/model-proxy', '/proxy.js']) assert.equal((await call(s, path)).status, 403, path);
for (const path of ['/proxy', '/settings/model-proxy', '/proxy.js']) for (const method of ['GET', 'HEAD']) assert.equal((await call(s, path, method)).status, 403, `${method} ${path}`);
for (const [path, method, , args] of routes)
assert.equal((await call(s, '/api/proxy/' + path, method, method === 'POST' ? args : undefined)).status, 403, path);
assert.deepEqual(calls, []);
@@ -79,8 +88,8 @@ test('native page offers write-only key input and no panel script', async t => {
const { s } = await setup(t);
const old = await call(s, '/proxy');
assert.equal(old.status, 303);
assert.equal(old.headers.location, '/settings/model-proxy');
const r = await call(s, old.headers.location);
assert.equal(old.headers.location, '/#/settings/model-proxy');
const r = await call(s, '/');
assert.equal(r.status, 200);
const script = await call(s, '/display.js');
assert.equal(script.status, 200);
@@ -100,7 +109,7 @@ test('tester socket refuses proxy page, reads and writes before any owner call',
const { mkdtempSync, rmSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const { startTesterSocket } = await import('../src/tester-socket.mjs');
const { startTesterSocket, testerSocketPath, TESTER_ORIGIN } = await import('../src/tester-socket.mjs');
const { testerRequest } = await import('../../cli/src/tester-http.mjs');
const root = mkdtempSync(join(tmpdir(), 'p4-tester-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
@@ -109,7 +118,15 @@ test('tester socket refuses proxy page, reads and writes before any owner call',
t.after(() => new Promise(r => s.close(r)));
for (const path of ['/proxy', '/settings/model-proxy', '/proxy.js']) {
const page = await testerRequest({ dataRoot: root, path });
assert.equal(page.status, 403, path);
assert.equal(page.status, 403, `GET ${path}`);
const head = await new Promise((resolve, reject) => {
const req = request({ socketPath: testerSocketPath(root), path, method: 'HEAD',
headers: { host: new URL(TESTER_ORIGIN).host, origin: TESTER_ORIGIN, 'sec-fetch-site': 'same-origin' } }, res => {
res.resume(); res.on('end', () => resolve(res.statusCode));
});
req.on('error', reject); req.end();
});
assert.equal(head, 403, `HEAD ${path}`);
}
for (const [path, method, , args] of routes) {
const r = await testerRequest({ dataRoot: root, path: '/api/proxy/' + path, method, body: method === 'POST' ? args : undefined });
@@ -140,7 +157,7 @@ test('proxy pages require sign-in and named host before redirect or content', as
assert.equal(response.status, 303, path);
assert.match(response.headers.location, /^\/auth\/login\?/);
const loopback = { ...s.headers, host: '127.0.0.1' };
assert.equal((await call(s, path, 'GET', undefined, loopback)).status, 403, path);
for (const method of ['GET', 'HEAD']) assert.equal((await call(s, path, method, undefined, loopback)).status, 403, `${method} ${path}`);
}
});
@@ -2,6 +2,24 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { w15Fixture } from './w15-fixture.mjs';
test('Settings proxy navigation keeps the root document through Settings and Dashboard', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t,{scenario:{'/api/config':{devOwner:true,send:true},'/api/proxy/accounts':{accounts:[]},'/api/proxy/keys':{keys:[]}}});
await b.navigate(f.base+'/#/settings');
await wait('!!document.querySelector("#settings-nav")');
assert.equal(await b.evaluate('location.pathname'), '/');
await b.evaluate('[...document.querySelectorAll("#settings-nav a")].find(a=>a.textContent.includes("Model proxy")).click()');
await new Promise(r=>setTimeout(r,500));
assert.equal(await b.evaluate('location.pathname'), '/');
await wait('document.querySelector("#settings-page-title")?.textContent==="Model proxy" && !!document.querySelector("#proxy-accounts")');
await b.evaluate('[...document.querySelectorAll("#settings-nav a")].find(a=>a.textContent.includes("Appearance")).click()');
await wait('document.querySelector("#settings-page-title")?.textContent==="Appearance" && !!document.querySelector(".settings-appearance")');
assert.equal(await b.evaluate('location.pathname'), '/');
await b.evaluate('document.querySelector(".wordmark").click()');
await wait('location.hash==="#/" && !document.querySelector("#board-head").hidden');
assert.equal(await b.evaluate('location.pathname'), '/');
assert.equal(await b.evaluate('document.querySelector("#board-head h1").textContent'), 'Dashboard');
});
test('Settings opens browser appearance and its section menu', {timeout:90000}, async t => {
const {f,b,wait}=await w15Fixture(t);
await b.navigate(f.base+'/#/settings');
@@ -1,9 +1,5 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import {readFile} from 'node:fs/promises';
import {resolve,extname} from 'node:path';
import {browser} from './browser.mjs';
import {w15Fixture} from './w15-fixture.mjs';
const rev='a'.repeat(64), long='a'.repeat(64);
@@ -31,9 +27,9 @@ test('F4 polling preserves Vars and fallback expansion, focus and caret',opts,as
test('F5 Enter in seat name saves only name despite settings draft',opts,async t=>{const {b,wait}=await page(t,'assistant',390);await input(b,'[data-assistant-field=definition]','changed');await input(b,'[data-assistant-field=name]','Wren');await b.key('Enter');await wait('__writes.length>0');assert.equal(await b.evaluate('__writes[0].path'),'/api/name');assert.deepEqual(await b.evaluate('JSON.parse(__writes[0].body)'),{instance:'assistant',name:'Wren'});});
test('F6 add agent labels left aligned with launch on own row',opts,async t=>{const {b}=await page(t,'agents');const geometry=await b.evaluate(`(()=>{let f=document.querySelector('#instance-add-form'),l=[...f.querySelectorAll('.filters>label')],c=l.map(e=>e.querySelector('input,select').getBoundingClientRect()),r=l.map(e=>e.getBoundingClientRect());return {left:l.slice(0,2).every(e=>getComputedStyle(e).alignItems==='flex-start'),separate:r[2].top>=Math.max(c[0].bottom,c[1].bottom),gap:f.querySelector('button[type=submit]').getBoundingClientRect().top-f.querySelector('details').getBoundingClientRect().bottom}})()`);assert.equal(geometry.left,true);assert.equal(geometry.separate,true);assert.ok(geometry.gap>=12);});
test('F7 maximum length agent values wrap at 390',opts,async t=>{const {b}=await page(t,'agents',390);assert.equal(await b.evaluate('document.documentElement.scrollWidth<=innerWidth'),true);});
test('F8 standalone proxy document mounts its settings section',opts,async t=>{const root=resolve('packages/webui/src/public'),server=http.createServer(async(req,res)=>{if(req.method!=='GET'){res.writeHead(403);return res.end();}const path=new URL(req.url,'http://fixture').pathname;if(path.startsWith('/api/')){res.setHeader('content-type','application/json');return res.end(JSON.stringify(path==='/api/config'?{send:true,devOwner:true}:path==='/api/proxy/accounts'?{accounts:[]}:path==='/api/proxy/keys'?{keys:[]}:{rows:[],sessions:[],projects:[],unread:0}));}const file=resolve(root,'.'+(path==='/'?'/index.html':path));if(!file.startsWith(root+'/')){res.writeHead(403);return res.end();}try{res.setHeader('content-type',({'.html':'text/html','.js':'text/javascript','.css':'text/css','.svg':'image/svg+xml'})[extname(file)]||'application/octet-stream');res.end(await readFile(file));}catch{res.writeHead(404);res.end();}});await new Promise(r=>server.listen(0,'127.0.0.1',r));t.after(()=>new Promise(r=>server.close(r)));const b=await browser({machine:false});t.after(()=>b.close());await b.navigate(`http://127.0.0.1:${server.address().port}/proxy.html`);await b.evaluate('new Promise(r=>setTimeout(r,500))');assert.equal(await b.evaluate('!!document.querySelector("#proxy-accounts")'),true);});
test('F8 root proxy route mounts its settings section',opts,async t=>{const {b}=await page(t,'model-proxy');assert.equal(await b.evaluate('location.pathname'),'/');assert.equal(await b.evaluate('!!document.querySelector("#proxy-accounts")'),true);});
test('F9 add agent saving and refusal remain local and visible',opts,async t=>{const {b,wait}=await page(t,'agents');await pointer(b,'.agent-more');await pointer(b,'[data-agent-edit]');await wait('!!document.querySelector("#instance-change-form")');await input(b,'#instance-add-form [data-instance-field=instance]','new-coder');await b.evaluate(`document.querySelector('#instance-add-form select').value='coder';window.__baseFetch=fetch;window.fetch=(u,o)=>o?.method==='POST'?new Promise(resolve=>window.__answer=(status,body)=>resolve(new Response(JSON.stringify(body),{status}))):window.__baseFetch(u,o);document.querySelector('#instance-add-form').requestSubmit()`);await wait('!!window.__answer && !!document.querySelector("#instance-add-form .settings-spinner")');assert.equal(await b.evaluate('!!document.querySelector("#instance-add-form .settings-spinner")'),true);assert.equal(await b.evaluate('document.querySelector("#instance-change-form select").disabled'),false);await b.evaluate('__answer(409,{message:"Fixture conflict",sent:false})');await wait('document.querySelector("#instances-receipt")?.textContent.includes("Fixture conflict")');assert.equal(await b.evaluate('!!document.querySelector("#instance-add-form .banner.err")'),true);assert.equal(await b.evaluate('document.querySelector("#instance-add-form .banner.err").getBoundingClientRect().bottom<=document.querySelector("#instance-add-form button[type=submit]").getBoundingClientRect().top'),true);});
test('F10 routing widths, segmented focus, conditional proxy link and priority style',opts,async t=>{const {b,wait}=await page(t,'routing');assert.equal(await b.evaluate('[...document.querySelectorAll("#routes-table select")].every(e=>e.getBoundingClientRect().width>=132)'),true);await b.evaluate('location.hash="#/settings/appearance"');await wait('!!document.querySelector(".settings-segments")');await b.evaluate('document.querySelector("[name=theme]:checked").focus()');await b.key('ArrowLeft');assert.equal(await b.evaluate('getComputedStyle(document.querySelector(".settings-segments")).outlineStyle!=="none"'),true);await b.evaluate('location.hash="#/settings/model-proxy"');await wait('!!document.querySelector("#proxy-accounts form")');await b.evaluate(`window.open=()=>null;__scenario['POST /api/proxy/login/start']={status:200,body:{url:'https://fixture.invalid/authorize',state:'fixture-state'}}`);await pointer(b,'#proxy-login-start button[type=submit]');await wait('document.querySelector("#proxy-authorize")?.hidden===false');assert.equal(await b.evaluate(`document.querySelector('#proxy-authorize').getBoundingClientRect().height>=44`),true);assert.equal(await b.evaluate('getComputedStyle(document.querySelector("#proxy-accounts button")).backgroundColor===getComputedStyle(document.querySelector("#proxy-refresh")).backgroundColor'),true);});
test('F10 routing widths, segmented focus, conditional proxy link and priority style',opts,async t=>{const {b,wait}=await page(t,'routing');assert.equal(await b.evaluate('[...document.querySelectorAll("#routes-table select")].every(e=>e.getBoundingClientRect().width>=132)'),true);await b.evaluate('location.hash="#/settings/appearance"');await wait('!!document.querySelector(".settings-segments")');await b.evaluate('document.querySelector("[name=theme]:checked").focus()');await b.key('ArrowLeft');assert.equal(await b.evaluate('getComputedStyle(document.querySelector(".settings-segments")).outlineStyle!=="none"'),true);await pointer(b,'#settings-nav a[href="#/settings/model-proxy"]');await wait('!!document.querySelector("#proxy-accounts form")');await b.evaluate(`window.open=()=>null;__scenario['POST /api/proxy/login/start']={status:200,body:{url:'https://fixture.invalid/authorize',state:'fixture-state'}}`);await pointer(b,'#proxy-login-start button[type=submit]');await wait('document.querySelector("#proxy-authorize")?.hidden===false');assert.equal(await b.evaluate(`document.querySelector('#proxy-authorize').getBoundingClientRect().height>=44`),true);assert.equal(await b.evaluate('getComputedStyle(document.querySelector("#proxy-accounts button")).backgroundColor===getComputedStyle(document.querySelector("#proxy-refresh")).backgroundColor'),true);});
test('F11 Vars fields have labels and mobile agent values identify columns',opts,async t=>{const {b,wait}=await page(t,'agents',390);assert.equal(await b.evaluate('document.querySelector("#instance-add-form textarea").labels.length>0'),true);assert.match(await b.evaluate('document.querySelector("#agents-table tbody tr").innerText'),/Definition/);assert.equal(await b.evaluate('document.querySelector("#agents-table tbody td").getAttribute("headers")'), 'agent-col-definition');await b.viewport(1440,1000);await pointer(b,'.agent-more');await pointer(b,'[data-agent-edit]');await wait('!!document.querySelector("#instance-change-form")');assert.equal(await b.evaluate('document.querySelector("#instance-change-form textarea").labels.length>0'),true);});
test('F12 collapsed section announces current draft and clears after discard',opts,async t=>{const {b,wait}=await page(t,'routing',390);await b.evaluate(`(()=>{let e=[...document.querySelectorAll('[data-route]')].find(e=>JSON.parse(e.dataset.route)[1]==='thinking');e.value='xhigh';e.dispatchEvent(new Event('change',{bubbles:true}))})()`);assert.equal(await b.evaluate('!!document.querySelector("#settings-disclosure .settings-dot")'),true);assert.match(await b.evaluate('document.querySelector("#settings-disclosure").textContent'),/unsaved changes/);await pointer(b,'#routes-reload');await wait('!document.querySelector("#settings-nav .settings-dot")');assert.equal(await b.evaluate('!!document.querySelector("#settings-disclosure .settings-dot")'),false);});
test('F13 each browser save receipt expires independently',opts,async t=>{const {b}=await page(t,'appearance');await b.evaluate('document.querySelector("[name=theme][value=dim]").focus();document.querySelector("[name=theme][value=dim]").click();document.querySelector("[name=palette][value=grove]").focus();document.querySelector("[name=palette][value=grove]").click();document.querySelector("[name=density][value=compact]").focus();document.querySelector("[name=density][value=compact]").click()');await b.evaluate('new Promise(r=>setTimeout(r,4200))');assert.deepEqual(await b.evaluate('[...document.querySelectorAll(".settings-browser-status")].map(e=>e.textContent)'),['','','']);});
+1 -1
View File
@@ -186,7 +186,7 @@ test('Queue, Business and Settings in their five states, with no move button', {
// The owner's model proxy is reached from Settings, never from the Console tree.
await go('#/settings', 'Settings');
assert.equal(await count('nav.tree a[href="/proxy"]'), 0);
assert.equal(await count('#settings-nav a[href="/settings/model-proxy"]'), 1);
assert.equal(await count('#settings-nav a[href="#/settings/model-proxy"]'), 1);
// About holds the notifier and instance facts; browser appearance saves immediately.
await go('#/settings/about', 'Settings');