import assert from 'node:assert/strict'; import { once } from 'node:events'; import { createServer as createHttpServer } from 'node:http'; import { realpathSync } from 'node:fs'; import { unlink, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); const uiRoot = join(repoRoot, 'ui'); process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; process.chdir(uiRoot); const requireFromUi = createRequire(join(uiRoot, 'package.json')); const { createServer: createViteServer } = await import(pathToFileURL(requireFromUi.resolve('vite')).href); const playwrightModule = await import(pathToFileURL(requireFromUi.resolve('playwright')).href); const { chromium } = playwrightModule.chromium ? playwrightModule : playwrightModule.default; const fixturePath = `/tmp/task-files-toolbar-${process.pid}.js`; await writeFile(fixturePath, ` import React from 'react'; import { createRoot } from 'react-dom/client'; import '${uiRoot}/src/index.css'; import i18n from '${uiRoot}/src/i18n/index.ts'; import { FileBrowser } from '${uiRoot}/src/components/files/FileBrowser.tsx'; const entries = [ { name: 'report.pdf', path: 'report.pdf', kind: 'file', size: 10, modifiedAt: '2026-08-01T00:00:00Z' }, { name: 'data.csv', path: 'data.csv', kind: 'file', size: 10, modifiedAt: '2026-08-01T00:00:00Z' }, ]; const management = { canManage: true, writableSection: true, selected: new Set(), toggleSelect() {}, toggleSelectAll() {}, upload() {}, remove() {}, download() {}, isUploading: false, isDeleting: false, isDownloading: false, message: null, }; const root = createRoot(document.getElementById('root')); window.__renderToolbar = (managed) => root.render(React.createElement(FileBrowser, { section: 'output', currentPath: '', entries, pathSegments: [], taskId: 42, onSectionChange(section) { window.__selectedSection = section; }, onNavigate() {}, onPreview() {}, onRefresh() {}, management: managed ? management : undefined, })); window.__setToolbarLanguage = (language) => i18n.changeLanguage(language); window.__renderToolbar(true); `); const vite = await createViteServer({ root: uiRoot, appType: 'custom', logLevel: 'error', server: { middlewareMode: true, hmr: false, fs: { allow: [repoRoot, '/tmp', realpathSync(join(uiRoot, 'node_modules'))] }, }, }); const server = createHttpServer(async (req, res) => { if (req.url === '/') { const html = await vite.transformIndexHtml('/', `
`); res.setHeader('content-type', 'text/html; charset=utf-8'); res.end(html); return; } vite.middlewares(req, res); }); server.listen(0, '127.0.0.1'); await once(server, 'listening'); const address = server.address(); if (!address || typeof address === 'string') throw new Error('Fixture server did not bind TCP'); let browser; try { browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] }); const page = await browser.newPage({ viewport: { width: 900, height: 700 } }); await page.goto(`http://127.0.0.1:${address.port}/`); await page.getByTestId('task-file-origin-toggle').waitFor(); for (const language of ['ja', 'en']) { await page.evaluate(async lang => { await window.__setToolbarLanguage(lang); }, language); for (const managed of [true, false]) { await page.evaluate(value => window.__renderToolbar(value), managed); await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); await page.evaluate(() => document.fonts.ready); for (const width of [699, 700, 701, 720, 721, 740, 620, 461, 460, 400, 320]) { await page.locator('#case').evaluate((element, value) => { element.style.width = `${value}px`; }, width); await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))); const layout = await page.evaluate(() => { const rect = element => { const value = element.getBoundingClientRect(); return { left: value.left, top: value.top, right: value.right, bottom: value.bottom, width: value.width, height: value.height }; }; const tabs = document.querySelector('[data-testid="task-file-sections"]'); const actions = document.querySelector('.file-toolbar-actions'); if (!tabs || !actions) throw new Error('Toolbar regions missing'); const tabRect = rect(tabs); const actionRect = rect(actions); const buttons = [...tabs.querySelectorAll('button')].map(button => { const buttonRect = rect(button); const center = { x: buttonRect.left + buttonRect.width / 2, y: buttonRect.top + buttonRect.height / 2 }; return { id: button.dataset.testid, rect: buttonRect, hit: document.elementFromPoint(center.x, center.y)?.closest('[data-testid]')?.getAttribute('data-testid') ?? null, inside: buttonRect.left >= tabRect.left && buttonRect.right <= tabRect.right, }; }); return { tabs: tabRect, actions: actionRect, buttons }; }); const context = `${language}, managed=${managed}, width=${width}`; if (width <= 720) { assert.ok(layout.actions.top >= layout.tabs.bottom, `actions must occupy a separate row: ${context}`); } else { assert.ok(layout.actions.top < layout.tabs.bottom, `wide toolbar must remain one row: ${context}`); } for (const button of layout.buttons) { assert.ok(button.inside, `${button.id} is clipped outside the section region: ${context}; ${JSON.stringify({ button: button.rect, tabs: layout.tabs })}`); assert.equal(button.hit, button.id, `${button.id} is not the hit target: ${context}`); await page.evaluate(() => { window.__selectedSection = null; }); await page.getByTestId(button.id).click(); assert.equal(await page.evaluate(() => window.__selectedSection), button.id.replace('task-file-section-', ''), `${button.id} click did not select: ${context}`); } const origin = page.getByTestId('task-file-origin-toggle'); await origin.click(); assert.equal(await origin.getAttribute('aria-pressed'), 'true', `origin toggle did not activate: ${context}`); await origin.click(); assert.equal(await origin.getAttribute('aria-pressed'), 'false', `origin toggle did not reset: ${context}`); } } } console.log('task-files-toolbar: 44 width/language/permission cases passed'); } finally { await browser?.close(); await vite.close(); server.close(); await unlink(fixturePath).catch(() => {}); }