610 lines
21 KiB
JavaScript
610 lines
21 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { imageMetadataMethods } from '../web/resolver/views/tabs_image_metadata_methods.js';
|
|
import {
|
|
extractWorkflowPrompts,
|
|
getPromptCandidateScore,
|
|
sortPromptCandidates,
|
|
updateWorkflowPromptValue,
|
|
} from '../web/resolver/utils/prompt_utils.js';
|
|
|
|
function clipTextNode(id, text, outputLink, { title = '', textLink = null } = {}) {
|
|
return {
|
|
id,
|
|
type: 'CLIPTextEncode',
|
|
title,
|
|
widgets_values: [text],
|
|
inputs: [
|
|
{ name: 'clip', type: 'CLIP', link: 1 },
|
|
{ name: 'text', type: 'STRING', widget: { name: 'text' }, link: textLink },
|
|
],
|
|
outputs: [{ links: [outputLink] }],
|
|
};
|
|
}
|
|
|
|
function createRoleWorkflow(positiveText = 'positive source', negativeText = 'negative source') {
|
|
return {
|
|
nodes: [
|
|
clipTextNode(1, positiveText, 11),
|
|
clipTextNode(2, negativeText, 12),
|
|
{
|
|
id: 3,
|
|
type: 'KSampler',
|
|
inputs: [
|
|
{ name: 'positive', type: 'CONDITIONING', link: 11 },
|
|
{ name: 'negative', type: 'CONDITIONING', link: 12 },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
function createContext(sourceWorkflow, currentWorkflow) {
|
|
const state = {
|
|
metadata: { workflow: sourceWorkflow },
|
|
promptTransfer: null,
|
|
};
|
|
const notifications = [];
|
|
let workflow = currentWorkflow;
|
|
return {
|
|
...imageMetadataMethods,
|
|
imageInspectorState: state,
|
|
getCurrentWorkflow: () => workflow,
|
|
getWorkflowSignature: value => JSON.stringify(value),
|
|
updateWorkflowInComfyUI: async value => {
|
|
workflow = value;
|
|
return true;
|
|
},
|
|
openMetadataTransfer() {},
|
|
renderMetadataPromptTransferPanelInPlace() {},
|
|
escapeHtml: value => String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, '''),
|
|
showNotification(message, type) {
|
|
notifications.push({ message, type });
|
|
},
|
|
getWorkflow() {
|
|
return workflow;
|
|
},
|
|
notifications,
|
|
};
|
|
}
|
|
|
|
test('live prompt preview detects positive and negative text changes without changing imported metadata', () => {
|
|
const source = createRoleWorkflow('imported positive', 'imported negative');
|
|
const current = createRoleWorkflow('current positive', 'current negative');
|
|
const dialog = createContext(source, current);
|
|
let refreshes = 0;
|
|
dialog.renderMetadataPromptTransferPanelInPlace = () => {
|
|
refreshes += 1;
|
|
dialog.renderMetadataPromptTransferPanel(dialog.imageInspectorState);
|
|
return true;
|
|
};
|
|
dialog.renderMetadataPromptTransferPanel(dialog.imageInspectorState);
|
|
assert.equal(dialog.refreshMetadataPromptPreview(current), false);
|
|
current.nodes[0].widgets_values[0] = 'edited positive';
|
|
assert.equal(dialog.refreshMetadataPromptPreview(current), true);
|
|
assert.match(dialog.renderMetadataPromptTransferPanel(dialog.imageInspectorState), /edited positive/);
|
|
current.nodes[1].widgets_values[0] = 'edited negative';
|
|
assert.equal(dialog.refreshMetadataPromptPreview(current), true);
|
|
assert.equal(dialog.refreshMetadataPromptPreview(current), false);
|
|
assert.equal(refreshes, 2);
|
|
assert.equal(source.nodes[0].widgets_values[0], 'imported positive');
|
|
});
|
|
|
|
test('workflow prompt extraction distinguishes positive and negative CLIP text nodes', () => {
|
|
const prompts = extractWorkflowPrompts(createRoleWorkflow());
|
|
|
|
assert.deepEqual(
|
|
prompts.map(prompt => [prompt.node_id, prompt.text, prompt.role, prompt.widget_name]),
|
|
[
|
|
[1, 'positive source', 'positive', 'text'],
|
|
[2, 'negative source', 'negative', 'text'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('prompt candidates are ranked by role and prompt-specific node type', () => {
|
|
const candidates = [
|
|
{
|
|
id: 'generic',
|
|
node_type: 'PrimitiveStringMultiline',
|
|
node_title: 'Text value',
|
|
widget_name: 'value',
|
|
role: 'unknown',
|
|
role_candidates: [],
|
|
},
|
|
{
|
|
id: 'clip',
|
|
node_type: 'CLIPTextEncode',
|
|
node_title: '',
|
|
widget_name: 'text',
|
|
role: 'positive',
|
|
role_candidates: ['positive'],
|
|
},
|
|
];
|
|
|
|
assert.ok(getPromptCandidateScore(candidates[1], 'positive') > getPromptCandidateScore(candidates[0], 'positive'));
|
|
assert.deepEqual(
|
|
sortPromptCandidates(candidates, 'positive').map(candidate => candidate.id),
|
|
['clip', 'generic'],
|
|
);
|
|
});
|
|
|
|
test('workflow prompt extraction inherits a role for a plain text node feeding CLIPTextEncode', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 10,
|
|
type: 'PrimitiveStringMultiline',
|
|
widgets_values: ['plain positive'],
|
|
outputs: [{ links: [20] }],
|
|
},
|
|
clipTextNode(11, 'stale value', 30, { textLink: 20 }),
|
|
{
|
|
id: 12,
|
|
type: 'KSampler',
|
|
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 30 }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const prompts = extractWorkflowPrompts(workflow);
|
|
|
|
assert.equal(prompts.find(prompt => prompt.node_id === 10)?.role, 'positive');
|
|
assert.equal(prompts.find(prompt => prompt.node_id === 10)?.text, 'plain positive');
|
|
assert.equal(prompts.find(prompt => prompt.node_id === 11)?.linked, true);
|
|
});
|
|
|
|
test('workflow prompt extraction recognizes generic text nodes through prompt-node links', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 20,
|
|
type: 'StringLiteral',
|
|
widgets_values: ['generic positive'],
|
|
outputs: [{ links: [40] }],
|
|
},
|
|
{
|
|
id: 21,
|
|
type: 'CLIPTextEncodeSD3',
|
|
widgets_values: ['stale value'],
|
|
inputs: [{ name: 'clip_l', type: 'STRING', widget: { name: 'clip_l' }, link: 40 }],
|
|
outputs: [{ links: [41] }],
|
|
},
|
|
{
|
|
id: 22,
|
|
type: 'KSampler',
|
|
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 41 }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const prompt = extractWorkflowPrompts(workflow).find(item => item.node_id === 20);
|
|
|
|
assert.equal(prompt?.text, 'generic positive');
|
|
assert.equal(prompt?.role, 'positive');
|
|
});
|
|
|
|
test('workflow prompt extraction ignores list and control widgets on prompt-named nodes', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 30,
|
|
type: 'Prompt enhancement seed',
|
|
title: 'Prompt enhancement seed (if used)',
|
|
widgets: [
|
|
{ name: 'value', type: 'INT' },
|
|
{ name: 'control_after_generate', type: 'COMBO', options: ['fixed', 'increment'] },
|
|
],
|
|
widgets_values: [1, 'fixed'],
|
|
inputs: [
|
|
{ name: 'value', type: 'INT' },
|
|
{ name: 'control_after_generate', type: 'COMBO', options: ['fixed', 'increment'] },
|
|
],
|
|
},
|
|
{
|
|
id: 31,
|
|
type: 'Prompt enhancement seed',
|
|
title: 'Prompt enhancement seed (if used)',
|
|
widgets_values: [1, 'fixed'],
|
|
},
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(extractWorkflowPrompts(workflow), []);
|
|
});
|
|
|
|
test('workflow prompt extraction ignores filename fields on image output nodes', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 371,
|
|
type: 'CLIPTextEncode',
|
|
title: 'Prompt',
|
|
widgets_values: ['This is prompt positive'],
|
|
outputs: [{ name: 'CONDITIONING', type: 'CONDITIONING', links: [584] }],
|
|
},
|
|
{
|
|
id: 594,
|
|
type: 'SaveImage',
|
|
title: 'Almost done',
|
|
widgets: [{ name: 'filename_prefix', type: 'STRING', value: 'ComfyUI' }],
|
|
widgets_values: ['ComfyUI'],
|
|
inputs: [{ name: 'images', type: 'IMAGE', link: 942 }],
|
|
outputs: [{ name: 'images', type: 'IMAGE', links: null }],
|
|
},
|
|
{
|
|
id: 596,
|
|
type: 'SaveImage',
|
|
title: 'Your Porn',
|
|
widgets: [{ name: 'filename_prefix', type: 'STRING', value: 'ComfyUI' }],
|
|
widgets_values: ['ComfyUI'],
|
|
inputs: [{ name: 'images', type: 'IMAGE', link: 945 }],
|
|
outputs: [{ name: 'images', type: 'IMAGE', links: null }],
|
|
},
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(
|
|
extractWorkflowPrompts(workflow).map(prompt => prompt.node_id),
|
|
[371],
|
|
);
|
|
});
|
|
|
|
test('workflow prompt extraction tolerates sparse widget value arrays', () => {
|
|
const widgetsValues = new Array(2);
|
|
widgetsValues[1] = 'sparse prompt';
|
|
const workflow = {
|
|
nodes: [{
|
|
id: 32,
|
|
type: 'PrimitiveStringMultiline',
|
|
widgets_values: widgetsValues,
|
|
}],
|
|
};
|
|
|
|
assert.deepEqual(
|
|
extractWorkflowPrompts(workflow).map(prompt => [prompt.node_id, prompt.widget_index, prompt.text]),
|
|
[[32, 1, 'sparse prompt']],
|
|
);
|
|
});
|
|
|
|
test('workflow prompt extraction follows serialized positive links and leaves negative optional', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 40,
|
|
type: 'PrimitiveStringMultiline',
|
|
widgets_values: ['serialized positive'],
|
|
},
|
|
{
|
|
id: 41,
|
|
type: 'CLIPTextEncode',
|
|
widgets_values: ['stale value'],
|
|
inputs: [{ name: 'text', type: 'STRING', widget: { name: 'text' }, link: 401 }],
|
|
},
|
|
{
|
|
id: 42,
|
|
type: 'KSampler',
|
|
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 402 }],
|
|
},
|
|
],
|
|
links: [
|
|
[401, 40, 0, 41, 0, 'STRING'],
|
|
[402, 41, 0, 42, 0, 'CONDITIONING'],
|
|
],
|
|
};
|
|
|
|
const prompts = extractWorkflowPrompts(workflow);
|
|
|
|
assert.equal(prompts.find(prompt => prompt.node_id === 40)?.role, 'positive');
|
|
assert.equal(prompts.some(prompt => prompt.role === 'negative'), false);
|
|
});
|
|
|
|
test('workflow prompt traversal follows text inputs and skips unrelated seed branches', () => {
|
|
const workflow = {
|
|
nodes: [
|
|
{
|
|
id: 50,
|
|
type: 'StringLiteral',
|
|
widgets_values: ['connected positive'],
|
|
outputs: [{ links: [501] }],
|
|
},
|
|
{
|
|
id: 51,
|
|
type: 'PromptEnhancer',
|
|
widgets: [
|
|
{ name: 'text', type: 'STRING' },
|
|
{ name: 'control_after_generate', type: 'COMBO', options: ['fixed', 'increment'] },
|
|
],
|
|
widgets_values: ['enhanced positive', 'fixed'],
|
|
inputs: [
|
|
{ name: 'text', type: 'STRING', link: 501 },
|
|
{ name: 'seed', type: 'INT', link: 502 },
|
|
],
|
|
outputs: [{ links: [503] }],
|
|
},
|
|
{
|
|
id: 52,
|
|
type: 'Prompt enhancement seed',
|
|
widgets: [
|
|
{ name: 'value', type: 'INT' },
|
|
{ name: 'control_after_generate', type: 'COMBO', options: ['fixed', 'increment'] },
|
|
],
|
|
widgets_values: [1, 'fixed'],
|
|
outputs: [{ links: [502] }],
|
|
},
|
|
{
|
|
id: 53,
|
|
type: 'KSampler',
|
|
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 503 }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const prompts = extractWorkflowPrompts(workflow);
|
|
|
|
assert.deepEqual(
|
|
prompts.map(prompt => [prompt.node_id, prompt.text, prompt.role]),
|
|
[
|
|
[50, 'connected positive', 'positive'],
|
|
[51, 'enhanced positive', 'positive'],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('workflow prompt replacement updates the serialized text widget and named value', () => {
|
|
const workflow = createRoleWorkflow();
|
|
workflow.nodes[0].widgets_values_named = { text: 'positive source' };
|
|
const prompt = extractWorkflowPrompts(workflow).find(item => item.role === 'positive');
|
|
|
|
assert.equal(updateWorkflowPromptValue(workflow, prompt, 'replacement'), true);
|
|
assert.equal(workflow.nodes[0].widgets_values[0], 'replacement');
|
|
assert.equal(workflow.nodes[0].widgets_values_named.text, 'replacement');
|
|
});
|
|
|
|
test('linked prompt inputs are not treated as replaceable text fields', () => {
|
|
const workflow = createRoleWorkflow('linked value');
|
|
workflow.nodes[0].inputs[1].link = 99;
|
|
const prompt = extractWorkflowPrompts(workflow).find(item => item.node_id === 1);
|
|
|
|
assert.equal(prompt.linked, true);
|
|
assert.equal(updateWorkflowPromptValue(workflow, prompt, 'replacement'), false);
|
|
assert.equal(workflow.nodes[0].widgets_values[0], 'linked value');
|
|
});
|
|
|
|
test('metadata prompt panel renders separate positive and negative transfer rows', () => {
|
|
const context = createContext(createRoleWorkflow(), createRoleWorkflow('current positive', 'current negative'));
|
|
const contextMenuCalls = [];
|
|
context.getContextMenuAttrs = (model, tooltip) => {
|
|
contextMenuCalls.push({ model, tooltip });
|
|
return ` data-context-scope="${model.context_scope}" data-node-id="${model.node_id}"`;
|
|
};
|
|
const rows = context.getMetadataPromptTransferRows(context.imageInspectorState);
|
|
const html = context.renderMetadataPromptTransferPanel(context.imageInspectorState);
|
|
|
|
assert.deepEqual(rows.map(row => row.role), ['positive', 'negative']);
|
|
assert.equal(rows[0].source.text, 'positive source');
|
|
assert.equal(rows[1].source.text, 'negative source');
|
|
assert.match(html, /Positive prompt/);
|
|
assert.match(html, /Negative prompt/);
|
|
assert.match(html, /data-image-prompt-source="positive"/);
|
|
assert.match(html, /data-image-prompt-target="negative"/);
|
|
assert.match(html, /data-image-prompt-role-selection="positive"[^>]*checked/);
|
|
assert.match(html, /data-image-prompt-role-selection="negative"[^>]*checked/);
|
|
assert.equal((html.match(/data-image-prompt-copy/g) || []).length, 4);
|
|
assert.match(html, /class="mr-btn mr-btn-secondary mr-btn-sm mr-image-prompt-copy"[^>]*data-image-prompt-copy/);
|
|
assert.match(html, /data-tooltip="["]*Positive\/negative role detected from workflow connections\./);
|
|
assert.doesNotMatch(html, /class="mr-image-prompt-meta"/);
|
|
assert.match(html, /class="mr-tooltip-badge"[^>]*data-tooltip="[\s\S]*Select which prompt roles to replace/);
|
|
assert.doesNotMatch(html, /class="mr-image-prompt-note"/);
|
|
const positiveTargetIndex = html.indexOf('data-image-prompt-target="positive"');
|
|
const positiveSourceIndex = html.indexOf('data-image-prompt-source="positive"');
|
|
assert.ok(positiveTargetIndex >= 0 && positiveTargetIndex < positiveSourceIndex);
|
|
assert.deepEqual(
|
|
contextMenuCalls.map(call => [call.model.context_scope, call.model.node_id]),
|
|
[['workflow_node', 1], ['workflow_node', 2]],
|
|
);
|
|
assert.match(html, /class="mr-image-prompt-side is-context-menu" data-context-scope="workflow_node" data-node-id="1"/);
|
|
assert.match(html, /Replace prompts/);
|
|
});
|
|
|
|
test('metadata prompt copy uses the visible prompt preview text', async () => {
|
|
const context = createContext(null, createRoleWorkflow());
|
|
const copied = [];
|
|
const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
|
Object.defineProperty(globalThis, 'navigator', {
|
|
configurable: true,
|
|
value: {
|
|
clipboard: {
|
|
async writeText(value) {
|
|
copied.push(value);
|
|
},
|
|
},
|
|
},
|
|
writable: true,
|
|
});
|
|
const button = {
|
|
innerHTML: '<svg></svg> Copy',
|
|
isConnected: false,
|
|
classList: {
|
|
add() {},
|
|
remove() {},
|
|
},
|
|
closest(selector) {
|
|
assert.equal(selector, '.mr-image-prompt-side');
|
|
return {
|
|
querySelector(query) {
|
|
assert.equal(query, '.mr-image-prompt-text');
|
|
return { textContent: 'visible prompt preview' };
|
|
},
|
|
};
|
|
},
|
|
};
|
|
|
|
try {
|
|
assert.equal(await context.copyMetadataPromptText(button), true);
|
|
assert.deepEqual(copied, ['visible prompt preview']);
|
|
} finally {
|
|
if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator);
|
|
else delete globalThis.navigator;
|
|
}
|
|
});
|
|
|
|
test('metadata prompt selectors put the most likely prompt node first', () => {
|
|
const currentWorkflow = {
|
|
nodes: [
|
|
{
|
|
id: 90,
|
|
type: 'PrimitiveStringMultiline',
|
|
widgets_values: ['generic text field'],
|
|
},
|
|
clipTextNode(371, 'actual prompt', 11),
|
|
{
|
|
id: 3,
|
|
type: 'KSampler',
|
|
inputs: [{ name: 'positive', type: 'CONDITIONING', link: 11 }],
|
|
},
|
|
],
|
|
};
|
|
const context = createContext(createRoleWorkflow(), currentWorkflow);
|
|
const positiveRow = context.getMetadataPromptTransferRows(context.imageInspectorState)
|
|
.find(row => row.role === 'positive');
|
|
|
|
assert.deepEqual(
|
|
positiveRow.targetOptions.map(prompt => prompt.node_id),
|
|
[371, 90],
|
|
);
|
|
});
|
|
|
|
test('metadata prompt role checkboxes stay selectable without an imported workflow', () => {
|
|
const context = createContext(null, createRoleWorkflow('current positive', 'current negative'));
|
|
context.imageInspectorState.metadata = null;
|
|
|
|
const html = context.renderMetadataPromptTransferPanel(context.imageInspectorState);
|
|
|
|
for (const role of ['positive', 'negative']) {
|
|
const checkbox = html.match(new RegExp(`<input type="checkbox" data-image-prompt-role-selection="${role}"[^>]*>`))?.[0] || '';
|
|
assert.match(checkbox, /type="checkbox"/);
|
|
assert.doesNotMatch(checkbox, /disabled/);
|
|
}
|
|
});
|
|
|
|
test('metadata prompt role selection survives importing a source workflow', async () => {
|
|
const context = createContext(null, createRoleWorkflow('current positive', 'current negative'));
|
|
const promptTransfer = context.getMetadataPromptTransferState(context.imageInspectorState);
|
|
promptTransfer.selectedRoles = new Set(['positive']);
|
|
promptTransfer.sourceSelections = { positive: 'old-source' };
|
|
context.fetchJson = async (_endpoint, options) => {
|
|
const payload = JSON.parse(options.body);
|
|
if (payload.source_type === 'workflow') {
|
|
return { workflow: createRoleWorkflow('imported positive', 'imported negative') };
|
|
}
|
|
return { loaded_models: [], total: 0 };
|
|
};
|
|
context.renderImageMetadataResult = () => {};
|
|
|
|
await context.runImageMetadataInspection({
|
|
source_type: 'workflow',
|
|
filename: 'source.json',
|
|
});
|
|
|
|
assert.deepEqual(Array.from(context.imageInspectorState.promptTransfer.selectedRoles), ['positive']);
|
|
assert.deepEqual(context.imageInspectorState.promptTransfer.sourceSelections, {});
|
|
});
|
|
|
|
test('metadata prompt transfer applies only the selected prompt role', async () => {
|
|
const previousWindow = globalThis.window;
|
|
globalThis.window = { dispatchEvent() {} };
|
|
try {
|
|
for (const [selectedRole, replacedNodeIndex, untouchedNodeIndex] of [
|
|
['positive', 0, 1],
|
|
['negative', 1, 0],
|
|
]) {
|
|
const context = createContext(
|
|
createRoleWorkflow('imported positive', 'imported negative'),
|
|
createRoleWorkflow('current positive', 'current negative'),
|
|
);
|
|
const state = context.getMetadataPromptTransferState(context.imageInspectorState);
|
|
state.selectedRoles = new Set([selectedRole]);
|
|
const html = context.renderMetadataPromptTransferPanel(context.imageInspectorState);
|
|
const selectedCheckbox = html.match(new RegExp(`<input type="checkbox" data-image-prompt-role-selection="${selectedRole}"[^>]*>`))?.[0] || '';
|
|
const otherRole = selectedRole === 'positive' ? 'negative' : 'positive';
|
|
const otherCheckbox = html.match(new RegExp(`<input type="checkbox" data-image-prompt-role-selection="${otherRole}"[^>]*>`))?.[0] || '';
|
|
|
|
assert.match(selectedCheckbox, /checked/);
|
|
assert.doesNotMatch(otherCheckbox, /checked/);
|
|
|
|
const result = await context.applyMetadataPromptTransfer();
|
|
const workflow = context.getWorkflow();
|
|
|
|
assert.equal(result.updated, 1);
|
|
assert.equal(workflow.nodes[replacedNodeIndex].widgets_values[0], `imported ${selectedRole}`);
|
|
assert.equal(
|
|
workflow.nodes[untouchedNodeIndex].widgets_values[0],
|
|
`current ${otherRole}`,
|
|
);
|
|
}
|
|
} finally {
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|
|
|
|
test('metadata prompt transfer keeps empty current text fields selectable', () => {
|
|
const context = createContext(createRoleWorkflow(), createRoleWorkflow('', ''));
|
|
const rows = context.getMetadataPromptTransferRows(context.imageInspectorState);
|
|
|
|
assert.equal(rows[0].target.node_id, 1);
|
|
assert.equal(rows[0].target.text, '');
|
|
assert.equal(rows[1].target.node_id, 2);
|
|
assert.equal(rows[1].target.text, '');
|
|
});
|
|
|
|
test('metadata prompt transfer avoids selecting one unknown target for both roles', () => {
|
|
const source = {
|
|
nodes: [
|
|
{ id: 1, type: 'PrimitiveStringMultiline', widgets_values: ['first'] },
|
|
{ id: 2, type: 'PrimitiveStringMultiline', widgets_values: ['second'] },
|
|
],
|
|
};
|
|
const current = {
|
|
nodes: [
|
|
{ id: 10, type: 'PrimitiveStringMultiline', widgets_values: ['target one'] },
|
|
{ id: 11, type: 'PrimitiveStringMultiline', widgets_values: ['target two'] },
|
|
],
|
|
};
|
|
const context = createContext(source, current);
|
|
const rows = context.getMetadataPromptTransferRows(context.imageInspectorState);
|
|
|
|
assert.notEqual(rows[0].target.id, rows[1].target.id);
|
|
});
|
|
|
|
test('metadata prompt transfer replaces both selected prompt fields in the current workflow', async () => {
|
|
const context = createContext(
|
|
createRoleWorkflow('imported positive', 'imported negative'),
|
|
createRoleWorkflow('current positive', 'current negative'),
|
|
);
|
|
const previousWindow = globalThis.window;
|
|
globalThis.window = { dispatchEvent() {} };
|
|
|
|
try {
|
|
const result = await context.applyMetadataPromptTransfer();
|
|
const workflow = context.getWorkflow();
|
|
|
|
assert.equal(result.updated, 2);
|
|
assert.equal(workflow.nodes[0].widgets_values[0], 'imported positive');
|
|
assert.equal(workflow.nodes[1].widgets_values[0], 'imported negative');
|
|
assert.deepEqual(context.notifications, [{
|
|
message: 'Replaced 2 prompt fields in the current workflow.',
|
|
type: 'success',
|
|
}]);
|
|
} finally {
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|