Skip to content

Commit bd7c561

Browse files
refactor(core-components): extract shared Prompt Template helpers
Centralizes the bootstrap/locator/modal-fill code that was duplicated across the four `prompt-template-*` specs into `tests/helpers/ui/prompt-template.ts`. The helper exposes `addPromptComponent`, `dynamicHandlesLocator`, `errorToastLocator`, `setUseDoubleBrackets`, and `fillPromptTemplate`. The latter unifies four previous variants behind `{ mode, waitForHide }` options so error-path tests can keep submitting without waiting for the textarea to hide (the modal stays in edit mode on validation failure). No change to test behavior: same `test()` count per file (6/5/4/6), same assertions, same tags.
1 parent 1214a08 commit bd7c561

5 files changed

Lines changed: 214 additions & 352 deletions
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { type Locator, type Page, expect } from "@playwright/test";
2+
import { awaitBootstrapTest } from "../other/await-bootstrap-test";
3+
import { adjustScreenView } from "./adjust-screen-view";
4+
5+
// Shared testids and selectors for the Prompt Template component, sourced
6+
// from live UI inspection and the upstream Langflow frontend source:
7+
// add button: "add-component-button-prompt-template"
8+
// node title: "title-Prompt Template"
9+
// toggle (InspectionPanel): "toggle_bool_use_double_brackets"
10+
// f-string modal open: "button_open_prompt_modal"
11+
// f-string textarea: "modal-promptarea_prompt_template"
12+
// mustache modal open: "button_open_mustache_prompt_modal"
13+
// mustache textarea: "modal-mustachepromptarea_mustache_template"
14+
// modal save btn: "genericModalBtnSave"
15+
// modal preview: "edit-prompt-sanitized" (shared between modes)
16+
// output handle: "handle-prompt template-shownode-prompt-right"
17+
// dynamic handles: "handle-prompt template-shownode-{varname}-left"
18+
// error toast: CSS class ".error-build-message" (no data-testid;
19+
// sourced from src/frontend/src/alerts/error/index.tsx)
20+
21+
const FSTRING_OPEN_BUTTON = "button_open_prompt_modal";
22+
const FSTRING_TEXTAREA = "modal-promptarea_prompt_template";
23+
const MUSTACHE_OPEN_BUTTON = "button_open_mustache_prompt_modal";
24+
const MUSTACHE_TEXTAREA = "modal-mustachepromptarea_mustache_template";
25+
26+
/**
27+
* Bootstraps a fresh blank flow, drops a Prompt Template node onto it via the
28+
* sidebar search/add path, and waits for exactly one node to render on the
29+
* canvas.
30+
*/
31+
export async function addPromptComponent(page: Page): Promise<void> {
32+
await awaitBootstrapTest(page);
33+
await expect(page.getByTestId("blank-flow")).toBeAttached({ timeout: 30000 });
34+
await page.getByTestId("blank-flow").click();
35+
36+
await page.getByTestId("sidebar-search-input").click();
37+
await page.getByTestId("sidebar-search-input").fill("prompt");
38+
await expect(
39+
page.getByTestId("add-component-button-prompt-template"),
40+
).toBeAttached({ timeout: 30000 });
41+
await page.getByTestId("add-component-button-prompt-template").click();
42+
43+
await adjustScreenView(page);
44+
await expect(page.locator(".react-flow__node")).toHaveCount(1, {
45+
timeout: 10000,
46+
});
47+
}
48+
49+
/**
50+
* Locator for the dynamic (left-side) input handles created from variable
51+
* placeholders on the Prompt Template node. The output `-right` handle is
52+
* excluded by the suffix filter so counts reflect dynamic-handle-only deltas.
53+
*/
54+
export function dynamicHandlesLocator(page: Page): Locator {
55+
return page.locator(
56+
'[data-testid^="handle-prompt template-shownode-"][data-testid$="-left"]',
57+
);
58+
}
59+
60+
/**
61+
* Locator for the error toast rendered by `ErrorAlert` when the prompt modal's
62+
* `onError` callback fires (no `data-testid` is exposed by the upstream alert
63+
* component, so the CSS class is the stable anchor).
64+
*/
65+
export function errorToastLocator(page: Page): Locator {
66+
return page.locator(".error-build-message");
67+
}
68+
69+
/**
70+
* Flip the `use_double_brackets` toggle in the InspectionPanel and wait for the
71+
* matching modal-open button to mount. With `real_time_refresh=True`, toggling
72+
* the bool causes `update_build_config` to swap `template.type` between PROMPT
73+
* and MUSTACHE_PROMPT, which re-renders the modal-open button under a different
74+
* testid — that re-render is the reliable signal that the switch has landed.
75+
*
76+
* @param enabled `true` enables mustache mode; `false` reverts to f-string.
77+
*/
78+
export async function setUseDoubleBrackets(
79+
page: Page,
80+
enabled: boolean,
81+
): Promise<void> {
82+
await page.getByTestId("toggle_bool_use_double_brackets").click();
83+
const expectedOpenButton = enabled
84+
? MUSTACHE_OPEN_BUTTON
85+
: FSTRING_OPEN_BUTTON;
86+
await expect(page.getByTestId(expectedOpenButton)).toBeVisible({
87+
timeout: 10000,
88+
});
89+
}
90+
91+
export interface FillPromptTemplateOptions {
92+
/** Which modal to drive. Defaults to `"fstring"`. */
93+
mode?: "fstring" | "mustache";
94+
/**
95+
* Whether to wait for the textarea to hide after clicking save. Defaults to
96+
* `true` (success path). Set to `false` when submitting input that the
97+
* backend is expected to reject — the modal stays in edit mode (the
98+
* frontend sets `isEdit=true`) and the textarea remains visible.
99+
*/
100+
waitForHide?: boolean;
101+
}
102+
103+
/**
104+
* Open the active prompt modal (f-string or mustache, depending on
105+
* `opts.mode`), replace its current value with `value`, and click save.
106+
*
107+
* The save round-trip can end in one of two states:
108+
* - success: textarea hides, sanitized preview appears
109+
* - error: `setIsEdit(true)` keeps the textarea visible and a toast
110+
* with class `.error-build-message` is rendered
111+
*
112+
* After a previous successful save, the modal initially shows the sanitized
113+
* preview (read-only) instead of the textarea — this helper clicks the preview
114+
* to re-enter edit mode before filling. With `waitForHide=true` (default),
115+
* waits for the textarea to disappear as the close signal; callers asserting
116+
* on the error path should pass `waitForHide: false` and assert on the
117+
* post-save state themselves.
118+
*/
119+
export async function fillPromptTemplate(
120+
page: Page,
121+
value: string,
122+
opts: FillPromptTemplateOptions = {},
123+
): Promise<void> {
124+
const { mode = "fstring", waitForHide = true } = opts;
125+
const openButtonTestId =
126+
mode === "mustache" ? MUSTACHE_OPEN_BUTTON : FSTRING_OPEN_BUTTON;
127+
const textareaTestId =
128+
mode === "mustache" ? MUSTACHE_TEXTAREA : FSTRING_TEXTAREA;
129+
130+
await page.getByTestId(openButtonTestId).click();
131+
132+
const textarea = page.getByTestId(textareaTestId);
133+
134+
const preview = page.getByTestId("edit-prompt-sanitized");
135+
if (await preview.isVisible({ timeout: 2000 }).catch(() => false)) {
136+
await preview.click();
137+
}
138+
139+
await expect(textarea).toBeVisible({ timeout: 10000 });
140+
await textarea.click();
141+
await textarea.fill(value);
142+
143+
await page.getByTestId("genericModalBtnSave").click();
144+
145+
if (waitForHide) {
146+
// The textarea testid is scoped to the active prompt modal, so its
147+
// disappearance is a reliable signal that the modal closed and the save
148+
// round-trip began.
149+
await expect(textarea).toBeHidden({ timeout: 10000 });
150+
}
151+
}

tests/tests-automations/regression/core-components/prompt-template-component-regression.spec.ts

Lines changed: 13 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,14 @@
1-
import type { Page } from "@playwright/test";
21
import { expect, test } from "../../../fixtures/fixtures";
3-
import { adjustScreenView } from "../../../helpers/ui/adjust-screen-view";
4-
import { awaitBootstrapTest } from "../../../helpers/other/await-bootstrap-test";
2+
import {
3+
addPromptComponent,
4+
dynamicHandlesLocator,
5+
fillPromptTemplate,
6+
} from "../../../helpers/ui/prompt-template";
57

68
// Run serially to avoid 500 errors from concurrent POST /api/v1/flows/
79
// when several workers create a blank flow at the same time.
810
test.describe.configure({ mode: "serial" });
911

10-
// Verified testids from live UI inspection:
11-
// add button: "add-component-button-prompt-template"
12-
// node title: "title-Prompt Template"
13-
// modal open btn: "button_open_prompt_modal"
14-
// modal textarea: "modal-promptarea_prompt_template" (unique to the prompt modal — use as anchor)
15-
// modal save btn: "genericModalBtnSave"
16-
// modal preview: "edit-prompt-sanitized" (shown after save; click to re-edit)
17-
// output handle: "handle-prompt template-shownode-prompt-right"
18-
// dynamic handles: "handle-prompt template-shownode-{varname}-left"
19-
20-
// Locator matching only the dynamic (left-side) input handles created from
21-
// {variable} placeholders. The output `-right` handle is excluded by the suffix
22-
// filter so counts reflect dynamic-handle-only deltas.
23-
const dynamicHandlesLocator = (page: Page) =>
24-
page.locator(
25-
'[data-testid^="handle-prompt template-shownode-"][data-testid$="-left"]',
26-
);
27-
28-
async function addPromptComponent(page: Page) {
29-
await awaitBootstrapTest(page);
30-
await expect(page.getByTestId("blank-flow")).toBeAttached({ timeout: 30000 });
31-
await page.getByTestId("blank-flow").click();
32-
33-
await page.getByTestId("sidebar-search-input").click();
34-
await page.getByTestId("sidebar-search-input").fill("prompt");
35-
await expect(
36-
page.getByTestId("add-component-button-prompt-template"),
37-
).toBeAttached({ timeout: 30000 });
38-
await page.getByTestId("add-component-button-prompt-template").click();
39-
40-
await adjustScreenView(page);
41-
await expect(page.locator(".react-flow__node")).toHaveCount(1, {
42-
timeout: 10000,
43-
});
44-
}
45-
46-
// Open the prompt modal and replace its current value with `value`.
47-
// Handles the post-save preview state by clicking it to re-enter edit mode.
48-
// The function returns after the save dialog closes; downstream assertions
49-
// must wait on their specific expected handle state (auto-retry via expect()),
50-
// because the canvas re-render is asynchronous to the modal close.
51-
async function setPromptTemplate(page: Page, value: string) {
52-
await page.getByTestId("button_open_prompt_modal").click();
53-
54-
const textarea = page.getByTestId("modal-promptarea_prompt_template");
55-
56-
// After a previous save, the modal initially shows the sanitized preview
57-
// (read-only) instead of the textarea. Clicking the preview re-enters edit
58-
// mode and mounts the textarea.
59-
const preview = page.getByTestId("edit-prompt-sanitized");
60-
if (await preview.isVisible({ timeout: 2000 }).catch(() => false)) {
61-
await preview.click();
62-
}
63-
64-
await expect(textarea).toBeVisible({ timeout: 10000 });
65-
await textarea.click();
66-
await page.keyboard.press("Control+a");
67-
await textarea.fill(value);
68-
69-
await page.getByTestId("genericModalBtnSave").click();
70-
// The textarea testid is scoped to the prompt modal, so its disappearance
71-
// is a reliable signal that the modal closed and the save round-trip began.
72-
await expect(textarea).toBeHidden({ timeout: 10000 });
73-
}
74-
7512
test(
7613
"Prompt Template component — renders on canvas with output handle",
7714
{ tag: ["@stable", "@release", "@regression", "@components"] },
@@ -112,7 +49,7 @@ test(
11249
await test.step(
11350
"Save template with two {variable} placeholders",
11451
async () => {
115-
await setPromptTemplate(page, "Hello {name}, your job is {profession}.");
52+
await fillPromptTemplate(page, "Hello {name}, your job is {profession}.");
11653
},
11754
);
11855

@@ -152,7 +89,7 @@ test(
15289
await test.step(
15390
"Save template `Hello {name}!` — expect 1 dynamic handle for {name}",
15491
async () => {
155-
await setPromptTemplate(page, "Hello {name}!");
92+
await fillPromptTemplate(page, "Hello {name}!");
15693
await expect(nameHandle).toBeVisible({ timeout: 10000 });
15794
await expect(dynamicHandlesLocator(page)).toHaveCount(1);
15895
},
@@ -161,7 +98,7 @@ test(
16198
await test.step(
16299
"Save template without variables — expect 0 dynamic handles",
163100
async () => {
164-
await setPromptTemplate(page, "Hello world!");
101+
await fillPromptTemplate(page, "Hello world!");
165102
await expect(nameHandle).toHaveCount(0, { timeout: 10000 });
166103
await expect(dynamicHandlesLocator(page)).toHaveCount(0);
167104
},
@@ -180,7 +117,7 @@ test(
180117
await test.step(
181118
"Save template `Hello {name}, you are {role}.` — both handles render",
182119
async () => {
183-
await setPromptTemplate(page, "Hello {name}, you are {role}.");
120+
await fillPromptTemplate(page, "Hello {name}, you are {role}.");
184121
await expect(
185122
page.getByTestId("handle-prompt template-shownode-name-left"),
186123
).toBeVisible({ timeout: 10000 });
@@ -193,7 +130,7 @@ test(
193130
await test.step(
194131
"Replace {role} with {title} — old handle is gone, new one appears, {name} stays",
195132
async () => {
196-
await setPromptTemplate(page, "Hello {name}, you are {title}.");
133+
await fillPromptTemplate(page, "Hello {name}, you are {title}.");
197134
await expect(
198135
page.getByTestId("handle-prompt template-shownode-name-left"),
199136
).toBeVisible({ timeout: 10000 });
@@ -219,7 +156,7 @@ test(
219156
await test.step(
220157
"Save template with 3 variables — expect 3 dynamic handles",
221158
async () => {
222-
await setPromptTemplate(page, "{a} and {b} and {c}");
159+
await fillPromptTemplate(page, "{a} and {b} and {c}");
223160
await expect(dynamicHandlesLocator(page)).toHaveCount(3, {
224161
timeout: 10000,
225162
});
@@ -229,7 +166,7 @@ test(
229166
await test.step(
230167
"Save plain-text template — all dynamic handles disappear",
231168
async () => {
232-
await setPromptTemplate(page, "No variables here.");
169+
await fillPromptTemplate(page, "No variables here.");
233170
await expect(dynamicHandlesLocator(page)).toHaveCount(0, {
234171
timeout: 10000,
235172
});
@@ -254,7 +191,7 @@ test(
254191
await test.step(
255192
"Save template — the {topic} handle confirms save was applied",
256193
async () => {
257-
await setPromptTemplate(page, expected);
194+
await fillPromptTemplate(page, expected);
258195
await expect(
259196
page.getByTestId("handle-prompt template-shownode-topic-left"),
260197
).toBeVisible({ timeout: 10000 });

0 commit comments

Comments
 (0)