Files
DemonGatanjieuandClaude Opus 5.5 47442d00f6 Keep the Saves list still during a press
A new picture puts itself first in its line, so a press on a picture
ended on another one and was no click. While the main button is held on
a button of the list, what comes waits until the press and its click
are over.

Tested with a real mouse on an isolated ComfyUI: a press held until a
new picture came opened the pressed picture 4 times of 4 (0 before), and
16 of 16 long presses during runs answered (13 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 21:21:41 +08:00

211 lines
11 KiB
HTML

<!doctype html>
<meta charset="utf-8">
<title>Saves bar and page regression</title>
<link rel="stylesheet" href="../web/styles/28-workflows.css">
<script type="importmap">{"imports":{"/scripts/app.js":"data:text/javascript,export const app = {};","/scripts/api.js":"data:text/javascript,export const api = new EventTarget();"}}</script>
<pre id="results">Running…</pre>
<div class="graph-canvas-panel"></div>
<div id="page"></div>
<script type="module">
import { app } from '/scripts/app.js';
import { api } from '/scripts/api.js';
import { translate as t } from '../web/modules/locales.js';
import { MAX_RUNS } from '../web/modules/snapshot_plan.js';
import { refreshGroup } from '../web/modules/snapshot_store.js';
import { savesToggle } from '../web/modules/ui_saves_list.js';
import { watchSaves } from '../web/modules/ui_snapshot_bar.js';
// The real bar and Saves page. The browser sends no click to an element that left the page during the press,
// so what must hold is that nothing here takes a control off it. Only ComfyUI and the saves routes are fake.
const save = id => ({ id, at: 0, count: 1, changes: [], pictures: [], running: 0 });
const group = { key: 'k', title: '', path: '', saves: [save('a'), save('b')] };
let slow = 0; // how long a count's answer takes
let refused = 0; // the count that is not taken
window.fetch = async (url, options) => {
if (String(url).includes('/saves/update')) {
await new Promise(resolve => setTimeout(resolve, slow));
const { id, count } = JSON.parse(options.body);
if (count === refused) return new Response(JSON.stringify({ message: 'disk full' }), { status: 500 });
group.saves.find(item => item.id === id).count = count;
}
return new Response(JSON.stringify({ group, groups: [{ key: 'k', title: 'canvas', path: '', count: group.saves.length, updated: 0, cover: null }], workflow: {} }));
};
app.rootGraph = { extra: { anomalous_saves: 'k' }, serialize: () => ({}) };
app.graph = app.rootGraph;
app.extensionManager = { workflow: { activeWorkflow: { path: 'a.json', changeTracker: { undoQueue: [], redoQueue: [], updateState: async () => {} } } } };
const results = document.querySelector('#results');
let failures = 0;
const check = (condition, text) => { results.textContent += `${condition ? 'PASS' : 'FAIL'} ${text}\n`; if (!condition) failures++; };
const one = selector => document.querySelector(selector);
const all = selector => [...document.querySelectorAll(selector)];
const answered = () => new Promise(resolve => setTimeout(resolve, 30));
const typed = async (box, value) => {
box.value = value;
box.dispatchEvent(new Event('change'));
await answered();
};
try {
results.textContent = '';
watchSaves(() => null);
await answered();
one('.anomalous-snapshot-number').click();
await answered();
check(all('.anomalous-snapshot-mark.is-current').length === 1, 'a number lights when the canvas goes back to it');
const controls = all('#anomalous-snapshot-bar button, #anomalous-snapshot-bar input');
const stayed = () => controls.every(node => node.isConnected);
api.dispatchEvent(new CustomEvent('graphChanged'));
check(!one('.is-current'), 'a change darkens the lit number');
check(stayed(), 'a change keeps every control on the page: ComfyUI tells of it between a press and its click');
const [box, other] = all('.anomalous-snapshot-count');
await typed(box, '3');
check(group.saves[0].count === 3, 'a count is sent');
check(stayed(), 'the answer to a count keeps every control on the page: it comes during the press on the next one');
check(one('.anomalous-snapshot-run').textContent.includes('4'), 'Run says the new total');
await typed(box, '5');
check(group.saves[0].count === 5, 'a box that was redrawn sends what it holds');
box.focus();
box.value = '7';
group.saves[1].count = 2;
await refreshGroup('k');
check(document.activeElement === box && box.value === '7', 'a run ending leaves a box being typed in alone');
check(other.value === '2', 'the other boxes come up to date');
check(stayed(), 'a run ending keeps every control on the page');
box.blur();
refused = 9;
await typed(box, '9');
check(box.value === '5' && group.saves[0].count === 5, 'a count not taken goes back to the one stored');
check(all('.anomalous-mixer-toast').at(-1)?.textContent === t('snapshotCountFailed', { why: 'disk full' }), 'and says so');
box.focus();
await typed(box, '9');
check(document.activeElement === box && box.value === '5', 'in a box that keeps the focus too, after Enter');
box.blur();
slow = 10;
box.value = '6';
box.dispatchEvent(new Event('change'));
box.value = '9';
box.dispatchEvent(new Event('change'));
await answered();
check(box.value === '6', 'behind a count that was taken, it goes back to that one');
slow = 0;
refused = 0;
await typed(box, '5');
check(group.saves[0].count === 5, 'and the box goes on working');
group.saves.push(save('c'));
await refreshGroup('k');
check(all('.anomalous-snapshot-number').length === 3, 'a new save shows');
group.saves.shift();
await refreshGroup('k');
const left = all('.anomalous-snapshot-mark');
check(left.map(chip => chip.dataset.key).join() === 'b,c' && all('.anomalous-snapshot-count')[0].value === '2', 'a part left by a removed save does not go on as the next one');
one('.anomalous-snapshot-close').click();
const handle = one('.anomalous-snapshot-unfold');
await refreshGroup('k');
check(handle.isConnected, 'the folded handle stays on the page too');
handle.click();
check(one('.anomalous-snapshot-add') && one('.anomalous-snapshot-status') && !one('.anomalous-snapshot-unfold'), 'unfolding brings the row back');
const picture = filename => ({ filename, subfolder: '', type: 'output' });
group.saves[0].pictures = [picture('one.png')];
const owner = { recipeView: one('#page'), close() {}, show() {}, refreshRecipes() {} };
savesToggle(owner);
owner.savesOpen.add('k');
owner.savesToggle.click();
await answered();
const pageControls = all('.anomalous-saves-part button, .anomalous-saves-part input');
const pageStayed = () => pageControls.every(node => node.isConnected);
check(all('.anomalous-saves-save').length === 2 && one('.anomalous-saves-picture'), 'the page shows the saves and their pictures');
const pageBox = one('.anomalous-saves-tools .anomalous-snapshot-count');
await typed(pageBox, '4');
check(group.saves[0].count === 4 && pageStayed(), 'on the page too, the answer to a count keeps every control there');
check(one('.anomalous-saves-head .anomalous-snapshot-run').textContent.includes('5'), 'the part says the new total');
pageBox.focus();
pageBox.value = '8';
await refreshGroup('k');
check(document.activeElement === pageBox && pageBox.value === '8' && pageStayed(), 'a run ending leaves the part alone, a box being typed in too');
pageBox.blur();
const more = one('.anomalous-saves-tools .anomalous-wf-icon-btn');
more.click();
const [button, menu] = [one('.anomalous-saves-tools .anomalous-wf-icon-btn').getBoundingClientRect(), one('.anomalous-popup-menu').getBoundingClientRect()];
check(Math.abs(menu.right - button.right) < 2, 'a redrawn card\'s menu opens by its button');
more.click();
check(!one('.anomalous-popup-menu'), 'a second press closes it');
const tiles = all('.anomalous-saves-picture');
await refreshGroup('k');
check(tiles.every(node => node.isConnected), 'pictures that did not change stay on the page');
group.saves[0].pictures.push(picture('two.png'));
await refreshGroup('k');
const thumbs = all('.anomalous-saves-picture img');
check(thumbs.length === 2 && thumbs[0].src.includes('two.png'), 'a new picture goes first in its line');
const press = (node, button = 0) => node.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button }));
const firstThumb = () => one('.anomalous-saves-picture img').src;
const pressed = one('.anomalous-saves-picture');
press(pressed.querySelector('img'));
group.saves[0].pictures.push(picture('three.png'));
await refreshGroup('k');
check(pressed.isConnected && firstThumb().includes('two.png'), 'a press on a picture holds its line: a new one would put another under the pointer');
group.saves[0].pictures.push(picture('four.png'));
await refreshGroup('k');
window.dispatchEvent(new PointerEvent('pointerup'));
check(pressed.isConnected, 'its click still finds it as the press ends');
await answered();
check(firstThumb().includes('four.png'), 'what came meanwhile shows after the press, as it stands last');
press(one('.anomalous-saves-picture'));
group.saves[0].pictures.push(picture('five.png'));
await refreshGroup('k');
window.dispatchEvent(new PointerEvent('pointercancel'));
await answered();
check(firstThumb().includes('five.png'), 'a press the browser takes over, a drag, ends the hold too');
press(one('.anomalous-saves-picture'), 2);
group.saves[0].pictures.push(picture('six.png'));
await refreshGroup('k');
check(firstThumb().includes('six.png'), 'a right press holds nothing: no click follows it, its pointerup may never come');
press(one('.anomalous-saves-list'));
group.saves[0].pictures.push(picture('seven.png'));
await refreshGroup('k');
check(firstThumb().includes('seven.png'), 'nor does a press beside the buttons, on the scrollbar say');
group.saves[0].pictures.length = 2;
await refreshGroup('k');
group.saves[0].count = MAX_RUNS + 1;
await refreshGroup('k');
one('.anomalous-saves-head .anomalous-snapshot-run').click();
await answered();
check(one('.anomalous-saves-status').textContent, 'a refused run says so on the part it has after a redraw');
group.saves[0].count = 1;
one('.anomalous-saves-fold').click();
check(!one('.anomalous-saves-save'), 'a part folds');
one('.anomalous-saves-fold').click();
await answered();
check(all('.anomalous-saves-save').length === 2 && all('.anomalous-saves-picture').length === 2, 'and opens with its cards and pictures');
group.saves.shift();
await refreshGroup('k');
check(all('.anomalous-saves-save').map(card => card.dataset.key).join() === 'c' && one('.anomalous-saves-number').textContent === '1', 'a card left by a removed save does not go on as the next one');
group.saves.push({ ...save('d'), count: MAX_RUNS - 1 });
await refreshGroup('k');
slow = 60;
const late = one('.anomalous-snapshot-mark .anomalous-snapshot-count');
late.value = '2'; // one over, once it counts
late.dispatchEvent(new Event('change'));
one('#anomalous-snapshot-bar .anomalous-snapshot-run').click();
await new Promise(resolve => setTimeout(resolve, 150));
check(one('.anomalous-snapshot-status').textContent === t('snapshotTooMany', { most: MAX_RUNS }), 'Run goes by a count whose answer comes after the click');
results.textContent += failures ? `${failures} FAILED` : 'ALL PASSED';
} catch (error) { results.textContent += `FAIL ${error.stack}`; }
</script>