Files
Azornes-Comfyui-Model-Resolver/tests/test_error_overlay_button.mjs
T

279 lines
8.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { Window } from 'happy-dom';
const modelResolverSource = await readFile(
new URL('../web/resolver/model_resolver.js', import.meta.url),
'utf8'
);
function extractMethod(source, methodName) {
const signature = ` ${methodName}(`;
const start = source.indexOf(signature);
assert.notEqual(start, -1, `Could not find ${methodName}`);
const bodyStart = source.indexOf('{', start);
let depth = 0;
for (let index = bodyStart; index < source.length; index += 1) {
if (source[index] === '{') depth += 1;
if (source[index] !== '}') continue;
depth -= 1;
if (depth === 0) return source.slice(bodyStart + 1, index);
}
throw new Error(`Could not parse ${methodName}`);
}
const hasNativeMissingModelEvidence = new Function(
'overlay',
'detailsButton',
'ERROR_OVERLAY_MESSAGES_SELECTOR',
'NATIVE_MISSING_MODEL_GROUP_SELECTOR',
'NATIVE_MISSING_MODEL_ITEM_SELECTOR',
'NATIVE_MISSING_MODEL_TEXT_PATTERN',
extractMethod(modelResolverSource, 'hasNativeMissingModelEvidence')
);
const checkAndInjectErrorOverlayButton = new Function(
'node',
'ERROR_OVERLAY_SELECTOR',
'ERROR_OVERLAY_DETAILS_BUTTON_SELECTOR',
'ERROR_OVERLAY_RESOLVER_BUTTON_SELECTOR',
extractMethod(modelResolverSource, 'checkAndInjectErrorOverlayButton')
);
const handleNativeMissingModelStoreChange = new Function(
'store',
extractMethod(modelResolverSource, 'handleNativeMissingModelStoreChange')
);
const findNativeErrorChunkUrls = new Function(
'NATIVE_ERROR_CHUNK_PATTERN',
extractMethod(modelResolverSource, 'findNativeErrorChunkUrls')
);
function createOverlay(window, detailsLabel) {
const overlay = window.document.createElement('div');
overlay.setAttribute('data-testid', 'error-overlay');
const messages = window.document.createElement('div');
messages.setAttribute('data-testid', 'error-overlay-messages');
overlay.append(messages);
const actions = window.document.createElement('div');
const detailsButton = window.document.createElement('button');
detailsButton.setAttribute('data-testid', 'error-overlay-see-errors');
detailsButton.className = 'official-button';
detailsButton.textContent = detailsLabel;
actions.append(detailsButton);
overlay.append(actions);
window.document.body.append(overlay);
return { overlay, actions, detailsButton };
}
function createResolver({ nativeMissingModelsPending = false } = {}) {
const resolver = {
nativeMissingModelsPending,
nativeMissingModelsAutoOpened: false,
autoOpenCount: 0,
activationCount: 0,
getNativeMissingModelCandidates(store) {
const candidates = store?.missingModelCandidates?.value
?? store?.missingModelCandidates;
return Array.isArray(candidates) ? candidates : [];
},
hasNativeMissingModelEvidence(overlay, detailsButton) {
return hasNativeMissingModelEvidence.call(
this,
overlay,
detailsButton,
'[data-testid="error-overlay-messages"]',
'[data-testid="error-group-missing-model"]',
'[data-testid^="missing-model-"]',
/\bmissing\s+models?\b/i
);
},
maybeAutoOpenForNativeMissingModels() {
this.autoOpenCount += 1;
},
checkNativeStoreForMissingModels() {},
checkAndInjectErrorOverlayButton(node) {
injectFor(this, node);
},
activateResolverButton() {
this.activationCount += 1;
},
};
return resolver;
}
function injectFor(resolver, node) {
checkAndInjectErrorOverlayButton.call(
resolver,
node,
'[data-testid="error-overlay"]',
'[data-testid="error-overlay-see-errors"]',
'[data-model-resolver-error-action="open"]'
);
}
test('native error chunk lookup includes both ComfyUI frontend layouts', () => {
const previousDocument = globalThis.document;
const previousPerformanceDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'performance'
);
const mainChunk = 'http://localhost:8188/assets/dialogService-main.js';
const loaderChunk = 'http://localhost:8188/assets/dialogService-loader.js';
const settingChunk = 'http://localhost:8188/assets/settingStore-main.js';
globalThis.document = { querySelectorAll: () => [] };
Object.defineProperty(globalThis, 'performance', {
configurable: true,
value: {
getEntriesByType: () => [
{ name: loaderChunk, decodedBodySize: 1179 },
{ name: mainChunk, decodedBodySize: 1118077 },
{ name: settingChunk, decodedBodySize: 1600000 },
],
},
});
try {
assert.deepEqual(
findNativeErrorChunkUrls.call({}, /(?:^|\/)\b(?:dialogService|settingStore)-[^/]+\.js(?:[?#]|$)/),
[settingChunk, mainChunk, loaderChunk]
);
} finally {
globalThis.document = previousDocument;
if (previousPerformanceDescriptor) {
Object.defineProperty(globalThis, 'performance', previousPerformanceDescriptor);
} else {
delete globalThis.performance;
}
}
});
test('error overlay gets one Model Resolver button for native missing-model state', () => {
const window = new Window();
const previousDocument = globalThis.document;
globalThis.document = window.document;
try {
const { overlay, actions, detailsButton } = createOverlay(window, 'View details');
const resolver = createResolver({ nativeMissingModelsPending: true });
injectFor(resolver, overlay);
injectFor(resolver, detailsButton);
const resolverButton = actions.querySelector(
'[data-model-resolver-error-action="open"]'
);
assert.ok(resolverButton);
assert.equal(resolverButton.textContent, 'Open Model Resolver');
assert.equal(resolverButton.className, detailsButton.className);
assert.equal(resolverButton.style.marginLeft, '0.5rem');
assert.deepEqual(
Array.from(actions.children),
[detailsButton, resolverButton]
);
assert.equal(resolver.nativeMissingModelsPending, true);
assert.equal(resolver.autoOpenCount, 1);
resolverButton.click();
assert.equal(resolver.activationCount, 1);
assert.equal(
actions.querySelectorAll('[data-model-resolver-error-action="open"]').length,
1
);
} finally {
globalThis.document = previousDocument;
}
});
test('error overlay is updated when native missing-model state arrives asynchronously', () => {
const window = new Window();
const previousDocument = globalThis.document;
globalThis.document = window.document;
try {
const { overlay, actions } = createOverlay(window, 'View details');
const resolver = createResolver();
injectFor(resolver, overlay);
assert.equal(
actions.querySelector('[data-model-resolver-error-action="open"]'),
null
);
handleNativeMissingModelStoreChange.call(resolver, {
missingModelCandidates: [{ name: 'RealESRGAN_x2.pth' }],
});
assert.ok(actions.querySelector('[data-model-resolver-error-action="open"]'));
assert.equal(resolver.nativeMissingModelsPending, true);
} finally {
globalThis.document = previousDocument;
}
});
test('error overlay gets the button when ComfyUI labels the error as missing models', () => {
const window = new Window();
const previousDocument = globalThis.document;
globalThis.document = window.document;
try {
const { overlay, actions } = createOverlay(window, 'Show missing models');
const resolver = createResolver();
injectFor(resolver, overlay);
assert.ok(actions.querySelector('[data-model-resolver-error-action="open"]'));
} finally {
globalThis.document = previousDocument;
}
});
test('error overlay does not get the button for custom-node-only errors', () => {
const window = new Window();
const previousDocument = globalThis.document;
globalThis.document = window.document;
try {
const { overlay } = createOverlay(window, 'Show missing nodes');
const resolver = createResolver();
injectFor(resolver, overlay);
assert.equal(
overlay.querySelector('[data-model-resolver-error-action="open"]'),
null
);
assert.equal(resolver.autoOpenCount, 0);
} finally {
globalThis.document = previousDocument;
}
});
test('generic native errors do not get the button without model evidence', () => {
const window = new Window();
const previousDocument = globalThis.document;
globalThis.document = window.document;
try {
const { overlay } = createOverlay(window, 'View details');
const resolver = createResolver();
injectFor(resolver, overlay);
assert.equal(
overlay.querySelector('[data-model-resolver-error-action="open"]'),
null
);
} finally {
globalThis.document = previousDocument;
}
});