const {chromium}=require('playwright'); const assert=require('node:assert/strict'); const favoriteKey=f=>`image-model-collection:favorites:v1:${f}`; const settingsKey=f=>`image-model-collection:settings:v1:${f}`; (async()=>{ const browser=await chromium.launch({headless:true,...(process.env.TEST_CHROME?{executablePath:process.env.TEST_CHROME}:{})}); try { for(const mobile of [false,true]) { const page=await browser.newPage({viewport:mobile?{width:390,height:844}:{width:1360,height:1000},isMobile:mobile,hasTouch:mobile,acceptDownloads:true}); const errors=[]; page.on('pageerror',e=>errors.push(e.message)); let accept=true; page.on('dialog',d=>accept?d.accept():d.dismiss()); await page.goto('http://127.0.0.1:7868'); const panel=page.locator('.bf-panel:visible'); const prompt=page.getByRole('textbox',{name:'Prompt',exact:true}); const number=label=>page.getByRole('spinbutton',{name:`number input for ${label}`,exact:true}); const data=()=>page.evaluate(k=>JSON.parse(localStorage.getItem(k)),favoriteKey('anima')); const count=async n=>{await page.waitForFunction(({k,n})=>JSON.parse(localStorage.getItem(k))?.entries.length===n,{k:favoriteKey('anima'),n});}; const importData=async object=>panel.locator('.bf-file').setInputFiles({name:'favorites.json',mimeType:'application/json',buffer:Buffer.from(JSON.stringify(object))}); await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'}); assert.equal(await panel.evaluate(el=>el.open),false); assert(await page.evaluate(()=>{ const visible=selector=>[...document.querySelectorAll(selector)].find(el=>el.offsetParent!==null); const lora=visible('.lora-accordion'), samples=visible('.jujo-samples'); const generate=[...document.querySelectorAll('button')].find(el=>el.textContent.trim()==='Generate with Anima'); return Boolean(lora.compareDocumentPosition(samples)&Node.DOCUMENT_POSITION_FOLLOWING)&&Boolean(samples.compareDocumentPosition(generate)&Node.DOCUMENT_POSITION_FOLLOWING); }),'LoRA -> JujoHotaru -> Generate order'); await panel.locator('summary').click(); assert.equal(await panel.locator('.bf-apply').innerText(),'設定を読み込む'); assert.match(await panel.innerText(),/サイトデータの削除/); assert.match(await panel.innerText(),/画像生成は開始しません/); assert.match(await page.locator('.generation-count-notice:visible').first().innerText(),/モデルの入れ替え・ラインナップ改善/); assert.match(await page.locator('.generation-count-notice:visible').first().innerText(),/利用者を識別する情報は含みません/); await panel.locator('.bf-name').fill('水彩'); await prompt.fill(' favorite prompt'); await number('CFG').fill('3.2'); await number('CFG').press('Tab'); await panel.locator('.bf-add').click(); await count(1); let first=(await data()).entries[0]; assert.equal(first.values.prompt,' favorite prompt'); assert.equal(first.values.cfg,3.2); assert.equal(first.values.seed,'-1'); assert.equal(await panel.locator('b,img').count(),0); await prompt.fill('changed'); await panel.locator('.bf-apply').click(); await panel.locator('.bf-message').filter({hasText:'設定を読み込みました'}).waitFor(); assert.equal(await prompt.inputValue(),first.values.prompt); await prompt.fill('updated'); accept=false; await panel.locator('.bf-update').click(); assert.equal((await data()).entries[0].values.prompt,first.values.prompt); accept=true; await panel.locator('.bf-update').click(); await page.waitForFunction(k=>JSON.parse(localStorage.getItem(k)).entries[0].values.prompt==='updated',favoriteKey('anima')); const downloadPromise=page.waitForEvent('download'); await panel.locator('.bf-export').click(); const download=await downloadPromise; const stream=await download.createReadStream(); const chunks=[];for await(const chunk of stream)chunks.push(chunk); assert.equal(download.suggestedFilename(),'anima-collection-plus-mcp-favorites-anima.json'); const exported=JSON.parse(Buffer.concat(chunks)); assert.equal(exported.entries.length,1); assert.equal(exported.format,'anima-collection-plus-mcp-favorites'); await importData({...exported,format:'imggen-favorites'}); await count(2); assert.equal((await data()).entries[0].name,(await data()).entries[1].name); await panel.locator('.bf-delete').click(); await count(1); await page.reload(); await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'}); await panel.locator('summary').click(); assert.equal(await panel.locator('.bf-list option').count(),2); await panel.locator('.bf-list').selectOption({index:1}); await panel.locator('.bf-seed').selectOption('last'); await panel.locator('.bf-name').fill('last seed'); await panel.locator('.bf-add').click(); await panel.locator('.bf-message').filter({hasText:'まだ生成が完了していません'}).waitFor(); await page.getByRole('button',{name:'Generate with Anima',exact:true}).click(); await page.getByText(/Received LoRA slots:.*seed 3952492001372691585/).waitFor(); await panel.locator('.bf-add').click(); await count(2); assert.equal((await data()).entries[1].values.seed,'3952492001372691585'); await panel.locator('.bf-apply').click(); await panel.locator('.bf-message').filter({hasText:'設定を読み込みました'}).waitFor(); await page.getByRole('button',{name:'Generate with Anima',exact:true}).click(); await page.getByText(/Received LoRA slots:.*seed 3952492001372691585/).waitFor(); const actualSeed=page.locator('input[aria-label*="Seed"]:visible'); await actualSeed.fill('42'); await actualSeed.press('Tab'); await page.waitForTimeout(500); await page.getByRole('button',{name:'Generate with Anima',exact:true}).click(); await page.getByText(/Received LoRA slots:.*seed 42$/).waitFor(); // Automatic prompt opt-out does not silently wipe explicit favorites. const settings=page.locator('.bs-settings:visible'); await settings.locator('summary').click(); await settings.locator('.bs-prompts').uncheck(); await settings.locator('.bs-enabled').uncheck(); assert.equal((await data()).entries.length,2); await panel.locator('.bf-seed').selectOption('input'); await panel.locator('.bf-name').fill('explicit'); await prompt.fill('explicit while auto save off'); await panel.locator('.bf-add').click(); await count(3); assert.equal((await data()).entries[2].values.prompt,'explicit while auto save off'); // Reject malformed/wrong-family/range/oversized imports atomically. for(const invalid of [{...exported,family:'illustrious'}, {...exported,entries:[{name:'x',values:{cfg:999}}]}, {...exported,entries:Array(101).fill(exported.entries[0])}]) { await importData(invalid); await panel.locator('.bf-message').filter({hasText:'インポートできません'}).waitFor(); assert.equal((await data()).entries.length,3); } const missing={...exported,entries:[{name:'missing LoRA',values:{...first.values,lora3_file:'removed.safetensors',lora3_enabled:true,unexpected:'not stored'}}]}; await importData(missing); await count(4); assert(!Object.hasOwn((await data()).entries[3].values,'unexpected')); await panel.locator('.bf-list').selectOption({index:4}); await panel.locator('.bf-apply').click(); await panel.locator('.bf-message').filter({hasText:'未登録の項目'}).waitFor(); await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click(); assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),false); await page.getByRole('tab',{name:'Illustrious',exact:true}).click(); await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'}); await panel.locator('summary').click(); assert.equal(await panel.locator('.bf-list option').count(),1); await panel.locator('.bf-name').fill('ILL'); await panel.locator('.bf-add').click(); await page.waitForFunction(k=>JSON.parse(localStorage.getItem(k))?.entries.length===1,favoriteKey('illustrious')); await page.getByRole('tab',{name:'Anima',exact:true}).click(); const beforePrompt=await prompt.inputValue(); const beforeAuto=await page.evaluate(k=>localStorage.getItem(k),settingsKey('anima')); accept=false; await panel.locator('.bf-clear').click(); assert.equal((await data()).entries.length,4); accept=true; await panel.locator('.bf-clear').click(); assert.equal(await data(),null); assert.equal(await prompt.inputValue(),beforePrompt); assert.equal(await page.evaluate(k=>localStorage.getItem(k),settingsKey('anima')),beforeAuto); assert.equal(await page.evaluate(k=>JSON.parse(localStorage.getItem(k)).entries.length,favoriteKey('illustrious')),1); await importData({...exported,entries:Array.from({length:100},(_,i)=>({...exported.entries[0],name:(i<50?'水彩ABC ':'油彩xyz ')+i}))}); await count(100); assert(await panel.locator('.bf-add').isDisabled()); await importData(exported); await panel.locator('.bf-message').filter({hasText:'100件を超えます'}).waitFor(); await count(100); await page.evaluate(()=>{const original=Storage.prototype.setItem; window.restoreStorage=()=>Storage.prototype.setItem=original;Storage.prototype.setItem=function(){throw Error('quota test');};}); await panel.locator('.bf-list').selectOption({index:1}); await panel.locator('.bf-update').click(); await panel.locator('.bf-message').filter({hasText:'失敗しました'}).waitFor(); await count(100); await page.evaluate(()=>window.restoreStorage()); await panel.locator('.bf-search').fill('水彩abc'); assert.equal(await panel.locator('.bf-list option').count(),51); assert.match(await panel.locator('.bf-count').innerText(),/50件表示/全100件/); await panel.locator('.bf-list').selectOption({index:1}); assert(!await panel.locator('.bf-apply').isDisabled()); await panel.locator('.bf-search').fill('油彩XYZ'); assert.equal(await panel.locator('.bf-list').inputValue(),''); assert(await panel.locator('.bf-update').isDisabled()); assert(await panel.locator('.bf-delete').isDisabled()); assert.equal(await panel.locator('.bf-name').inputValue(),''); await panel.locator('.bf-search').fill('not found'); assert.equal(await panel.locator('.bf-list option').count(),1); assert.match(await panel.locator('.bf-list').innerText(),/一致するお気に入りがありません/); await count(100); const allDownloadPromise=page.waitForEvent('download'); await panel.locator('.bf-export').click(); const allDownload=await allDownloadPromise; const allStream=await allDownload.createReadStream(); const allChunks=[]; for await(const chunk of allStream)allChunks.push(chunk); assert.equal(JSON.parse(Buffer.concat(allChunks)).entries.length,100); await panel.locator('.bf-search').fill(''); assert.equal(await panel.locator('.bf-list option').count(),101); await panel.locator('.bf-search').fill('水彩'); await page.getByRole('tab',{name:'Illustrious',exact:true}).click(); assert.equal(await panel.locator('.bf-search').inputValue(),''); await page.getByRole('tab',{name:'Anima',exact:true}).click(); await panel.locator('.bf-clear').click(); assert.equal(await data(),null); assert.equal(await page.evaluate(k=>JSON.parse(localStorage.getItem(k)).entries.length,favoriteKey('illustrious')),1); assert(await page.evaluate(()=>document.documentElement.scrollWidth <= innerWidth+2)); assert.deepEqual(errors,[]); await page.close(); console.log(`PASS ${mobile?'mobile':'desktop'} 100 favorites, name search, filtered full export/clear, CRUD, JSON, exact seed and mode isolation`); } } finally {await browser.close();} })().catch(e=>{console.error(e);process.exit(1);});