Max recent
conversations (N)
diff --git a/tests/browser/responsiveShell.cjs b/tests/browser/responsiveShell.cjs
index 3d1d04a72..c5a05decf 100644
--- a/tests/browser/responsiveShell.cjs
+++ b/tests/browser/responsiveShell.cjs
@@ -57,6 +57,7 @@ async function noOverflow(page) {
{ name: 'desktop', width: 1440, height: 900 }
].filter(profile => !baseline || profile.name === 'desktop')) {
const page = await browser.newPage({ viewport: profile, hasTouch: !!profile.touch, isMobile: !!profile.touch });
+ page.setDefaultTimeout(20000);
await page.goto(`http://127.0.0.1:${server.address().port}`);
await noOverflow(page);
await expect(page.locator('#vonGoogleLoginButton')).toBeVisible();
@@ -81,7 +82,8 @@ async function noOverflow(page) {
const { CONVERSATION_NARROW_QUERY, loadConversationLayoutPreferences } = await import('/static/js/utils/conversationLayoutPreferences.js');
const tray = createConversationTray(workspace);
const refresh = () => {
- workspace.dataset.effectiveTabsLayout = matchMedia(CONVERSATION_NARROW_QUERY).matches ? 'horizontal' : 'vertical';
+ workspace.dataset.effectiveTabsLayout = matchMedia(CONVERSATION_NARROW_QUERY).matches
+ ? 'vertical' : loadConversationLayoutPreferences().layout;
tray.refresh(loadConversationLayoutPreferences());
};
refresh();
@@ -92,6 +94,10 @@ async function noOverflow(page) {
await preparePage();
await page.waitForTimeout(100);
await noOverflow(page);
+ if (profile.touch) {
+ await expect(page.locator('#conversationWorkspace')).toHaveAttribute('data-effective-tabs-layout', 'vertical');
+ await expect(page.locator('#conversationTrayToggle')).toHaveAttribute('aria-expanded', 'true');
+ }
const input = page.locator('#promptInput');
await input.fill('Draft survives resizing and folding.');
await page.locator('#sendButton').click({ trial: true });
@@ -143,10 +149,13 @@ async function noOverflow(page) {
const context = page.locator('#conversationTrayContext');
const selected = page.locator('#chatSessionTabs [aria-selected="true"]');
await expect(context).toHaveText('Current: Saved research conversation 11');
+ await toggle.click(); // Outside presses above dismiss the mobile panel.
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
+ await expect(page.locator('#conversationWorkspace')).toHaveAttribute('data-effective-tabs-layout', 'vertical');
+ await expect(page.locator('#conversationTrayResize')).toBeHidden();
const listBox = await page.locator('#chatSessionTabs').boundingBox();
const activeBox = await selected.boundingBox();
- assert(activeBox.x >= listBox.x - 1 && activeBox.x + activeBox.width <= listBox.x + listBox.width + 1, 'active conversation revealed without reordering');
+ assert(activeBox.y >= listBox.y - 1 && activeBox.y + activeBox.height <= listBox.y + listBox.height + 1, 'active conversation revealed without reordering');
const longTitle = 'Research conversation with a deliberately long title about observations, provenance and the next collaborative steps';
await selected.locator('.chat-session-tab-label').evaluate((el, title) => { el.textContent = title; }, longTitle);
await expect(context).toHaveText(`Current: ${longTitle}`);
@@ -172,16 +181,37 @@ async function noOverflow(page) {
await expect(input).toHaveValue('Draft survives resizing and folding.');
await noOverflow(page);
if (evidence) await page.screenshot({ path: path.join(evidence, `${profile.name}-tray-collapsed.png`) });
+ await page.evaluate(() => localStorage.setItem('von:chatSessionTabsLayout', 'horizontal'));
await page.reload();
await preparePage();
await expect(toggle).toHaveAttribute('aria-expanded', 'false');
await expect(context).toHaveText('Current: Saved research conversation 11');
await toggle.click();
await expect(selected).toBeVisible();
- await input.fill('Draft survives resizing and folding.');
+ assert.equal(await page.evaluate(() => localStorage.getItem('von:chatSessionTabsLayout')), 'horizontal');
if (evidence) await page.screenshot({ path: path.join(evidence, `${profile.name}-tray-reopened.png`) });
+ await selected.click();
+ await expect(toggle).toHaveAttribute('aria-expanded', 'false');
+ await input.fill('Draft survives resizing and folding.');
+ for (const viewport of [{ width: 915, height: 412 }, { width: profile.width, height: profile.height }]) {
+ await page.setViewportSize(viewport);
+ await expect(input).toHaveValue('Draft survives resizing and folding.');
+ await expect(selected).toHaveAttribute('aria-selected', 'true');
+ await expect(page.locator('#conversationWorkspace')).toHaveAttribute('data-effective-tabs-layout', 'vertical');
+ await toggle.click();
+ await expect(selected).toBeVisible();
+ await toggle.click();
+ await expect(page.locator('#chatSessionTabs')).toBeHidden();
+ await noOverflow(page);
+ }
}
if (trayOnly) {
+ await page.goto(`http://127.0.0.1:${server.address().port}/settings`);
+ for (const id of ['settingsConversationLayoutSelect', 'settingsConversationTrayHoverToggle']) {
+ if (profile.touch) await expect(page.locator(`#${id}`)).toBeHidden();
+ else await expect(page.locator(`#${id}`)).toBeVisible();
+ }
+ if (evidence) await page.screenshot({ path: path.join(evidence, `${profile.name}-settings.png`) });
console.log(JSON.stringify({ profile: profile.name, passed: true, scope: 'conversation tray', source: 'synthetic production-template fixture' }));
await page.close();
continue;
@@ -215,6 +245,10 @@ async function noOverflow(page) {
await page.goto(`http://127.0.0.1:${server.address().port}/settings`);
await noOverflow(page);
await expect(page.locator('#preferredLanguageSelect')).toBeVisible();
+ for (const id of ['settingsConversationLayoutSelect', 'settingsConversationTrayHoverToggle']) {
+ if (profile.touch) await expect(page.locator(`#${id}`)).toBeHidden();
+ else await expect(page.locator(`#${id}`)).toBeVisible();
+ }
if (evidence) await page.screenshot({ animations: 'disabled', path: path.join(evidence, `${profile.name}-settings.png`) });
console.log(JSON.stringify({ profile: profile.name, passed: true, source: 'synthetic production-template fixture' }));
await page.close();
diff --git a/tests/frontend/chatSessionAgentCreatedFilter.test.js b/tests/frontend/chatSessionAgentCreatedFilter.test.js
index 6c08c8949..641a2bd65 100644
--- a/tests/frontend/chatSessionAgentCreatedFilter.test.js
+++ b/tests/frontend/chatSessionAgentCreatedFilter.test.js
@@ -348,40 +348,24 @@ describe('chat session agent-created filtering', () => {
expect(document.activeElement).toBe(newChatButton);
});
- test('describes a left-rail preference truthfully when the narrow layout stays horizontal', async () => {
+ test('mobile uses the tray without offering a desktop layout command', async () => {
window.matchMedia = jest.fn(() => ({
matches: true,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
}));
- require(chatTabModulePath);
+ const chatTab = require(chatTabModulePath);
+ chatTab.__testOnly_loadChatSessionTabsLayoutPreference();
await window.refreshChatSessionTabsForOrgSwitch();
-
- const newChatButton = document.querySelector('#chatSessionTabs .chat-session-tab-new');
- newChatButton.dispatchEvent(new MouseEvent('contextmenu', {
- bubbles: true,
- clientX: 18,
- clientY: 18
- }));
-
- const chooseLeftButton = Array.from(document.querySelectorAll('.chat-session-menu button'))
- .find((button) => button.textContent === 'Use conversation tabs on left on wider screens');
- expect(chooseLeftButton).toBeTruthy();
- chooseLeftButton.click();
-
+ document.querySelector('#chatSessionTabs .chat-session-tab-new')
+ .dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, clientX: 18, clientY: 18 }));
+ const menu = document.querySelector('.chat-session-menu');
+ expect(menu.classList.contains('open')).toBe(true);
+ expect(menu.textContent).toContain('New conversation');
+ expect(menu.textContent).not.toMatch(/wider screens|Move conversation tabs/);
const workspace = document.getElementById('conversationWorkspace');
- expect(workspace.dataset.tabsLayout).toBe('vertical');
- expect(workspace.dataset.effectiveTabsLayout).toBe('horizontal');
- expect(document.querySelector('.toast')?.textContent)
- .toBe('Left conversation tabs saved for wider screens.');
-
- newChatButton.dispatchEvent(new MouseEvent('contextmenu', {
- bubbles: true,
- clientX: 22,
- clientY: 22
- }));
- expect(Array.from(document.querySelectorAll('.chat-session-menu button'))
- .some((button) => button.textContent === 'Use conversation tabs across top on wider screens'))
- .toBe(true);
+ expect(workspace.dataset.tabsLayout).toBe('horizontal');
+ expect(workspace.dataset.effectiveTabsLayout).toBe('vertical');
+ expect(localStorage.getItem('von:chatSessionTabsLayout')).toBe('horizontal');
});
});
diff --git a/tests/frontend/conversationTray.test.js b/tests/frontend/conversationTray.test.js
index 26093d12c..f32ca44be 100644
--- a/tests/frontend/conversationTray.test.js
+++ b/tests/frontend/conversationTray.test.js
@@ -168,7 +168,7 @@ test('Escape in the conversation does not collapse a docked tray', () => {
function useMobile() {
controller.destroy();
- workspace.dataset.effectiveTabsLayout = 'horizontal';
+ workspace.dataset.effectiveTabsLayout = 'vertical';
window.matchMedia.mockReturnValue({ matches: true, addEventListener() {}, removeEventListener() {} });
controller = createConversationTray(workspace);
controller.refresh(preferences());
@@ -181,7 +181,7 @@ test('mobile starts open independently of desktop and preserves context, order a
expect(toggle.getAttribute('aria-expanded')).toBe('true');
expect(document.getElementById('conversationTrayContext').textContent).toBe('Current: One');
tabs.firstChild.focus();
- tabs.firstChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
+ tabs.firstChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
expect(document.activeElement).toBe(tabs.lastChild);
tabs.lastChild.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(document.activeElement).toBe(toggle);
@@ -199,18 +199,18 @@ test('mobile starts open independently of desktop and preserves context, order a
const draft = document.getElementById('promptInput');
draft.focus();
draft.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
- expect(toggle.getAttribute('aria-expanded')).toBe('true');
+ expect(toggle.getAttribute('aria-expanded')).toBe('false');
});
test('mobile context follows asynchronous selection and reveals only the list', async () => {
useMobile();
- tabs.getBoundingClientRect = () => ({ left: 0, right: 300 });
- tabs.lastChild.getBoundingClientRect = () => ({ left: 400, right: 550 });
+ tabs.getBoundingClientRect = () => ({ top: 0, bottom: 300 });
+ tabs.lastChild.getBoundingClientRect = () => ({ top: 400, bottom: 550 });
tabs.firstChild.setAttribute('aria-selected', 'false');
tabs.lastChild.setAttribute('aria-selected', 'true');
await Promise.resolve();
expect(document.getElementById('conversationTrayContext').textContent).toBe('Current: Two');
- expect(tabs.scrollLeft).toBe(250);
+ expect(tabs.scrollTop).toBe(250);
toggle.click();
tabs.lastChild.textContent = 'Updated current conversation';
await Promise.resolve();
@@ -233,16 +233,38 @@ test('unavailable or invalid mobile storage defaults open and failed writes surv
saveConversationLayoutPreference(CONVERSATION_MOBILE_TRAY_COLLAPSED_KEY, false);
});
-test('switching to a remembered closed mobile list restores focus and preserves desktop choice', () => {
- useMobile();
- workspace.dataset.effectiveTabsLayout = 'vertical';
+test('switching to a remembered closed mobile tray restores focus and preserves desktop choice', () => {
+ controller.destroy();
+ const media = { matches: false, addEventListener() {}, removeEventListener() {} };
+ window.matchMedia.mockReturnValue(media);
+ controller = createConversationTray(workspace);
controller.refresh(preferences({ mobileCollapsed: true, collapsed: false }));
tabs.firstChild.focus();
- workspace.dataset.effectiveTabsLayout = 'horizontal';
+ media.matches = true;
controller.refresh(preferences({ mobileCollapsed: true, collapsed: false }));
expect(document.activeElement).toBe(toggle);
expect(toggle.getAttribute('aria-expanded')).toBe('false');
- workspace.dataset.effectiveTabsLayout = 'vertical';
+ media.matches = false;
controller.refresh(preferences({ mobileCollapsed: true, collapsed: false }));
expect(toggle.getAttribute('aria-expanded')).toBe('true');
});
+
+test('mobile ignores desktop hover, closes on selection or outside press, and retains draft', () => {
+ localStorage.setItem(CONVERSATION_TRAY_COLLAPSED_KEY, 'true');
+ useMobile();
+ toggle.click();
+ pointer(navigation, 'pointerenter');
+ jest.advanceTimersByTime(300);
+ expect(workspace.dataset.trayPeek).toBe('false');
+ expect(toggle.getAttribute('aria-expanded')).toBe('false');
+ toggle.click();
+ tabs.lastChild.click();
+ expect(toggle.getAttribute('aria-expanded')).toBe('false');
+ toggle.click();
+ const draft = document.getElementById('promptInput');
+ draft.value = 'Unsent mobile draft';
+ pointer(draft, 'pointerdown');
+ expect(toggle.getAttribute('aria-expanded')).toBe('false');
+ expect(draft.value).toBe('Unsent mobile draft');
+ expect(localStorage.getItem(CONVERSATION_TRAY_COLLAPSED_KEY)).toBe('true');
+});
diff --git a/tests/frontend/settingsConversationLayout.test.js b/tests/frontend/settingsConversationLayout.test.js
index 39be871d4..70964b2dd 100644
--- a/tests/frontend/settingsConversationLayout.test.js
+++ b/tests/frontend/settingsConversationLayout.test.js
@@ -11,7 +11,7 @@ const settingsTemplate = fs.readFileSync(path.resolve(__dirname, '../../src/fron
beforeEach(() => {
localStorage.clear();
- window.matchMedia = jest.fn(() => ({ matches: false }));
+ window.matchMedia = jest.fn(() => ({ matches: false, addEventListener() {}, removeEventListener() {} }));
document.body.innerHTML = `
`;
const template = document.createElement('template');
template.innerHTML = settingsTemplate;
@@ -43,11 +43,12 @@ test('new desktop defaults left; Settings switches immediately without remountin
});
test('narrow/coarse layout and cross-window changes keep the explicit preference truthful', () => {
- window.matchMedia = jest.fn(() => ({ matches: true }));
- expect(loadLayout()).toEqual({ preferred: 'vertical', effective: 'horizontal' });
+ window.matchMedia = jest.fn(() => ({ matches: true, addEventListener() {}, removeEventListener() {} }));
+ expect(loadLayout()).toEqual({ preferred: 'vertical', effective: 'vertical' });
localStorage.setItem(CONVERSATION_LAYOUT_KEY, 'horizontal');
window.dispatchEvent(new StorageEvent('storage', { key: CONVERSATION_LAYOUT_KEY, newValue: 'horizontal' }));
expect(document.getElementById('settingsConversationLayoutSelect').value).toBe('horizontal');
- window.matchMedia = jest.fn(() => ({ matches: false }));
+ expect(document.getElementById('conversationWorkspace').dataset.effectiveTabsLayout).toBe('vertical');
+ window.matchMedia = jest.fn(() => ({ matches: false, addEventListener() {}, removeEventListener() {} }));
expect(loadLayout()).toEqual({ preferred: 'horizontal', effective: 'horizontal' });
});