const {chromium}=require('playwright'); const assert=require('node:assert/strict'); (async()=>{ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})}); try { for(const theme of ['light','dark']) for(const mobile of [false,true]) { const page=await browser.newPage({viewport:mobile?{width:390,height:844}:{width:1440,height:1000}}); const errors=[]; page.on('pageerror',e=>errors.push(e.message)); await page.goto('http://127.0.0.1:7869?__theme='+theme); const toggle=page.getByRole('button',{name:'利用状況・生成統計'}); await toggle.click(); await page.getByText('PRIVATE-TEST-MODEL',{exact:true}).filter({visible:true}).first().waitFor(); assert.equal(await page.getByRole('button',{name:'Hugging Faceでログイン'}).count(),0); const text=await page.locator('body').innerText(); for(const label of ['昨日・今日のモデル別生成件数','月別合計','統計ファイル更新日時','ログイン不要']) assert(text.includes(label)); assert(text.includes('前月合計')); assert(text.includes('当月合計')); const daily=page.locator('.statistics-daily table'); const monthly=page.locator('.statistics-monthly table'); assert.equal(await daily.locator('tbody tr').count(),3); assert.equal(await monthly.locator('tbody tr').count(),3); assert.match(await daily.locator('tfoot').innerText(),/表示中のモデル合計/); const colors=await daily.evaluate(table=>{ const color=e=>getComputedStyle(e).backgroundColor; const effective=e=>{for(let n=e;n;n=n.parentElement){const c=color(n);if(c!=='rgba(0, 0, 0, 0)'&&c!=='transparent')return c;}}; const panel=document.querySelector('.statistics-panel'); return {body:color(table.querySelector('tbody td')),head:color(table.querySelector('thead th')), total:color(table.querySelector('tfoot td')),panel:effective(panel), page:effective(document.querySelector('.admin-statistics')),text:getComputedStyle(table.querySelector('tbody td')).color, border:getComputedStyle(table.querySelector('tbody td')).borderTopWidth}; }); assert.notEqual(colors.body,'rgba(0, 0, 0, 0)'); assert.equal(colors.panel,colors.page,'Description panel must not introduce a gray Group background'); assert.equal(colors.body,colors.page,'Table body should match the normal theme background'); assert.notEqual(colors.body,colors.head); assert.equal(colors.head,colors.total); assert.equal(colors.border,'1px'); const luminance=color=>{ const channels=color.match(/[\d.]+/g).slice(0,3).map(Number).map(n=>{n/=255;return n<=.04045?n/12.92:((n+.055)/1.055)**2.4;}); return channels[0]*.2126+channels[1]*.7152+channels[2]*.0722; }; const l1=luminance(colors.body),l2=luminance(colors.text); assert((Math.max(l1,l2)+.05)/(Math.min(l1,l2)+.05)>=4.5,'Theme text contrast'); const history=page.getByRole('checkbox',{name:'過去のモデルも表示',exact:true}); assert.equal(await history.isChecked(),false); await history.check(); await page.waitForFunction(()=>document.querySelectorAll('.statistics-daily tbody tr').length===33); assert.equal(await monthly.locator('tbody tr').count(),33); // Every table wrapper grows with content: page scrolling only, no inner Y scroller. const scrollers=await page.locator('.admin-statistics').evaluate(root=>[root,...root.querySelectorAll('*')].filter(e=>{ const s=getComputedStyle(e);return /(auto|scroll)/.test(s.overflowY)&&e.scrollHeight>e.clientHeight+2; }).map(e=>e.className)); assert.deepEqual(scrollers,[]); assert(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth+1)); await history.uncheck(); await page.waitForFunction(()=>document.querySelectorAll('.statistics-daily tbody tr').length===3); await page.getByRole('button',{name:'生成統計を更新'}).click(); await page.getByText('PRIVATE-TEST-MODEL',{exact:true}).filter({visible:true}).first().waitFor(); const config=await (await page.request.get('http://127.0.0.1:7869/config')).json(); const events=config.dependencies.filter(d=>d.api_name?.startsWith('refresh_statistics')); assert(events.length>=1); assert(events.every(d=>d.queue===false&&d.api_visibility==='private')); assert.deepEqual(errors,[]); await page.close(); console.log('PASS '+theme+' '+(mobile?'mobile':'desktop')+' theme backgrounds, borders/contrast, current/history toggle, full-height tables, no overflow, public refresh'); } } finally {await browser.close();} })().catch(e=>{console.error(e);process.exit(1);});