fix(nodes): linked text review round for #343
- A legacy frontend Primitive linked to an editor's text or the Pair's negative now runs with its value: the graphToPrompt wrapper and the queue hook fill it in (through virtual reroutes), in new and pre-#343 workflows. - Moving a linked text and releasing it on the editor keeps it on `text`: a link being moved counts as free, as the frontend counts it. - A link dropped on a prompt box (or its link note) fills that box's input, so the Pair's Negative prompt box takes `negative_text` (Classic through getInputOnPos, Nodes 2.0 through the box's slot key). - Only exact STRING links are steered; wildcards keep the frontend choice. - A linked Classic text box gives its row back and gets it back on disconnect; the PromptManagerText note uses the theme colour. - The metadata parsers map positional widget values through the widget table when an editor's widget-bound inputs do not line up with it. - Real-host cases for the legacy Primitive (new and saved workflows, a real Run), the moved link, the Negative box, paste/clone/undo and Split.
This commit is contained in:
@@ -147,6 +147,23 @@ test('paired editor offers positive and negative link sockets and keeps both typ
|
||||
await expect(positive).toHaveValue('Typed positive');await expect(negative).toHaveValue('Typed negative');
|
||||
});
|
||||
|
||||
test('a link dropped on either prompt box, or its note, fills that box (#343 review M1)',async({page})=>{
|
||||
const keys=await page.evaluate(()=>[...document.querySelectorAll('.pm-pair .lg-node-widget [data-slot-key]')].map(key=>key.dataset.slotKey));
|
||||
// Nodes 2.0 resolves a drop through the nearest .lg-node-widget and its slot key: positive box and note, negative box and note.
|
||||
expect(keys).toEqual(['1-in-3','1-in-3','1-in-4','1-in-4']);
|
||||
const at=(name,connecting=true)=>page.evaluate(({name,connecting})=>{
|
||||
fixtureApp.canvas={linkConnector:{state:{connectingTo:connecting?'input':undefined}},ds:{convertOffsetToCanvas:point=>point},canvas:{getBoundingClientRect:()=>({left:0,top:0})}};
|
||||
const box=[...document.querySelectorAll('.pm-pair .lg-node-widget')].find(element=>element.getClientRects().length&&element.textContent.includes(name)).getBoundingClientRect();
|
||||
return node.getInputOnPos([box.left+box.width/2,box.top+box.height/2])?.name??null;
|
||||
},{name,connecting});
|
||||
expect(await at('Negative prompt')).toBe('negative_text');
|
||||
expect(await at('Positive prompt')).toBe('text');
|
||||
expect(await at('Negative prompt',false)).toBeNull();
|
||||
await page.evaluate(()=>{node.inputs.find(input=>input.name==='negative_text').link=4;node.onConnectionsChange();});
|
||||
expect(await at('Negative prompt comes from a link')).toBe('negative_text');
|
||||
expect(await page.evaluate(()=>{fixtureApp.canvas={};return node.getInputOnPos([1,1])??null;})).toBeNull();
|
||||
});
|
||||
|
||||
test('queue hook synchronizes both editors, preserves links and forwards branch execution arguments',async({page})=>{
|
||||
await draft(page);
|
||||
const args=await page.evaluate(async()=>{
|
||||
|
||||
@@ -13,8 +13,9 @@ import { installHostHooks } from './host-fixture.mjs';
|
||||
test.beforeEach(async ({ execution }) => { expect(execution.databasePath).toBeTruthy(); });
|
||||
installHostHooks(test, ['PromptManager', 'PromptManagerText', 'PromptManagerFocused', 'PromptManagerPair', 'PMHistoryInputs']);
|
||||
|
||||
// `primitive`: a core string node the queue-time resolver follows. `preview`: PrimitiveString -> PreviewAny, the
|
||||
// shape of the maintainer's TextGenerate -> PreviewAny chain, whose text is only known when the node runs.
|
||||
// `primitive`: a core string node the queue-time resolver follows. `legacy`: the frontend's virtual Primitive node,
|
||||
// whose value is queued as literal text. `preview`: PrimitiveString -> PreviewAny, the shape of the maintainer's
|
||||
// TextGenerate -> PreviewAny chain, whose text is only known when the node runs.
|
||||
function build(page, { kind, positive, negative, prefix }) {
|
||||
return page.evaluate(async ({ kind, positive, negative, prefix }) => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
@@ -29,6 +30,13 @@ function build(page, { kind, positive, negative, prefix }) {
|
||||
if (negative) set('negative_text', negative.typed);
|
||||
const drive = (name, side, y) => {
|
||||
if (!side.via) return;
|
||||
if (side.via === 'legacy') {
|
||||
// The frontend's own Primitive: a virtual node whose value is applied when the workflow is queued.
|
||||
const primitive = add('PrimitiveNode', 0, y);
|
||||
connect(primitive, 0, prompt, name);
|
||||
primitive.widgets[0].value = side.text;
|
||||
return;
|
||||
}
|
||||
const text = add('PrimitiveString', 0, y);
|
||||
text.widgets.find(widget => widget.name === 'value').value = side.text;
|
||||
let source = text;
|
||||
@@ -121,14 +129,17 @@ async function expectImagesLinked(execution, evidence, viaRuntime) {
|
||||
|
||||
const unique = label => `${label} ${randomUUID()}`;
|
||||
|
||||
for (const kind of ['PromptManager', 'PromptManagerText', 'PromptManagerFocused']) {
|
||||
for (const via of ['primitive', 'preview']) {
|
||||
test(`${kind} saves, counts, records and links images to text linked from ${via === 'preview' ? 'PreviewAny' : 'a primitive'}`, async ({ execution, page }) => {
|
||||
const sources = { primitive: 'PrimitiveString', legacy: 'a legacy Primitive', preview: 'PreviewAny' };
|
||||
for (const kind of ['PromptManager', 'PromptManagerText', 'PromptManagerFocused', 'PromptManagerSplit']) {
|
||||
for (const via of Object.keys(sources)) {
|
||||
test(`${kind} saves, counts, records and links images to text linked from ${sources[via]}`, async ({ execution, page }) => {
|
||||
test.setTimeout(90000);
|
||||
const text = unique(`Linked ${kind} ${via}`);
|
||||
const evidence = await run(execution, page, { kind, positive: { typed: 'Typed under the link', text, via } });
|
||||
const source = evidence.output[evidence.promptNode].inputs.text;
|
||||
expect(Array.isArray(source), 'the frontend queued the link, not the typed text').toBe(true);
|
||||
// A real node is queued as a link; the virtual Primitive as its own value, never the hidden typed text.
|
||||
if (via === 'legacy') expect(source).toBe(text);
|
||||
else expect(Array.isArray(source), 'the frontend queued the link, not the typed text').toBe(true);
|
||||
expect(evidence.context.positive.raw).toBe(text);
|
||||
expect(evidence.context.positive.effective).toBe(text);
|
||||
expect(evidence.usage).toEqual([{ role: 'positive', run_count: 1 }]);
|
||||
@@ -139,16 +150,21 @@ for (const kind of ['PromptManager', 'PromptManagerText', 'PromptManagerFocused'
|
||||
}
|
||||
}
|
||||
|
||||
for (const [label, linked] of [['positive', ['text']], ['negative', ['negative_text']], ['both', ['text', 'negative_text']], ['neither', []]]) {
|
||||
for (const [label, linked, negativeVia] of [['positive', ['text'], 'primitive'], ['negative', ['negative_text'], 'primitive'],
|
||||
['both', ['text', 'negative_text'], 'primitive'], ['neither', [], 'primitive'], ['negative from a legacy Primitive', ['negative_text'], 'legacy']]) {
|
||||
test(`PromptManagerPair with ${label} linked saves, counts and records the pair and links its images`, async ({ execution, page }) => {
|
||||
test.setTimeout(90000);
|
||||
const sides = {
|
||||
text: { typed: unique('Typed positive'), text: unique('Linked positive'), via: linked.includes('text') ? 'preview' : null },
|
||||
negative_text: { typed: unique('Typed negative'), text: unique('Linked negative'), via: linked.includes('negative_text') ? 'primitive' : null },
|
||||
negative_text: { typed: unique('Typed negative'), text: unique('Linked negative'), via: linked.includes('negative_text') ? negativeVia : null },
|
||||
};
|
||||
const evidence = await run(execution, page, { kind: 'PromptManagerPair', positive: sides.text, negative: sides.negative_text });
|
||||
const expected = name => sides[name].via ? sides[name].text : sides[name].typed;
|
||||
for (const name of ['text', 'negative_text']) expect(Array.isArray(evidence.output[evidence.promptNode].inputs[name]), name).toBe(linked.includes(name));
|
||||
for (const name of ['text', 'negative_text']) {
|
||||
const queuedValue = evidence.output[evidence.promptNode].inputs[name];
|
||||
if (sides[name].via === 'legacy') expect(queuedValue, name).toBe(sides[name].text);
|
||||
else expect(Array.isArray(queuedValue), name).toBe(linked.includes(name));
|
||||
}
|
||||
expect(evidence.context.positive.raw).toBe(expected('text'));
|
||||
expect(evidence.context.negative.raw).toBe(expected('negative_text'));
|
||||
expect(evidence.usage).toEqual([{ role: 'positive', run_count: 1 }, { role: 'negative', run_count: 0 }, { role: 'pair', run_count: 1 }]);
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"id": "37b6baef-94f5-49d2-9b42-1ffc70e7da92",
|
||||
"revision": 0,
|
||||
"last_node_id": 4,
|
||||
"last_link_id": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "PromptManagerFocused",
|
||||
"pos": [
|
||||
600,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
550,
|
||||
650
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "prepend_text",
|
||||
"name": "prepend_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "append_text",
|
||||
"name": "append_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "text",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"localized_name": "category",
|
||||
"name": "category",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "category"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "tags",
|
||||
"name": "tags",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "tags"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "search_text",
|
||||
"name": "search_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "search_text"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "CONDITIONING",
|
||||
"name": "CONDITIONING",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"localized_name": "STRING",
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": "Focused fed by a Primitive",
|
||||
"properties": {
|
||||
"Node name for S&R": "PromptManagerFocused",
|
||||
"focusedUI": {
|
||||
"libraryOpen": false,
|
||||
"libraryFilters": {
|
||||
"category": "",
|
||||
"tags": ""
|
||||
},
|
||||
"composition": false,
|
||||
"metadata": false,
|
||||
"preview": false
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"Primitive focused text",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"text": "Primitive focused text",
|
||||
"category": "",
|
||||
"tags": "",
|
||||
"search_text": "",
|
||||
"prepend_text": "",
|
||||
"append_text": "",
|
||||
"prompt_manager_focused_ui": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "PromptManagerPair",
|
||||
"pos": [
|
||||
600,
|
||||
1200
|
||||
],
|
||||
"size": [
|
||||
1100,
|
||||
700
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"localized_name": "clip",
|
||||
"name": "clip",
|
||||
"type": "CLIP",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "prepend_text",
|
||||
"name": "prepend_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "append_text",
|
||||
"name": "append_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "text",
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "negative_text",
|
||||
"name": "negative_text",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "negative_text"
|
||||
},
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"localized_name": "category",
|
||||
"name": "category",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "category"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "tags",
|
||||
"name": "tags",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "tags"
|
||||
},
|
||||
"link": null
|
||||
},
|
||||
{
|
||||
"localized_name": "search_text",
|
||||
"name": "search_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "search_text"
|
||||
},
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"localized_name": "positive",
|
||||
"name": "positive",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"localized_name": "negative",
|
||||
"name": "negative",
|
||||
"type": "CONDITIONING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"localized_name": "positive_text",
|
||||
"name": "positive_text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
},
|
||||
{
|
||||
"localized_name": "negative_text",
|
||||
"name": "negative_text",
|
||||
"type": "STRING",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"title": "Pair negative fed by a Primitive",
|
||||
"properties": {
|
||||
"Node name for S&R": "PromptManagerPair",
|
||||
"pairUI": {
|
||||
"libraryOpen": true,
|
||||
"libraryFilters": {
|
||||
"category": "",
|
||||
"tags": ""
|
||||
},
|
||||
"composition": false,
|
||||
"metadata": false,
|
||||
"preview": false
|
||||
}
|
||||
},
|
||||
"widgets_values": [
|
||||
"Typed pair positive",
|
||||
"Primitive pair negative",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"text": "Typed pair positive",
|
||||
"negative_text": "Primitive pair negative",
|
||||
"category": "",
|
||||
"tags": "",
|
||||
"search_text": "",
|
||||
"prepend_text": "",
|
||||
"append_text": "",
|
||||
"prompt_manager_focused_ui": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "text"
|
||||
},
|
||||
"links": [
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
"Primitive focused text"
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"text": "Primitive focused text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveNode",
|
||||
"pos": [
|
||||
0,
|
||||
1200
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"widget": {
|
||||
"name": "negative_text"
|
||||
},
|
||||
"links": [
|
||||
2
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Run widget replace on values": false
|
||||
},
|
||||
"widgets_values": [
|
||||
"Primitive pair negative"
|
||||
],
|
||||
"widgets_values_named": {
|
||||
"negative_text": "Primitive pair negative"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
3,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
2,
|
||||
4,
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9,
|
||||
"offset": [
|
||||
416,
|
||||
110
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -140,6 +140,137 @@ for (const type of ['PromptManagerFocused', 'PromptManagerSplit', 'PromptManager
|
||||
});
|
||||
}
|
||||
|
||||
async function addEditors(page) {
|
||||
return page.evaluate(async () => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const add = (type, x, y) => { const node = LiteGraph.createNode(type); app.graph.add(node); node.pos = [x, y]; return node; };
|
||||
const focused = add('PromptManagerFocused', 420, 40);
|
||||
focused.setSize([560, 640]);
|
||||
const pair = add('PromptManagerPair', 1020, 40);
|
||||
pair.setSize([1000, 640]);
|
||||
for (const [node, name, value] of [[focused, 'text', 'Typed focused'], [pair, 'text', 'Typed positive'], [pair, 'negative_text', 'Typed negative']]) {
|
||||
const widget = node.widgets.find(item => item.name === name);
|
||||
widget.value = value;
|
||||
if (widget.inputEl) widget.inputEl.value = value;
|
||||
}
|
||||
const sources = [0, 1].map(index => add(index ? 'PrimitiveString' : 'PrimitiveNode', 20, 60 + index * 300).id);
|
||||
app.canvas.ds.scale = .7;
|
||||
// Clear of the floating graph menu (top left) and the PM run bar (top right).
|
||||
app.canvas.ds.offset = [110, 150];
|
||||
app.canvas.setDirty(true, true);
|
||||
return { focused: focused.id, pair: pair.id, primitive: sources[0], string: sources[1] };
|
||||
});
|
||||
}
|
||||
|
||||
/** The prompt a Run sends to the server, through the real app.queuePrompt path. */
|
||||
async function runPrompt(page) {
|
||||
const sent = new Promise(resolve => page.route('**/prompt', async route => {
|
||||
if (route.request().method() !== 'POST') return route.fallback();
|
||||
resolve(route.request().postDataJSON().prompt);
|
||||
return route.fulfill({ json: { prompt_id: 'captured', number: 1, node_errors: {} } });
|
||||
}));
|
||||
await page.evaluate(async () => { const { app } = await import('/scripts/app.js'); await app.queuePrompt(0, 1); });
|
||||
const prompt = await sent;
|
||||
await page.unroute('**/prompt');
|
||||
return prompt;
|
||||
}
|
||||
|
||||
const box = (page, name, nth = 0) => page.getByRole('textbox', { name, exact: true }).nth(nth);
|
||||
|
||||
test('a legacy Primitive dropped on an editor box or socket drives the text it shows as linked (#343 review I1, M1)', async ({ page }) => {
|
||||
test.setTimeout(90000);
|
||||
const ids = await addEditors(page);
|
||||
// A wildcard Primitive aimed at the Main prompt box, a PrimitiveString aimed at the Negative prompt box.
|
||||
await drag(page, await slotPosition(page, ids.primitive, 'out', 0), center(await box(page, 'Main prompt').boundingBox()));
|
||||
await expect.poll(() => linkedInputs(page, ids.focused)).toEqual(['text']);
|
||||
await drag(page, await slotPosition(page, ids.string, 'out', 0), center(await box(page, 'Negative prompt').boundingBox()));
|
||||
await expect.poll(() => linkedInputs(page, ids.pair)).toEqual(['negative_text']);
|
||||
await expect(page.getByText(note('Main prompt'), { exact: true })).toBeVisible();
|
||||
await expect(page.getByText(note('Negative prompt'), { exact: true })).toBeVisible();
|
||||
await page.evaluate(async ({ primitive, string }) => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
app.graph.getNodeById(primitive).widgets[0].value = 'From the legacy Primitive';
|
||||
app.graph.getNodeById(string).widgets.find(widget => widget.name === 'value').value = 'From PrimitiveString';
|
||||
}, ids);
|
||||
const prompt = await runPrompt(page);
|
||||
expect(prompt[String(ids.focused)].inputs.text).toBe('From the legacy Primitive');
|
||||
expect(prompt[String(ids.pair)].inputs).toMatchObject({ text: 'Typed positive', negative_text: [String(ids.string), 0] });
|
||||
// The Primitive is re-read on every run; the typed text stays for disconnect.
|
||||
await page.evaluate(async ({ primitive }) => { const { app } = await import('/scripts/app.js'); app.graph.getNodeById(primitive).widgets[0].value = 'Changed in the Primitive'; }, ids);
|
||||
expect((await queued(page, ids.focused)).text).toBe('Changed in the Primitive');
|
||||
await page.evaluate(async ({ focused }) => { const { app } = await import('/scripts/app.js'); const node = app.graph.getNodeById(focused); node.disconnectInput(node.inputs.findIndex(input => input.name === 'text')); }, ids);
|
||||
expect((await queued(page, ids.focused)).text).toBe('Typed focused');
|
||||
});
|
||||
|
||||
test('a workflow saved before #343 with legacy Primitives on editor text keeps running their values (#343 review I1)', async ({ page }) => {
|
||||
const workflow = JSON.parse(await readFile(new URL('./fixtures/workflow-before-343-primitive.json', import.meta.url), 'utf8'));
|
||||
const result = await page.evaluate(async workflow => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
await app.loadGraphData(structuredClone(workflow));
|
||||
const before = (await app.graphToPrompt()).output;
|
||||
app.graph.getNodeById(4).widgets[0].value = 'Edited after the load';
|
||||
const after = (await app.graphToPrompt()).output;
|
||||
const bound = id => app.graph.getNodeById(id).inputs.filter(input => ['text', 'negative_text'].includes(input.name) && input.widget).length;
|
||||
return { before: [before['1'].inputs.text, before['2'].inputs.text, before['2'].inputs.negative_text], after: after['2'].inputs.negative_text, bound: [bound(1), bound(2)],
|
||||
links: [app.graph.getNodeById(1).inputs.find(input => input.name === 'text').link != null, app.graph.getNodeById(2).inputs.find(input => input.name === 'negative_text').link != null] };
|
||||
}, workflow);
|
||||
expect(result).toEqual({ before: ['Primitive focused text', 'Typed pair positive', 'Primitive pair negative'], after: 'Edited after the load', bound: [0, 0], links: [true, true] });
|
||||
const prompt = await runPrompt(page);
|
||||
expect([prompt['1'].inputs.text, prompt['2'].inputs.negative_text]).toEqual(['Primitive focused text', 'Edited after the load']);
|
||||
});
|
||||
|
||||
test('moving a linked text and dropping it on the editor body keeps it on text, never prepend_text (#343 review I2)', async ({ page }) => {
|
||||
test.setTimeout(90000);
|
||||
const ids = await addEditors(page);
|
||||
await page.evaluate(async ({ focused, pair, string }) => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const source = app.graph.getNodeById(string);
|
||||
for (const id of [focused, pair]) { const node = app.graph.getNodeById(id); source.connect(0, node, node.inputs.findIndex(input => input.name === 'text')); }
|
||||
app.canvas.setDirty(true, true);
|
||||
}, ids);
|
||||
for (const [id, title] of [[ids.focused, 0], [ids.pair, 1]]) {
|
||||
// Grab the link at the text socket and release it over the editor's toolbar, away from any prompt box.
|
||||
const toolbar = page.locator('.pm-editor-toolbar strong').nth(title);
|
||||
await drag(page, await slotPosition(page, id, 'in', 'text'), center(await toolbar.boundingBox()));
|
||||
await expect.poll(() => linkedInputs(page, id)).toEqual(['text']);
|
||||
}
|
||||
});
|
||||
|
||||
test('paste, clone and undo keep the editor text a linked socket (#343 review M7)', async ({ page }) => {
|
||||
const ids = await addEditors(page);
|
||||
const state = await page.evaluate(async ({ pair, string }) => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const node = app.graph.getNodeById(pair);
|
||||
app.graph.getNodeById(string).connect(0, node, node.inputs.findIndex(input => input.name === 'text'));
|
||||
const describe = target => ({ bound: target.inputs.filter(input => ['text', 'negative_text'].includes(input.name) && input.widget).length,
|
||||
linked: target.inputs.find(input => input.name === 'text').link != null });
|
||||
app.canvas.selectItems([node]);
|
||||
app.canvas.copyToClipboard();
|
||||
app.canvas.pasteFromClipboard({ connectInputs: true });
|
||||
const pasted = app.graph._nodes.find(item => item.type === 'PromptManagerPair' && item.id !== pair);
|
||||
const clone = node.clone();
|
||||
app.graph.add(clone);
|
||||
const output = (await app.graphToPrompt()).output;
|
||||
return { pasted: describe(pasted), clone: describe(clone), queued: output[String(pasted.id)].inputs.text, pastedId: pasted.id };
|
||||
}, ids);
|
||||
expect(state).toMatchObject({ pasted: { bound: 0, linked: true }, clone: { bound: 0, linked: false }, queued: [String(ids.string), 0] });
|
||||
// Undo restores the graph through a workflow load: the restored link is still a socket link.
|
||||
const undone = await page.evaluate(async ({ pair }) => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
const tracker = app.extensionManager?.workflow?.activeWorkflow?.changeTracker;
|
||||
if (!tracker) throw new Error('no change tracker on the active workflow');
|
||||
tracker.checkState();
|
||||
const node = app.graph.getNodeById(pair);
|
||||
node.disconnectInput(node.inputs.findIndex(input => input.name === 'text'));
|
||||
tracker.checkState();
|
||||
await tracker.undo();
|
||||
const restored = app.graph.getNodeById(pair);
|
||||
return { bound: restored.inputs.filter(input => ['text', 'negative_text'].includes(input.name) && input.widget).length,
|
||||
text: (await app.graphToPrompt()).output[String(pair)].inputs.text };
|
||||
}, ids);
|
||||
expect(undone).toEqual({ bound: 0, text: [String(ids.string), 0] });
|
||||
});
|
||||
|
||||
test('PromptSearchList text is a core widget socket and takes a dragged STRING link too', async ({ page }) => {
|
||||
const { id, sources } = await page.evaluate(async () => {
|
||||
const { app } = await import('/scripts/app.js');
|
||||
|
||||
+34
@@ -186,6 +186,40 @@
|
||||
"prompt": null,
|
||||
"workflow": "{\"nodes\": [{\"id\": 4, \"type\": \"CheckpointLoaderSimple\", \"mode\": 0, \"inputs\": [], \"widgets_values\": [\"example.safetensors\"]}, {\"id\": 6, \"type\": \"CLIPTextEncode\", \"mode\": 0, \"inputs\": [{\"name\": \"clip\", \"link\": 3}], \"widgets_values\": [\"a tidepool at noon\"]}, {\"id\": 7, \"type\": \"CLIPTextEncode\", \"mode\": 0, \"inputs\": [{\"name\": \"clip\", \"link\": 5}], \"widgets_values\": [\"noisy\"]}, {\"id\": 10, \"type\": \"Reroute\", \"mode\": 0, \"inputs\": [{\"name\": \"\", \"link\": 7}]}, {\"id\": 3, \"type\": \"KSampler\", \"mode\": 0, \"inputs\": [{\"name\": \"model\", \"link\": 1}, {\"name\": \"positive\", \"link\": 8}, {\"name\": \"negative\", \"link\": 4}, {\"name\": \"latent_image\", \"link\": 2}], \"widgets_values\": [314, \"randomize\", 25, 6.5, \"dpmpp_2m\", \"karras\", 1]}, {\"id\": 11, \"type\": \"CLIPTextEncode\", \"mode\": 2, \"inputs\": [], \"widgets_values\": [\"muted node text\"]}], \"links\": [[1, 4, 0, 3, 0, \"MODEL\"], [3, 4, 1, 6, 0, \"CLIP\"], [5, 4, 1, 7, 0, \"CLIP\"], [7, 6, 0, 10, 0, \"CONDITIONING\"], [8, 10, 0, 3, 1, \"CONDITIONING\"], [4, 7, 0, 3, 2, \"CONDITIONING\"], {\"id\": 2, \"origin_id\": 99, \"origin_slot\": 0, \"target_id\": 3, \"target_slot\": 3, \"type\": \"LATENT\"}]}"
|
||||
},
|
||||
{
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"cfgScale": 5,
|
||||
"checkpoint": "example.safetensors",
|
||||
"negativePrompt": "plain negative",
|
||||
"positivePrompt": "a detached positive",
|
||||
"sampler": "euler",
|
||||
"seed": 7,
|
||||
"steps": 12
|
||||
}
|
||||
},
|
||||
"name": "case:workflow-only-detached-editor-text",
|
||||
"prompt": null,
|
||||
"workflow": "{\"nodes\": [{\"id\": 4, \"type\": \"CheckpointLoaderSimple\", \"mode\": 0, \"inputs\": [], \"widgets_values\": [\"example.safetensors\"]}, {\"id\": 6, \"type\": \"PromptManagerFocused\", \"mode\": 0, \"inputs\": [{\"name\": \"clip\", \"link\": 3}, {\"name\": \"prepend_text\", \"link\": null}, {\"name\": \"append_text\", \"link\": null}, {\"name\": \"text\", \"link\": null}, {\"name\": \"category\", \"widget\": {\"name\": \"category\"}, \"link\": null}, {\"name\": \"tags\", \"widget\": {\"name\": \"tags\"}, \"link\": null}, {\"name\": \"search_text\", \"widget\": {\"name\": \"search_text\"}, \"link\": null}], \"widgets_values\": [\"a detached positive\", \"a category\", \"a tag\", \"\", \"\", \"\"]}, {\"id\": 7, \"type\": \"CLIPTextEncode\", \"mode\": 0, \"inputs\": [{\"name\": \"clip\", \"link\": 5}], \"widgets_values\": [\"plain negative\"]}, {\"id\": 3, \"type\": \"KSampler\", \"mode\": 0, \"inputs\": [{\"name\": \"model\", \"link\": 1}, {\"name\": \"positive\", \"link\": 8}, {\"name\": \"negative\", \"link\": 4}], \"widgets_values\": [7, \"fixed\", 12, 5, \"euler\", \"normal\", 1]}], \"links\": [[1, 4, 0, 3, 0, \"MODEL\"], [3, 4, 1, 6, 0, \"CLIP\"], [5, 4, 1, 7, 0, \"CLIP\"], [8, 6, 0, 3, 1, \"CONDITIONING\"], [4, 7, 0, 3, 2, \"CONDITIONING\"]]}"
|
||||
},
|
||||
{
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"cfgScale": 5,
|
||||
"checkpoint": "example.safetensors",
|
||||
"negativePrompt": "a detached negative",
|
||||
"positivePrompt": "a detached positive",
|
||||
"sampler": "euler",
|
||||
"seed": 7,
|
||||
"steps": 12
|
||||
}
|
||||
},
|
||||
"name": "case:workflow-only-detached-pair-text",
|
||||
"prompt": null,
|
||||
"workflow": "{\"nodes\": [{\"id\": 4, \"type\": \"CheckpointLoaderSimple\", \"mode\": 0, \"inputs\": [], \"widgets_values\": [\"example.safetensors\"]}, {\"id\": 6, \"type\": \"PromptManagerPair\", \"mode\": 0, \"inputs\": [{\"name\": \"clip\", \"link\": 3}, {\"name\": \"prepend_text\", \"link\": null}, {\"name\": \"append_text\", \"link\": null}, {\"name\": \"text\", \"link\": null}, {\"name\": \"negative_text\", \"link\": null}, {\"name\": \"category\", \"widget\": {\"name\": \"category\"}, \"link\": null}, {\"name\": \"tags\", \"widget\": {\"name\": \"tags\"}, \"link\": null}, {\"name\": \"search_text\", \"widget\": {\"name\": \"search_text\"}, \"link\": null}], \"widgets_values\": [\"a detached positive\", \"a detached negative\", \"a category\", \"a tag\", \"\", \"\", \"\"]}, {\"id\": 3, \"type\": \"KSampler\", \"mode\": 0, \"inputs\": [{\"name\": \"model\", \"link\": 1}, {\"name\": \"positive\", \"link\": 8}, {\"name\": \"negative\", \"link\": 4}], \"widgets_values\": [7, \"fixed\", 12, 5, \"euler\", \"normal\", 1]}], \"links\": [[1, 4, 0, 3, 0, \"MODEL\"], [3, 4, 1, 6, 0, \"CLIP\"], [5, 4, 1, 7, 0, \"CLIP\"], [8, 6, 0, 3, 1, \"CONDITIONING\"], [4, 6, 1, 3, 2, \"CONDITIONING\"]]}"
|
||||
},
|
||||
{
|
||||
"expected": {
|
||||
"ok": true,
|
||||
|
||||
@@ -244,6 +244,74 @@ def _workflow_only():
|
||||
}
|
||||
|
||||
|
||||
def _detached_editor_text(pair):
|
||||
"""#343: editor text inputs are sockets, not widget inputs; no named values."""
|
||||
names = ["text"] + (["negative_text"] if pair else [])
|
||||
sockets = [
|
||||
{"name": "clip", "link": 3},
|
||||
{"name": "prepend_text", "link": None},
|
||||
{"name": "append_text", "link": None},
|
||||
]
|
||||
sockets += [{"name": name, "link": None} for name in names]
|
||||
sockets += [
|
||||
{"name": name, "widget": {"name": name}, "link": None}
|
||||
for name in ("category", "tags", "search_text")
|
||||
]
|
||||
editor = {
|
||||
"id": 6,
|
||||
"type": "PromptManagerPair" if pair else "PromptManagerFocused",
|
||||
"mode": 0,
|
||||
"inputs": sockets,
|
||||
"widgets_values": ["a detached positive"]
|
||||
+ (["a detached negative"] if pair else [])
|
||||
+ ["a category", "a tag", "", "", ""],
|
||||
}
|
||||
negative = (
|
||||
[]
|
||||
if pair
|
||||
else [
|
||||
{
|
||||
"id": 7,
|
||||
"type": "CLIPTextEncode",
|
||||
"mode": 0,
|
||||
"inputs": [{"name": "clip", "link": 5}],
|
||||
"widgets_values": ["plain negative"],
|
||||
}
|
||||
]
|
||||
)
|
||||
return {
|
||||
"nodes": [
|
||||
{
|
||||
"id": 4,
|
||||
"type": "CheckpointLoaderSimple",
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"widgets_values": ["example.safetensors"],
|
||||
},
|
||||
editor,
|
||||
*negative,
|
||||
{
|
||||
"id": 3,
|
||||
"type": "KSampler",
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{"name": "model", "link": 1},
|
||||
{"name": "positive", "link": 8},
|
||||
{"name": "negative", "link": 4},
|
||||
],
|
||||
"widgets_values": [7, "fixed", 12, 5, "euler", "normal", 1],
|
||||
},
|
||||
],
|
||||
"links": [
|
||||
[1, 4, 0, 3, 0, "MODEL"],
|
||||
[3, 4, 1, 6, 0, "CLIP"],
|
||||
[5, 4, 1, 7, 0, "CLIP"],
|
||||
[8, 6, 0, 3, 1, "CONDITIONING"],
|
||||
[4, 6 if pair else 7, 1 if pair else 0, 3, 2, "CONDITIONING"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _nested(depth):
|
||||
"""A value nested ``depth`` arrays deep, as a crafted file can hold."""
|
||||
value = ["leaf", 7]
|
||||
@@ -389,6 +457,8 @@ CASES = {
|
||||
}
|
||||
),
|
||||
"workflow-only": case(workflow=_workflow_only()),
|
||||
"workflow-only-detached-editor-text": case(workflow=_detached_editor_text(False)),
|
||||
"workflow-only-detached-pair-text": case(workflow=_detached_editor_text(True)),
|
||||
"overwritten-prompt-manager-text": case(
|
||||
{
|
||||
"4": CHECKPOINT,
|
||||
|
||||
@@ -65,14 +65,55 @@ test('a STRING link dropped on the editor prefers the first free text input over
|
||||
assert.equal(preferredTextSlot(node, 'STRING', ['text','negative_text']), undefined, 'both linked: the frontend chooses');
|
||||
});
|
||||
|
||||
test('only STRING-compatible links are steered to a text input', async () => {
|
||||
test('only exact STRING links are steered; wildcards keep the frontend choice (#343 review M2)', async () => {
|
||||
const {preferredTextSlot} = await source;
|
||||
const node = pairNode();
|
||||
for (const type of ['STRING','STRING,INT','*','']) assert.equal(preferredTextSlot(node, type, ['text'])?.index, 3, type);
|
||||
for (const type of ['CLIP','INT','IMAGE',undefined,null]) assert.equal(preferredTextSlot(node, type, ['text']), undefined, String(type));
|
||||
for (const type of ['STRING','STRING,INT',' STRING ']) assert.equal(preferredTextSlot(node, type, ['text'])?.index, 3, type);
|
||||
for (const type of ['*','','CLIP','INT','IMAGE',undefined,null,0]) assert.equal(preferredTextSlot(node, type, ['text']), undefined, String(type));
|
||||
assert.equal(preferredTextSlot({}, 'STRING', ['text']), undefined);
|
||||
});
|
||||
|
||||
test('a link being moved off a text input leaves it free, as the frontend counts it (#343 review I2)', async () => {
|
||||
const {preferredTextSlot} = await source;
|
||||
const links = new Map([[7,{id:7,_dragging:true}],[8,{id:8}]]);
|
||||
const node = Object.assign(pairNode(), {graph:{getLink:id => links.get(id)}});
|
||||
node.inputs[3].link = 7;
|
||||
assert.equal(preferredTextSlot(node, 'STRING', ['text','negative_text']).slot.name, 'text', 'the dragged link does not block text');
|
||||
links.get(7)._dragging = false;
|
||||
assert.equal(preferredTextSlot(node, 'STRING', ['text','negative_text']).slot.name, 'negative_text');
|
||||
node.inputs[4].link = 8;
|
||||
assert.equal(preferredTextSlot(node, 'STRING', ['text','negative_text']), undefined);
|
||||
node.inputs[4].link = 99;
|
||||
assert.equal(preferredTextSlot(node, 'STRING', ['text','negative_text']).slot.name, 'negative_text', 'a link the graph no longer has is free');
|
||||
const legacy = Object.assign(pairNode(), {graph:{links:{5:{id:5,_dragging:true}}}});
|
||||
legacy.inputs[3].link = 5;
|
||||
assert.equal(preferredTextSlot(legacy, 'STRING', ['text']).slot.name, 'text', 'object-keyed link tables too');
|
||||
});
|
||||
|
||||
test('a legacy frontend Primitive feeding a text input supplies its value (#343 review I1)', async () => {
|
||||
const {primitiveTextValue} = await source;
|
||||
const nodes = new Map();
|
||||
const links = new Map();
|
||||
const graph = {getNodeById:id => nodes.get(id), getLink:id => links.get(id)};
|
||||
const add = (id, node) => { nodes.set(id, {id, graph, ...node}); return nodes.get(id); };
|
||||
const target = add(1, {inputs:[{name:'text',link:10},{name:'negative_text',link:11},{name:'prepend_text',link:12},{name:'category',link:null}]});
|
||||
add(2, {type:'PrimitiveNode', isVirtualNode:true, widgets:[{value:'From the primitive'}]});
|
||||
add(3, {type:'Reroute', isVirtualNode:true, inputs:[{link:13}]});
|
||||
add(4, {type:'PrimitiveNode', isVirtualNode:true, widgets:[{value:42}]});
|
||||
add(5, {type:'PrimitiveStringMultiline', widgets:[{value:'a real node'}]});
|
||||
links.set(10, {origin_id:2}); links.set(11, {origin_id:3}); links.set(13, {origin_id:4}); links.set(12, {origin_id:5});
|
||||
assert.equal(primitiveTextValue(target, 'text'), 'From the primitive');
|
||||
assert.equal(primitiveTextValue(target, 'negative_text'), '42', 'through a virtual reroute');
|
||||
assert.equal(primitiveTextValue(target, 'prepend_text'), undefined, 'a real node is serialized as a link');
|
||||
assert.equal(primitiveTextValue(target, 'category'), undefined, 'not linked');
|
||||
assert.equal(primitiveTextValue(target, 'missing'), undefined);
|
||||
nodes.get(2).widgets = [];
|
||||
assert.equal(primitiveTextValue(target, 'text'), undefined, 'a primitive without its widget yet');
|
||||
links.set(13, {origin_id:3});
|
||||
assert.equal(primitiveTextValue(target, 'negative_text'), undefined, 'a reroute cycle ends');
|
||||
assert.equal(primitiveTextValue({inputs:[{name:'text',link:1}]}, 'text'), undefined, 'no graph');
|
||||
});
|
||||
|
||||
test('installing the preference wraps findInputByType and restores nothing it did not own', async () => {
|
||||
const {preferTextInputs} = await source;
|
||||
const calls = [];
|
||||
@@ -89,3 +130,32 @@ test('installing the preference wraps findInputByType and restores nothing it di
|
||||
preferTextInputs(bare, ['text']);
|
||||
assert.equal(bare.findInputByType, undefined, 'an older frontend without findInputByType is left unchanged');
|
||||
});
|
||||
|
||||
test('a drop point over a visible prompt box or its note picks that box input (#343 review M1)', async () => {
|
||||
const {textInputAtPoint} = await source;
|
||||
const box = (left, top, width, height) => ({getBoundingClientRect:() => ({left, top, width, height, right:left + width, bottom:top + height})});
|
||||
const node = pairNode();
|
||||
const targets = [{name:'text',element:box(0,0,100,100)},{name:'negative_text',element:box(120,0,100,100)},{name:'text',element:box(0,0,0,0)},{name:'missing',element:box(300,0,50,50)}];
|
||||
assert.equal(textInputAtPoint(node, [150,50], targets).name, 'negative_text');
|
||||
assert.equal(textInputAtPoint(node, [10,10], targets).name, 'text');
|
||||
assert.equal(textInputAtPoint(node, [110,50], targets), undefined, 'between the boxes');
|
||||
assert.equal(textInputAtPoint(node, [320,10], targets), undefined, 'a target whose input the node lacks');
|
||||
assert.equal(textInputAtPoint(node, [0,0], [{name:'text',element:box(0,0,0,0)}]), undefined, 'a hidden target');
|
||||
});
|
||||
|
||||
test('a linked Classic text box gives its row back and gets its size back on disconnect (#343 review M5)', async () => {
|
||||
const {collapseLinkedWidget} = await source;
|
||||
const own = () => 120;
|
||||
const widget = {options:{getMinHeight:own}};
|
||||
collapseLinkedWidget(widget, true);
|
||||
assert.equal(widget.options.getMinHeight(), 0);
|
||||
assert.equal(widget.options.getMaxHeight(), 0);
|
||||
collapseLinkedWidget(widget, true);
|
||||
collapseLinkedWidget(widget, false);
|
||||
assert.equal(widget.options.getMinHeight, own);
|
||||
assert.equal('getMaxHeight' in widget.options, false, 'an option it did not have is removed again');
|
||||
collapseLinkedWidget(widget, false);
|
||||
assert.equal(widget.options.getMinHeight, own);
|
||||
collapseLinkedWidget(undefined, true);
|
||||
collapseLinkedWidget({}, true);
|
||||
});
|
||||
|
||||
@@ -82,6 +82,17 @@ class TracesPrompts(unittest.TestCase):
|
||||
self.assertEqual(result["positivePrompt"], "the real positive")
|
||||
self.assertEqual(result["negativePrompt"], "the real negative")
|
||||
|
||||
def test_editor_text_inputs_without_widget_bindings_keep_their_positions(self):
|
||||
# #343: Focused/Split/Pair text inputs are plain sockets. The widget-bound
|
||||
# inputs that remain (category, tags, search_text) must not shift the
|
||||
# positional widget values onto the wrong names.
|
||||
result = run("workflow-only-detached-editor-text")
|
||||
self.assertEqual(result["positivePrompt"], "a detached positive")
|
||||
self.assertEqual(result["negativePrompt"], "plain negative")
|
||||
result = run("workflow-only-detached-pair-text")
|
||||
self.assertEqual(result["positivePrompt"], "a detached positive")
|
||||
self.assertEqual(result["negativePrompt"], "a detached negative")
|
||||
|
||||
|
||||
class ChoosesTheSampler(unittest.TestCase):
|
||||
def test_first_sampler_follows_javascript_key_order(self):
|
||||
|
||||
Vendored
+5
-4
@@ -23,21 +23,22 @@ test('execution script discovers only the new matrix and preserves all 78 origin
|
||||
env: { ...process.env, PM_COMFY_ROOT: '/unused-pinned-checkout' } });
|
||||
const stock = list('tools/vendor/comfyui.config.mjs');
|
||||
// + local Inspect host case (inspect-sender.spec.mjs, 2 projects), + the S10 renderer case (prompt-bridge.spec.mjs, 2 projects)
|
||||
// + #343 linked text: five node types, PromptSearchList and the pre-#343 workflow (text-link.spec.mjs, 2 projects)
|
||||
assert.match(stock, /Total: 98 tests in 9 files/);
|
||||
// + #343 linked text: five node types, PromptSearchList, the pre-#343 workflows, legacy Primitive, moved link,
|
||||
// paste/clone/undo (text-link.spec.mjs, 2 projects)
|
||||
assert.match(stock, /Total: 106 tests in 9 files/);
|
||||
assert.doesNotMatch(stock, /execution-history/);
|
||||
assert.doesNotMatch(stock, /execution-outputs/);
|
||||
assert.doesNotMatch(stock, /library-location/); // it starts its own fresh host (#249 S6b)
|
||||
const executions = list('tools/vendor/playwright.execution.config.mjs');
|
||||
assert.match(executions, /\[classic-start\]/);
|
||||
assert.match(executions, /\[nodes2-start\]/);
|
||||
assert.match(executions, /Total: 68 tests in 4 files/); // + the fresh-host library location case, + #343 linked text
|
||||
assert.match(executions, /Total: 82 tests in 4 files/); // + the fresh-host library location case, + #343 linked text
|
||||
for (const project of ['classic-start', 'nodes2-start']) {
|
||||
const cases = executions.split('\n').filter(line => line.includes(`[${project}]`));
|
||||
assert.equal(cases.filter(line => line.includes('execution-history.spec.mjs')).length, 15);
|
||||
assert.equal(cases.filter(line => line.includes('execution-outputs.spec.mjs')).length, 8);
|
||||
assert.equal(cases.filter(line => line.includes('library-location.spec.mjs')).length, 1);
|
||||
assert.equal(cases.filter(line => line.includes('execution-text-link.spec.mjs')).length, 10);
|
||||
assert.equal(cases.filter(line => line.includes('execution-text-link.spec.mjs')).length, 17);
|
||||
}
|
||||
assert.doesNotMatch(executions, /prompt-manager.spec/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user