73 lines
6.6 KiB
JavaScript
73 lines
6.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import vm from 'node:vm';
|
|
import { materialPromptText, loadMaterialPrompts } from '../web/modules/material_prompt_data.js';
|
|
import { composePromptPlan, joinPromptText, categorizePromptSnippet, smartSortPromptBlocks, assemblePromptBlocks, planToWorkbenchDraft, workbenchDraftToSavedPlan } from '../web/modules/prompt_composition.js';
|
|
import { translate } from '../web/modules/locales.js';
|
|
|
|
assert.deepEqual(materialPromptText({ data: { kind: 'prompt_text', note: { promptEn: ' exact ', promptZh: 'translation' } } }), { positive: ' exact ', negative: '' });
|
|
assert.deepEqual(materialPromptText({ data: { kind: 'prompt_plan', plan: { parts: [{ positive: 'base', negative: 'bad' }, { positive: 'skip', enabled: false }], positive: 'scene' } } }), { positive: 'base\nscene', negative: 'bad' });
|
|
assert.deepEqual(materialPromptText({ data: { kind: 'image_node_selection' }, prompt_groups: { positive: ['yes'], negative: ['no'] }, node_blocks: [{ type: 'CheckpointLoaderSimple', widgets_values: ['not-prompt'] }] }), { positive: 'yes', negative: 'no' });
|
|
assert.deepEqual(materialPromptText({ data: {}, node_blocks: [{ widgets_values: ['unknown role'] }] }), { positive: '', negative: '' });
|
|
let lastUrl;
|
|
globalThis.fetch = async url => { lastUrl = url; return { ok: true, json: async () => ({ status: 'success', data: { kind: 'prompt_note_bundle', note: { promptEn: 'real detail' } } }) }; };
|
|
assert.equal((await loadMaterialPrompts('note.json')).positive, 'real detail'); assert.ok(lastUrl.includes('include_workflow=0'));
|
|
|
|
class Element {
|
|
constructor(tag) { this.tagName = tag; this.children = []; this.style = {}; this.value = ''; this.attrs = {}; this.className = ''; this.classList = { add() {}, remove() {}, toggle() {} }; this.dataset = {}; }
|
|
appendChild(child) { child.parent = this; this.children.push(child); return child; }
|
|
append(...children) { children.forEach(c => this.appendChild(c)); }
|
|
replaceChildren() { this.children.forEach(c => c.parent = null); this.children = []; }
|
|
remove() { if (this.parent) this.parent.children = this.parent.children.filter(c => c !== this); this.parent = null; }
|
|
setAttribute(k, v) { this.attrs[k] = v; }
|
|
addEventListener(key, fn) { (this.listeners ||= {})[key] = fn; }
|
|
focus() {}
|
|
get isConnected() { return this.tagName === 'body' || !!this.parent?.isConnected; }
|
|
}
|
|
const document = { body: new Element('body'), createElement: tag => new Element(tag), addEventListener: (key, fn) => events[key] = fn };
|
|
const text = (parent, tag, value, cls = '') => { const el = document.createElement(tag); el.textContent = value; el.className = cls; parent.appendChild(el); return el; };
|
|
const all = parent => [parent, ...parent.children.flatMap(all)];
|
|
const events = {};
|
|
let sent;
|
|
const context = vm.createContext({ console, document, app: {}, translate, t: translate, text, composePromptPlan, joinPromptText,
|
|
categorizePromptSnippet, smartSortPromptBlocks, assemblePromptBlocks, planToWorkbenchDraft, workbenchDraftToSavedPlan,
|
|
window: { addEventListener: (key, fn) => events[key] = fn, removeEventListener: (key, fn) => { if (events[key] === fn) delete events[key]; } },
|
|
selectedMaterialNode: () => null, promptWidgetTargets: () => [], bindMaterialDrag() {},
|
|
anomalousConfirm: async () => true, anomalousAlert: async msg => { throw new Error(msg); }, showMaterialSaved() {},
|
|
jsonResponse: response => response.json(), loadMaterialPrompts: async () => ({ positive: 'imported', negative: '' }),
|
|
URL, URLSearchParams, AbortController, setTimeout, clearTimeout,
|
|
fetch: async (url, options) => {
|
|
sent = [url, options];
|
|
return { ok: true, json: async () => url.includes('save_prompt_plan') ? { status: 'success' } : { status: 'success', materials: [{ filename: 'p.json', name: 'Real prompt' }], total: 49, page: 1, pages: 2 } };
|
|
},
|
|
});
|
|
async function moduleFor(file, extra) {
|
|
const source = fs.readFileSync(`web/modules/${file}`, 'utf8').replace(/^import[\s\S]*?;\s*/gm, '');
|
|
const mod = new vm.SourceTextModule(source + `\nexport { ${extra} };`, {
|
|
context,
|
|
identifier: `file:///${process.cwd().replace(/\\/g, '/')}/web/modules/${file}`,
|
|
initializeImportMeta(meta, module) { meta.url = module.identifier; }
|
|
}); await mod.link(() => { throw new Error('unexpected import'); }); await mod.evaluate(); return mod.namespace;
|
|
}
|
|
const recipes = await moduleFor('ui_recipes.js', 'recipeMatchesFilter, updateRecipeFilterControls, getRecipeReadiness');
|
|
assert.equal(recipes.recipeMatchesFilter({ name: 'cat', workflow_scope: 'partial', tags: ['A'] }, 'cat', new Set(['a']), 'complete'), false);
|
|
assert.equal(recipes.recipeMatchesFilter({ name: 'cat', workflow_scope: 'partial', tags: ['A'] }, 'cat', new Set(['a']), 'partial'), true);
|
|
assert.equal(recipes.recipeMatchesFilter({ name: 'old' }, '', new Set(), 'complete'), true);
|
|
const recipeOwner = { recipeTagSelect: new Element('select'), recipeSelectedTags: new Set(['B']) };
|
|
recipes.updateRecipeFilterControls(recipeOwner, [{ data: { tags: ['A', 'B'] } }]);
|
|
assert.equal(recipeOwner.recipeTagSelect.children.length, 3); assert.equal(recipeOwner.recipeTagSelect.value, 'B');
|
|
assert.equal(recipes.getRecipeReadiness({ params: { model_references: [{ identity: { status: 'verified' }, currentAvailability: 'missing' }] } }).status, 'missing');
|
|
assert.equal(recipes.getRecipeReadiness({ params: { model_references: [{ identity: { status: 'verified' } }] } }).status, 'warning');
|
|
|
|
const drags = await moduleFor('material_drag.js', '');
|
|
const graph = { getNodeOnPos: () => null };
|
|
const surface = new Element('canvas'); surface.getBoundingClientRect = () => ({ left: 0, top: 0, right: 100, bottom: 100 });
|
|
context.app.graph = graph; context.app.canvas = { graph, canvas: surface, convertEventToCanvasOffset: () => [50, 50] };
|
|
const source = new Element('article'); let drops = 0;
|
|
drags.bindMaterialDrag(source, {}, { payload: () => ({}), dropOnCanvas: () => { drops++; } });
|
|
const event = target => ({ target, clientX: 50, clientY: 50, dataTransfer: { setData() {} }, preventDefault() {}, stopPropagation() {}, stopImmediatePropagation() {} });
|
|
source.listeners.dragstart(event(source)); await events.drop(event(new Element('button'))); assert.equal(drops, 0);
|
|
source.listeners.dragstart(event(source)); context.app.graph = {}; await events.drop(event(surface)); assert.equal(drops, 0); context.app.graph = graph;
|
|
source.listeners.dragstart(event(source)); await events.drop(event(surface)); assert.equal(drops, 1); assert.equal(events.drop, undefined);
|
|
console.log('studio contracts: recipe scope/tags/readiness, prompt detail shapes, active material drag cleanup passed');
|