const {chromium} = require('playwright');
const assert = require('node:assert/strict');
const key = family => `image-model-collection:settings:v1:${family}`;
(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:1360,height:1000}});
const errors = [];
page.on('pageerror', error => errors.push(error.message));
const number = label => page.getByRole('spinbutton', {name:`number input for ${label}`, exact:true});
const stored = family => page.evaluate(k => JSON.parse(localStorage.getItem(k)), key(family));
const saved = (family, field, value) => page.waitForFunction(({key,field,value}) => {
const data = JSON.parse(localStorage.getItem(key));
return data?.values?.[field] === value;
}, {key:key(family),field,value});
async function setNumber(label, value) {
await number(label).fill(String(value)); await number(label).press('Tab');
}
async function choose(label, value) {
await page.getByRole('combobox',{name:label,exact:true}).fill(value);
await page.getByRole('option').filter({hasText:value}).first().click();
}
async function settings() {
const area = page.locator('.bs-settings').filter({visible:true});
if (!await area.evaluate(el => el.open)) await area.locator('summary').click();
return area;
}
await page.goto('http://127.0.0.1:7868');
await saved('anima','cfg',5);
assert.equal(await page.locator('.bs-settings').first().evaluate(el => el.open),false);
let area = await settings();
assert.equal(await area.locator('.bs-enabled').isChecked(),true);
assert.equal(await area.locator('.bs-prompts').isChecked(),true);
await page.getByRole('textbox',{name:'Prompt',exact:true}).fill('saved by default');
await page.getByRole('textbox',{name:'Negative prompt',exact:true}).fill('negative saved by default');
await saved('anima','prompt','saved by default');
await saved('anima','negative_prompt','negative saved by default');
await page.reload();
await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
assert.equal(await page.getByRole('textbox',{name:'Prompt',exact:true}).inputValue(),'saved by default');
assert.equal(await page.getByRole('textbox',{name:'Negative prompt',exact:true}).inputValue(),'negative saved by default');
area = await settings();
await area.locator('.bs-prompts').uncheck();
console.log('PASS prompts saved by default and both prompts restored');
await page.getByRole('textbox',{name:'Prompt',exact:true}).fill('private unsaved prompt');
await page.getByRole('radio',{name:'非Turbo+Turbo LoRA',exact:true}).check();
await saved('anima','cfg',1);
await setNumber('CFG',3.2);
await setNumber('Steps',23);
await setNumber('Width',1152);
await choose('Anima Checkpoint','anima-checkpoint-2');
await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
await choose('LoRA 3 File','existing.safetensors');
await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).check();
await saved('anima','lora3_enabled',true);
await saved('anima','cfg',3.2);
assert.equal(Object.hasOwn((await stored('anima')).values,'prompt'),false);
await page.reload();
await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
await saved('anima','cfg',3.2);
assert.equal(await number('CFG').inputValue(),'3.2');
assert.equal(await number('Steps').inputValue(),'23');
assert.equal(await number('Width').inputValue(),'1152');
assert.equal(await page.getByRole('radio',{name:'非Turbo+Turbo LoRA',exact:true}).isChecked(),true);
assert.equal(await page.getByRole('textbox',{name:'Prompt',exact:true}).inputValue(),'masterpiece, best quality,');
await page.getByRole('button',{name:'LoRA設定・追加モデル(最大4つ)'}).click();
assert.equal(await page.getByRole('checkbox',{name:'LoRA 3 Enable',exact:true}).isChecked(),true);
console.log('PASS collapsed auto-restore, custom sampling/dimensions preserved, LoRA enabled, prompts excluded');
area = await settings();
await area.locator('.bs-prompts').check();
await page.getByRole('textbox',{name:'Prompt',exact:true}).fill('
saved as plain text');
await saved('anima','prompt','
saved as plain text');
await page.reload();
await page.waitForFunction(() => document.querySelector('#anima-positive-prompt textarea')?.value.includes('saved as plain text'));
assert.equal(await page.locator('#anima-positive-prompt img').count(),0);
area = await settings();
await area.locator('.bs-prompts').uncheck();
assert.equal(Object.hasOwn((await stored('anima')).values,'prompt'),false);
await page.reload();
await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
area = await settings();
assert.equal(await area.locator('.bs-prompts').isChecked(),false);
console.log('PASS prompt opt-in, plain-text restoration, opt-out removes stored prompt');
await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
await saved('illustrious','cfg',5);
await choose('VAE','SDXL VAE');
await page.getByRole('checkbox',{name:'test-embedding',exact:true}).check();
await setNumber('CFG',4.1);
await saved('illustrious','vae','sdxl');
await saved('illustrious','cfg',4.1);
assert.equal((await stored('anima')).values.cfg,3.2);
await page.reload();
await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
await page.waitForFunction(() => [...document.querySelectorAll('input[aria-label="number input for CFG"]')].some(el => el.value==='4.1'));
assert.equal(await number('CFG').inputValue(),'4.1');
assert.equal(await page.getByRole('combobox',{name:'VAE',exact:true}).inputValue(),'SDXL VAE');
assert.equal(await page.getByRole('checkbox',{name:'test-embedding',exact:true}).isChecked(),true);
console.log('PASS independent family settings and Illustrious VAE');
area = await settings();
await area.locator('.bs-reset').click();
await saved('illustrious','cfg',5);
assert.equal(await number('CFG').inputValue(),'5');
await area.locator('.bs-delete').click();
let record = await stored('illustrious');
assert.deepEqual(record,{version:1,family:'illustrious',enabled:false});
await setNumber('CFG',6);
await page.waitForTimeout(900);
assert.equal((await stored('illustrious')).values,undefined);
await page.reload();
await page.getByRole('tab',{name:'Illustrious',exact:true}).click();
area = await settings();
await page.waitForFunction(() => [...document.querySelectorAll('.bs-enabled')].some(el => !el.disabled&&!el.checked));
assert.equal(await area.locator('.bs-enabled').isChecked(),false);
assert.equal(await number('CFG').inputValue(),'5');
console.log('PASS reset defaults, deletion, disabled saving stays disabled on reload');
// Inject before app initialization; pagehide normally saves the current UI.
await page.addInitScript(k => localStorage.setItem(k, JSON.stringify({version:1,family:'anima',enabled:true,prompts:false,values:{model:'../evil',cfg:999,width:99999,lora3_file:'../evil.safetensors',lora3_enabled:true,prompt:'must not restore'}})), key('anima'));
await page.reload();
await page.locator('.bs-settings:visible .bs-enabled:not(:disabled)').waitFor({state:'attached'});
await saved('anima','cfg',5);
record = await stored('anima');
assert.equal(record.values.model,'anima-checkpoint');
assert.equal(record.values.lora3_file,'---');
assert.equal(record.values.lora3_enabled,false);
assert.equal(Object.hasOwn(record.values,'prompt'),false);
assert.equal(await page.getByText('Received LoRA slots:',{exact:false}).count(),0);
console.log('PASS modified browser data validated and restoration never generates');
const mobile = await browser.newPage({viewport:{width:390,height:844},isMobile:true,hasTouch:true});
mobile.on('pageerror', error => errors.push(error.message));
await mobile.goto('http://127.0.0.1:7868');
await mobile.locator('.bs-settings summary').click();
await mobile.locator('.bs-enabled:not(:disabled)').waitFor();
assert.equal(await mobile.evaluate(() => document.documentElement.scrollWidth<=innerWidth),true);
assert.equal(await mobile.locator('.bs-prompts').isChecked(),true);
await mobile.locator('.bs-prompts').check();
await mobile.getByRole('textbox',{name:'Prompt',exact:true}).fill('mobile prompt');
await mobile.waitForFunction(k => JSON.parse(localStorage.getItem(k))?.values?.prompt==='mobile prompt',key('anima'));
await mobile.reload();
await mobile.waitForFunction(() => document.querySelector('#anima-positive-prompt textarea')?.value==='mobile prompt');
console.log('PASS mobile layout and restoration');
const blocked = await browser.newPage();
blocked.on('pageerror', error => errors.push(error.message));
await blocked.addInitScript(() => {
const get = Storage.prototype.getItem, set = Storage.prototype.setItem;
Storage.prototype.getItem = function(key) {
if (key.startsWith('image-model-collection:settings:')) throw new Error('storage denied');
return get.call(this,key);
};
Storage.prototype.setItem = function(key,value) {
if (key.startsWith('image-model-collection:settings:')) throw new Error('storage denied');
return set.call(this,key,value);
};
});
await blocked.goto('http://127.0.0.1:7868');
await blocked.locator('.bs-settings summary').click();
await blocked.getByText('ブラウザの保存領域を利用できません。現在の設定で生成はできます。',{exact:true}).waitFor();
assert.equal(await blocked.getByRole('button',{name:'Generate with Anima',exact:true}).isEnabled(),true);
console.log('PASS unavailable browser storage does not prevent generation');
assert.deepEqual(errors,[]);
} finally { await browser.close(); }
})().catch(error => {console.error(error);process.exitCode=1;});