Files
stack/packages/webui/tests/w15-r1-regression-browser.test.mjs

94 lines
8.6 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { w15Fixture, ids } from './w15-fixture.mjs';
const settings = `#/projects/${ids.project}/settings`;
const thread = `#/projects/threads/${ids.thread}/sessions/${ids.session}`;
const room = `#/projects/rooms/${ids.project}`;
const remoteScenario = {
'/api/config': { send: true, projectRemotes: true, projectRemoteWrites: true },
[`/api/project-remotes/${ids.repository}`]: { project: ids.repository, remote: null, revision: 'a'.repeat(64), firstSaveAllowed: true },
};
const roomScenario = { [`/api/chat/rooms/${ids.project}/messages?limit=100`]: { rows: [], after: null, hasMore: false } };
const input = `const el=document.querySelector('#project-remote-url'); el.value='https://git.example/owner/unsaved'; el.dispatchEvent(new Event('input',{bubbles:true}));`;
const refresh = `document.querySelector('#refresh').click()`;
// Observe the actual chat render promise, including its completed read and paint.
const trackRefresh = `window.__pollRenders=0;{const original=window.MosaicChat;window.MosaicChat={...original,render:async (...args)=>{const result=await original.render(...args);window.__pollRenders++;return result}}}`;
const refreshed = `new Promise((resolve,reject)=>{const deadline=Date.now()+16000;const tick=()=>{if(window.__pollRenders)resolve(true);else if(Date.now()>deadline)reject(Error('No completed refresh'));else setTimeout(tick,25)};tick()})`;
test('refresh wait remains pending until the held project read and render finish', { timeout: 30000 }, async t => {
const { f, b, wait } = await w15Fixture(t, { scenario: remoteScenario });
await b.navigate(f.base + '/' + settings); await wait('!!document.querySelector("#project-remote-url")');
await b.evaluate(`window.__fetch=fetch;window.fetch=async (url,init)=>{const response=await __fetch(url,init);if(String(url)==='/api/chat/projects'){window.__held=true;await new Promise(resolve=>window.__release=resolve)}return response}; ${trackRefresh} ${refresh}; window.__settled=false;void (${refreshed}).then(()=>window.__settled=true);`);
await wait('!!window.__held');
await b.evaluate(`new Promise(resolve=>setTimeout(resolve,200))`);
assert.equal(await b.evaluate('window.__settled'),false);
await b.evaluate('__release()');
await wait('window.__settled');
});
test('remote draft survives a real poll and resets on project change', { timeout: 40000 }, async t => {
const { f, b, wait } = await w15Fixture(t, { scenario: remoteScenario });
await b.navigate(f.base + '/' + settings); await wait('!!document.querySelector("#project-remote-url")');
await b.evaluate(`${input} ${trackRefresh}`);
await b.evaluate(refreshed);
assert.equal(await b.evaluate(`document.querySelector('#project-remote-url')?.value`), 'https://git.example/owner/unsaved');
await b.evaluate(`location.hash='#/projects/${ids.empty}/settings'`); await wait(`document.querySelector('.project-remote')?.textContent.includes('no linked repository')`);
await b.evaluate(`location.hash=${JSON.stringify(settings)}`); await wait(`!!document.querySelector('#project-remote-url')`);
assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), '');
});
test('remote pending save and success survive shell refresh', { timeout: 30000 }, async t => {
const { f, b, wait } = await w15Fixture(t, { scenario: remoteScenario });
await b.navigate(f.base + '/' + settings); await wait('!!document.querySelector("#project-remote-url")');
await b.evaluate(`window.__fetch=fetch; window.fetch=async (url,init={})=>{if(init.method==='POST' && String(url).includes('/api/project-remotes/')) return new Promise(resolve=>window.__finish=()=>resolve(new Response(JSON.stringify({remote:'https://git.example/owner/unsaved'}),{status:200,headers:{'content-type':'application/json'}})));return __fetch(url,init)}; ${input} document.querySelector('.project-remote form').requestSubmit();`);
await wait('!!window.__finish'); await b.evaluate(`${trackRefresh} ${refresh}`); await b.evaluate(refreshed);
assert.equal(await b.evaluate(`document.querySelector('#project-remote-url')?.disabled`), true);
await b.evaluate('__finish()'); await wait(`document.querySelector('.project-remote')?.textContent.includes('Configured, not verified')`);
assert.match(await b.evaluate(`document.querySelector('.project-remote').textContent`), /https:\/\/git.example\/owner\/unsaved/);
});
test('remote unknown outcome and reconciliation survive shell refresh', { timeout: 30000 }, async t => {
const { f, b, wait } = await w15Fixture(t, { scenario: remoteScenario });
await b.navigate(f.base + '/' + settings); await wait('!!document.querySelector("#project-remote-url")');
await b.evaluate(`window.__fetch=fetch; window.fetch=async (url,init={})=>{if(init.method==='POST' && String(url).includes('/api/project-remotes/')) throw Error('lost');return __fetch(url,init)}; ${input} document.querySelector('.project-remote form').requestSubmit();`);
await wait(`document.querySelector('.project-remote')?.textContent.includes('Save outcome is unknown')`);
await b.evaluate(`${trackRefresh} ${refresh}`); await b.evaluate(refreshed);
assert.match(await b.evaluate(`document.querySelector('.project-remote').textContent`), /Save outcome is unknown/);
assert.equal(await b.evaluate(`document.querySelector('.project-remote button[type=submit]').disabled`), true);
await b.evaluate(`document.querySelector('.project-remote button[type=button]').click()`); await wait(`document.querySelector('.project-remote').textContent.includes('Review your entry')`);
assert.equal(await b.evaluate(`document.querySelector('#project-remote-url').value`), 'https://git.example/owner/unsaved');
});
test('room context replaces thread on navigation and on a cold visit', { timeout: 30000 }, async t => {
const { f, b, wait } = await w15Fixture(t, { scenario: roomScenario });
await b.navigate(f.base + '/' + thread); await wait('!!document.querySelector("#project-context .context-conversation")');
await b.evaluate(`location.hash=${JSON.stringify(room)}`); await wait('!!document.querySelector(".chat-room")');
await wait(`document.querySelector('#project-context .context-conversation')?.getAttribute('href')===${JSON.stringify(room)}`);
assert.equal(await b.evaluate(`document.querySelector('#project-context .context-conversation').getAttribute('href')`), room);
await b.navigate(f.base + '/' + room); await wait('!!document.querySelector(".chat-room")');
await wait(`document.querySelector('#project-context .context-conversation')?.getAttribute('href')===${JSON.stringify(room)}`);
assert.equal(await b.evaluate(`document.querySelector('#project-context .context-project').getAttribute('href')`), `#/projects/${ids.project}`);
});
test('drawer destination retains focus and traps Tab across a real poll', { timeout: 40000 }, async t => {
const { f, b, wait } = await w15Fixture(t); await b.viewport(390,1000);
await b.navigate(f.base + '/' + thread); await wait('!!document.querySelector("#chat-body")');
const href = await b.evaluate(`document.querySelector('#nav-open').click();window.__oldLink=document.querySelector('#sections a');__oldLink.focus();__oldLink.getAttribute('href')`);
await b.evaluate(`new Promise((resolve,reject)=>{const deadline=Date.now()+16000;const tick=()=>{if(document.querySelector('#sections a')!==window.__oldLink)resolve(true);else if(Date.now()>deadline)reject(Error('No poll'));else setTimeout(tick,25)};tick()})`);
assert.equal(await b.evaluate(`document.activeElement.getAttribute('href')`), href);
await b.key('Tab'); assert.equal(await b.evaluate(`!!document.activeElement.closest('#console-nav')`), true);
await b.key('Tab','Tab',8); assert.equal(await b.evaluate(`document.activeElement.getAttribute('href')`), href);
await b.evaluate(`document.querySelector('#nav-close').focus()`); await b.key('Tab','Tab',8);
assert.equal(await b.evaluate(`!!document.activeElement.closest('#console-nav')`), true);
await b.key('Tab'); assert.equal(await b.evaluate(`document.activeElement.id`), 'nav-close');
});
test('unsupported sidebar order is hidden while density saves on change', { timeout: 30000 }, async t => {
const { f, b, wait } = await w15Fixture(t); await b.navigate(f.base + '/#/settings/display'); await wait('!!document.querySelector("[name=density]")');
assert.equal(await b.evaluate(`document.querySelectorAll('[name=order]').length`), 0);
await b.evaluate(`document.querySelector('[name=density][value=compact]').click()`);
assert.match(await b.evaluate(`document.querySelector('[data-status=density]').textContent`), /saved/i);
assert.equal(await b.evaluate(`document.documentElement.dataset.sidebarDensity`), 'compact');
});