Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/frontend/web/von_interface/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -3536,6 +3536,7 @@ button.unified-search-conversation-main {
min-height: 40px;
padding: 6px 12px;
background: #fff;
color: #0f172a;
border: 1px solid #cbd5e1;
border-radius: 10px;
cursor: pointer;
Expand Down Expand Up @@ -3606,6 +3607,9 @@ button.unified-search-conversation-main {

.global-task-main {
min-width: 0;
align-self: stretch;
display: flex;
flex-direction: column;
}

.task-board-scroll-hint {
Expand All @@ -3625,7 +3629,10 @@ button.unified-search-conversation-main {

.task-list[data-view-mode="board"] {
max-width: 100%;
max-height: clamp(420px, 68dvh, 760px);
/* Fill the space beside the inspector without sizing to every loaded card. */
flex: 1 1 0;
min-height: max(420px, 68dvh);
height: 0;
overflow: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
Expand All @@ -3647,6 +3654,8 @@ button.unified-search-conversation-main {

.task-pagination-control {
display: flex;
flex-wrap: wrap;
flex-shrink: 0;
align-items: center;
justify-content: center;
gap: 12px;
Expand Down
93 changes: 93 additions & 0 deletions tests/browser/taskBoardPagination.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/** Tier 1 rendering fixture: real task panel/CSS, synthetic read-only API pages. */
const { chromium, expect } = require('@playwright/test');
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const root = path.resolve('src/frontend/web/von_interface/static');
const output = path.resolve(process.argv[2] || '.run/task-board-pagination');
const baseline = process.argv.find(arg => arg.startsWith('--baseline='))?.slice('--baseline='.length);
const task = i => ({ task_concept_id: `#V#fixture_task_${i}`, title: `Research task ${i}`, description: 'Review the research evidence and record the outcome.', status: 'pending', priority: 'medium' });
const requests = [];
const stubs = {
'/js/apiService.js': `export const WINDOW_SESSION_HEADER='X-Von-Window-Session'; export const ensureUniqueWindowSessionId=async()=> 'fixture'; export const getUserContext=()=>({user_id:'#V#fixture',org_id:'#V#fixture_org'}); export const getJson=async url=>(await fetch(url)).json(); export const postJson=()=>{throw Error('Unexpected write')}; export const patchJson=postJson; export const deleteJson=postJson;`,
'/js/tabNavigation.js': 'export const activateTab=()=>{};',
'/js/utils/toast.js': 'export const showToast=()=>{};',
'/js/markdownUtils.js': 'export const renderMarkdownViaServer=async()=>"";',
};
const server = http.createServer((req,res) => {
const url = new URL(req.url,'http://localhost');
if(url.pathname === '/') {
res.setHeader('Content-Type','text/html');
res.end('<!doctype html><html><head><link rel="stylesheet" href="/styles.css"></head><body><main id="globalTasksTab" class="tab-content active"><div id="globalTasksContainer" class="global-tasks-content"></div></main><script type="module">import {showGlobalTasks} from "/js/components/taskPanel.js"; await showGlobalTasks();</script></body></html>');
} else if (stubs[url.pathname]) {
res.setHeader('Content-Type','text/javascript');res.end(stubs[url.pathname]);
} else if(url.pathname === '/api/tasks/') {
const offset = Number(url.searchParams.get('offset')); requests.push(Object.fromEntries(url.searchParams));
const data = {tasks: Array.from({length:50},(_,i)=>task(offset+i)),count:50,offset,has_more:offset===0};
res.setHeader('Content-Type','application/json');
setTimeout(()=>res.end(JSON.stringify(data)),offset ? 600 : 0);
} else if(url.pathname.startsWith('/api/')) {
res.setHeader('Content-Type','application/json');
res.end(JSON.stringify(url.pathname === '/api/tasks/taxonomy' ? {task_types:[],task_sources:[],defaults:{}} : {}));
} else {
const file = path.join(root,url.pathname);
if(!file.startsWith(root + '/') || !fs.existsSync(file)) {res.statusCode=404;res.end();return;}
res.setHeader('Content-Type',file.endsWith('.css')?'text/css':'text/javascript');
let source=fs.readFileSync(file,'utf8');
if(baseline && url.pathname === '/styles.css') source=require('node:child_process').execFileSync('git',['show',`${baseline}:src/frontend/web/von_interface/static/styles.css`],{encoding:'utf8'});
res.end(source);
}
});
(async()=>{
fs.mkdirSync(output,{recursive:true});
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
const browser=await chromium.launch();
const measurements=[];
try {
const page=await browser.newPage();
const errors=[];page.on('pageerror',e=>errors.push(e.message));
for(const viewport of [{width:1920,height:1080},{width:1536,height:864},{width:1280,height:900}]) {
await page.setViewportSize(viewport);
await page.goto(`http://127.0.0.1:${server.address().port}/`);
const more=page.getByRole('button',{name:'Load 50 more',exact:true});
await expect(more).toBeVisible();
await expect(page.locator('.task-inspector-card')).toBeVisible();
const measure=async state=>{
const result=await page.evaluate(()=>{
const board=document.querySelector('#globalTaskList');
const main=document.querySelector('.global-task-main').getBoundingClientRect();
const inspector=document.querySelector('#globalTaskInspector').getBoundingClientRect();
const btn=document.querySelector('[data-task-page-action]');
const style=btn && getComputedStyle(btn);
board.scrollTop=board.scrollHeight;
return {boardHeight:board.clientHeight,scrollTop:board.scrollTop,scrollHeight:board.scrollHeight,mainBottom:main.bottom,inspectorBottom:inspector.bottom,sideBySide:inspector.x>main.x+main.width-1,color:style?.color,background:style?.backgroundColor,overflow:document.documentElement.scrollWidth-innerWidth};
});
measurements.push({viewport,state,...result});
if(!baseline) {
assert(result.scrollTop>0,'loaded cards remain scrollable');
assert(result.overflow<=1,'board does not overflow the document');
if(result.sideBySide && state==='open') assert(Math.abs(result.mainBottom-result.inspectorBottom)<2,'board and pagination fill inspector height');
if(result.color) assert.notEqual(result.color,result.background,'pagination label contrast');
}
await page.locator('#globalTaskPagination').scrollIntoViewIfNeeded();
await page.screenshot({path:path.join(output,`${viewport.width}-${state}.png`)});
};
await measure('open');
await more.click();
await expect(page.getByRole('button',{name:'Loading…',exact:true})).toBeDisabled();
await expect(page.locator('#globalTaskPagination')).toHaveText('No more tasks to load.');
const ids=await page.locator('#globalTaskList .task-item[data-task-id]').evaluateAll(nodes=>nodes.map(n=>n.dataset.taskId));
assert.equal(ids.length,100);assert.equal(new Set(ids).size,100);
for(let i=0;i<100;i++) assert(ids.includes(task(i).task_concept_id));
// The current UI always selects an inspector; hide it only to exercise
// the requested closed-pane layout, without inventing a product control.
await page.locator('#globalTaskInspector').evaluate(el=>el.hidden=true);
await measure('closed-fixture');
}
assert.deepEqual(errors,[]);
assert.equal(requests.length,6);requests.forEach(r=>assert.equal(r.limit,'50'));
fs.writeFileSync(path.join(output,'receipt.json'),JSON.stringify({scope:'isolated real-module browser fixture; no live authentication or deployment',baseline,requests,measurements,passed:true},null,2));
console.log(JSON.stringify(measurements,null,2));
} finally {await browser.close();await new Promise(resolve=>server.close(resolve));}
})().catch(e=>{console.error(e);process.exitCode=1;});
3 changes: 2 additions & 1 deletion tests/frontend/taskPanelLayout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ describe('global task board layout', () => {
const mainRule = cssRule('.global-task-main');

expect(boardViewportRule).toContain('overflow: auto');
expect(boardViewportRule).toContain('max-height: clamp(');
expect(boardViewportRule).toContain('flex: 1 1 0');
expect(boardViewportRule).toContain('min-height: max(420px, 68dvh)');
expect(boardViewportRule).toContain('overscroll-behavior: contain');
expect(listViewportRule).toContain('overflow: visible');
expect(mainRule).not.toContain('overflow-x: auto');
Expand Down
Loading