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 { const page=await browser.newPage({viewport:{width:1440,height:1000}}); const errors=[];page.on('pageerror',e=>errors.push(e.message)); await page.goto('http://127.0.0.1:7868',{waitUntil:'domcontentloaded'}); await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click(); const openSamples=()=>page.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click(); await openSamples(); let area=page.locator('.jujo-samples:visible'); await area.locator('.jb-card').first().waitFor(); assert.equal(await area.locator('.jb-card').count(),8); await area.locator('.jb-category-tabs').getByRole('button',{name:'目の形',exact:true}).click(); await area.locator('.jb-search').fill('eye126A'); assert.equal(await area.locator('.jb-card').count(),1); await area.locator('.jb-card').click(); assert.equal(await area.locator('.jb-target').inputValue(),'3'); assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true,includeHidden:true}).isChecked(),false); await area.locator('.jb-strength').fill('0.9'); await area.locator('.jb-apply-button').click(); await area.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました'}).waitFor(); assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true,includeHidden:true}).isChecked(),true); assert.equal(await page.getByRole('checkbox',{name:'LoRA 2 Enable',exact:true,includeHidden:true}).isChecked(),false); await page.getByRole('button',{name:'Generate with Anima',exact:true}).click(); await page.getByText('Received LoRA slots:',{exact:false}).filter({hasText:'eye126A_anima_v10.safetensors'}).waitFor(); console.log('PASS desktop 8 cards, search/category, empty-slot priority, explicit apply, other slots preserved'); await area.locator('.jb-search').fill(''); await area.locator('.jb-next').click(); assert.match(await area.locator('.jb-page').innerText(),/^2 \/ /); await area.locator('.jb-card').first().click(); assert.equal(await area.locator('.jb-target').inputValue(),'2'); console.log('PASS pagination and disabled-slot fallback'); await page.getByRole('checkbox',{name:'LoRA 2 Enable',exact:true}).check(); await page.getByRole('checkbox',{name:'LoRA 4 Enable',exact:true}).check(); await page.waitForFunction(()=>[...document.querySelectorAll('.jb-target option')].filter(x=>x.textContent.includes('(有効)')).length===4); await area.locator('.jb-card').nth(1).click(); assert.equal(await area.locator('.jb-target').inputValue(),''); assert.equal(await area.locator('.jb-apply-button').isDisabled(),true); await area.locator('.jb-target').selectOption('4'); assert.equal(await area.locator('.jb-apply-button').isDisabled(),false); console.log('PASS all-enabled slots require manual choice'); const file3=page.getByRole('combobox',{name:'LoRA 3 File',exact:true}); await file3.fill('---'); await page.getByRole('option').filter({hasText:'未選択・適用しない'}).first().click(); await page.waitForFunction(()=>[...document.querySelectorAll('label')].some(el=>el.innerText.includes('LoRA 3 Enable')&&el.querySelector('input')?.disabled)); assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),false); console.log('PASS manual clear still disables the slot'); const file4=page.getByRole('combobox',{name:'LoRA 4 File',exact:true}); await file4.fill('---'); await page.getByRole('option').filter({hasText:'未選択・適用しない'}).first().click(); await page.getByRole('radio',{name:'非Turbo+Turbo LoRA',exact:true}).check(); await page.waitForFunction(()=>[...document.querySelectorAll('label')].some(el=>el.innerText.includes('LoRA 4 Enable')&&el.querySelector('input')?.checked&&!el.querySelector('input')?.disabled)); console.log('PASS preset restores and enables LoRA 4 after an empty selection'); await page.waitForFunction(()=>[...document.querySelectorAll('.jb-detail[open] .jb-preview img')].some(img=>img.complete&&img.naturalWidth>0),{},{timeout:45000}); if(process.env.JUJO_DESKTOP_SCREENSHOT){await area.screenshot({path:process.env.JUJO_DESKTOP_SCREENSHOT});} // A separate mobile session starts with the same default state. const mobile=await browser.newPage({viewport:{width:390,height:844},isMobile:true,hasTouch:true}); mobile.on('pageerror',e=>errors.push(e.message)); await mobile.goto('http://127.0.0.1:7868',{waitUntil:'domcontentloaded'}); await mobile.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click(); const ma=mobile.locator('.jujo-samples:visible'); await ma.locator('.jb-card').first().waitFor(); assert.equal(await ma.locator('.jb-card').count(),4); await ma.locator('.jb-category').selectOption('目の形'); await ma.locator('.jb-next').click(); const before=await ma.locator('.jb-page').innerText(); const firstName=await ma.locator('.jb-card').first().getAttribute('data-name'); await ma.locator('.jb-card').first().click(); const dialog=ma.locator('.jb-detail'); assert.equal(await dialog.evaluate(el=>el.matches(':modal')),true); assert.equal(await ma.locator('.jb-target').inputValue(),'3'); await ma.locator('.jb-preview').click(); assert.equal(await ma.locator('.jb-zoom').evaluate(el=>el.matches(':modal')),true); await ma.locator('.jb-zoom-close').click(); await ma.locator('.jb-apply-button').click(); await ma.locator('.jb-message').filter({hasText:'LoRA 3に設定して有効化しました'}).waitFor(); await mobile.waitForFunction(()=>[...document.querySelectorAll('.jb-detail[open] .jb-preview img')].some(img=>img.complete&&img.naturalWidth>0),{},{timeout:45000}); if(process.env.JUJO_MOBILE_DETAIL_SCREENSHOT)await mobile.screenshot({path:process.env.JUJO_MOBILE_DETAIL_SCREENSHOT}); await ma.locator('.jb-close').click(); assert.equal(await dialog.isVisible(),false); assert.equal(await ma.locator('.jb-page').innerText(),before); assert.equal(await ma.locator('.jb-card').first().getAttribute('data-name'),firstName); assert.equal(await ma.locator('.jb-category').inputValue(),'目の形'); assert.equal(await mobile.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true); if(process.env.JUJO_MOBILE_SCREENSHOT)await ma.screenshot({path:process.env.JUJO_MOBILE_SCREENSHOT}); console.log('PASS mobile 4 cards, modal detail/zoom, apply, close restores list position and category; no horizontal overflow'); await mobile.getByRole('tab',{name:'Illustrious',exact:true}).click(); await mobile.getByRole('button',{name:'JujoHotaruのLoRAサンプル・使い方はこちら'}).click(); const ia=mobile.locator('.jujo-samples:visible'); await ia.locator('.jb-search').fill('tareme1_v20'); await ia.locator('.jb-card').first().click(); assert.match(await ia.locator('.jb-filename').innerText(),/hotarueye_xl_tareme1_v20/); await ia.locator('.jb-target').selectOption('2'); await ia.locator('.jb-apply-button').click(); await ia.locator('.jb-message').filter({hasText:'LoRA 2に設定して有効化しました'}).waitFor(); await ia.locator('.jb-close').click(); await mobile.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click(); assert.equal(await mobile.getByRole('checkbox',{name:'LoRA 2 Enable',exact:true,includeHidden:true}).isChecked(),true); assert.deepEqual(errors,[]); console.log('PASS ILL slot assignment; no browser errors'); } finally {await browser.close();} })().catch(e=>{console.error(e);process.exitCode=1;});