Skip to content

Commit effdd24

Browse files
Merge pull request #173 from oriontech-me/validate/output-modal-copy-button-stable
chore(playground): rewrite output-modal-copy-button and mark @stable
2 parents d65ebcb + 54a3293 commit effdd24

3 files changed

Lines changed: 157 additions & 119 deletions

File tree

QA-CHECKLIST.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,8 @@
458458
- [x] Stop button in Playground → `core-functionality/playground/stop-button-playground.spec.ts`
459459

460460
#### 9.4 Output Modal
461-
- [-] Copy component output → `playground/output-modal-copy-button.spec.ts` (needs rewrite — see issue)
462-
- [-] Copy button in output → `playground/output-modal-copy-button.spec.ts` (needs rewrite — see issue)
461+
- [x] Copy component output → `core-functionality/playground/output-modal-copy-button.spec.ts`
462+
- [x] Copy button in output → `core-functionality/playground/output-modal-copy-button.spec.ts`
463463

464464
#### 9.5 Structured Data Output
465465
- [x] JSON Data output renders as code block → `core-functionality/playground/playground-output-data.spec.ts`
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Output Modal — Copy Button
2+
3+
**Last validated:** Langflow 1.10.x
4+
5+
---
6+
7+
## What this test validates *(required)*
8+
9+
Validates that the **Copy** button inside a component's Output Modal copies the component's text output to the clipboard and gives the user a clear visual confirmation:
10+
11+
1. The "Copied to clipboard" toast appears
12+
2. The button's icon transitions from Copy → Check (success state)
13+
3. The button reverts back to Copy after the success state expires
14+
15+
If this breaks, users have no reliable way to grab the output of a component into the clipboard from the modal — a primary path for sharing or pasting results out of Langflow.
16+
17+
---
18+
19+
## Tags *(required)*
20+
21+
`@stable` `@release` `@workspace` `@playground`
22+
23+
---
24+
25+
## Step by step *(required)*
26+
27+
1. Open Langflow and create a blank flow; capture the flow id from the `POST /api/v1/flows` 201 response
28+
2. Add a **Text Input** component and fill its `textarea_str_input_value` with `"Test content to copy"`
29+
3. Run the component (`button_run_text input`) and wait for the "built successfully" toast
30+
4. Click the first `output-inspection-*` button to open the Component Output modal
31+
5. Click `copy-output-button`
32+
6. Assert "Copied to clipboard" toast is visible
33+
7. Assert the Check icon (`icon-Check`) is visible inside the button
34+
8. Assert the Copy icon (`icon-Copy`) returns within 5s (web-first assertion — no `waitForTimeout`)
35+
36+
`afterEach` navigates to `/` and deletes the captured flow via `DELETE /api/v1/flows/{id}`.
37+
38+
---
39+
40+
## Validation criterion *(required)*
41+
42+
- "Copied to clipboard" toast appears within 5s of clicking the copy button
43+
- Button shows `icon-Check` immediately after the click (success state)
44+
- Button returns to `icon-Copy` within 5s (state revert)
45+
46+
---
47+
48+
## External dependencies *(required)*
49+
50+
- `data-testid="copy-output-button"` — copy button rendered inside the Output Modal
51+
- `data-testid="icon-Check"` and `data-testid="icon-Copy"` — icon components inside the button
52+
- `data-testid="output-inspection-*"` — the inspector entry point that opens the Output Modal
53+
- `data-testid="textarea_str_input_value"` and `button_run_text input` — Text Input component fields
54+
- Backend endpoints: `POST /api/v1/flows` (flow creation), `DELETE /api/v1/flows/{id}` (cleanup)
55+
56+
---
57+
58+
## What this test does not cover *(optional)*
59+
60+
- Copying outputs of other component types (e.g., JSON from API Request) — was previously a separate test that depended on `httpbin.org` and was removed for being flaky and externally dependent
61+
- The clipboard contents themselves — the test asserts the UI confirmation (toast + icon transitions), not the OS clipboard
62+
63+
---
64+
65+
## Preconditions *(optional)*
66+
67+
- Langflow running at `PLAYWRIGHT_BASE_URL`
68+
- No LLM, no external HTTP calls — Text Input runs are local
69+
70+
---
71+
72+
## Notes *(optional)*
73+
74+
- Runs in `serial` mode (only one test, but kept for consistency with sibling playground specs)
75+
- Cleanup is scoped to the flow this test creates (id captured from the 201 response of `POST /api/v1/flows`)

tests/tests-automations/regression/core-functionality/playground/output-modal-copy-button.spec.ts

Lines changed: 80 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,99 @@
11
import { expect, test } from "../../../../fixtures/fixtures";
22
import { awaitBootstrapTest } from "../../../../helpers/other/await-bootstrap-test";
33

4-
test.describe("Output Modal Copy Button", () => {
5-
test(
6-
"user should be able to copy text output from component output modal",
7-
{ tag: ["@release", "@workspace", "@playground"] },
8-
async ({ page }) => {
9-
await awaitBootstrapTest(page);
10-
11-
await page.getByTestId("blank-flow").click();
12-
13-
await page.waitForSelector('[data-testid="sidebar-search-input"]', {
14-
timeout: 30000,
15-
state: "visible",
16-
});
17-
18-
// Add a Text Input component
19-
await page.getByTestId("sidebar-search-input").click();
20-
await page.getByTestId("sidebar-search-input").fill("text input");
21-
22-
await page.waitForSelector('[data-testid="input_outputText Input"]', {
23-
timeout: 10000,
24-
state: "visible",
25-
});
26-
27-
await page
28-
.getByTestId("input_outputText Input")
29-
.hover()
30-
.then(async () => {
31-
await page.getByTestId("add-component-button-text-input").click();
32-
});
33-
34-
await page.waitForTimeout(500);
35-
36-
// Fill in some test text
37-
await page
38-
.getByTestId("textarea_str_input_value")
39-
.fill("Test content to copy");
40-
41-
// Run the component
42-
await page.getByTestId("button_run_text input").click();
43-
44-
await page.waitForSelector("text=built successfully", { timeout: 30000 });
45-
46-
// Open the output modal
47-
await page.locator('[data-testid^="output-inspection-"]').first().click();
48-
49-
await page.waitForSelector("text=Component Output", { timeout: 30000 });
50-
51-
// Verify the copy button exists
52-
const copyButton = page.getByTestId("copy-output-button");
53-
await expect(copyButton).toBeVisible();
54-
55-
// Click the copy button
56-
await copyButton.click();
57-
58-
// Verify the success message appears
59-
await page.waitForSelector("text=Copied to clipboard", {
60-
timeout: 5000,
61-
});
62-
63-
// Verify the check icon appears (button changes state)
64-
await expect(
65-
copyButton.locator('[data-testid="icon-Check"]'),
66-
).toBeVisible();
67-
68-
// Wait for the icon to revert back to copy icon
69-
await page.waitForTimeout(2500);
70-
await expect(
71-
copyButton.locator('[data-testid="icon-Copy"]'),
72-
).toBeVisible();
73-
},
74-
);
4+
test.describe("Output Modal — Copy Button", () => {
5+
test.describe.configure({ mode: "serial" });
6+
7+
let createdFlowId: string | null = null;
8+
9+
test.afterEach(async ({ page }) => {
10+
if (createdFlowId) {
11+
// Navigate to home before deleting to stop background browser requests
12+
// for the current flow; without this, pending polling GETs complete
13+
// after the DELETE and trigger spurious 404 fixture errors.
14+
await page.goto("/");
15+
await page.request.delete(`/api/v1/flows/${createdFlowId}`);
16+
createdFlowId = null;
17+
}
18+
});
7519

7620
test(
77-
"copy button should work with JSON output from API Request component",
78-
{ tag: ["@release", "@workspace", "@playground"] },
21+
"copy button copies Text Input output and toggles Check icon",
22+
{ tag: ["@stable", "@release", "@workspace", "@playground"] },
7923
async ({ page }) => {
80-
await awaitBootstrapTest(page);
81-
82-
await page.getByTestId("blank-flow").click();
83-
84-
await page.waitForSelector('[data-testid="sidebar-search-input"]', {
85-
timeout: 30000,
86-
state: "visible",
24+
await test.step("create blank flow and capture flow id", async () => {
25+
await awaitBootstrapTest(page);
26+
27+
const flowCreationPromise = page.waitForResponse(
28+
(resp) =>
29+
resp.url().includes("/api/v1/flows") &&
30+
resp.request().method() === "POST" &&
31+
resp.status() === 201,
32+
{ timeout: 15000 },
33+
);
34+
35+
await page.getByTestId("blank-flow").click();
36+
37+
const creationResponse = await flowCreationPromise;
38+
const flowData = await creationResponse.json();
39+
// Capture id before asserting format so afterEach can still clean up
40+
// if the regex assertion fails on an unexpected id shape.
41+
createdFlowId = flowData.id ?? null;
42+
expect(flowData.id, "flow creation response missing id").toMatch(
43+
/^[0-9a-f-]{36}$/,
44+
);
8745
});
8846

89-
await page.getByTestId("sidebar-search-input").fill("api request");
47+
await test.step("add Text Input and fill its value", async () => {
48+
await page.getByTestId("sidebar-search-input").fill("text input");
49+
await page
50+
.getByTestId("input_outputText Input")
51+
.hover()
52+
.then(async () => {
53+
await page.getByTestId("add-component-button-text-input").click();
54+
});
55+
56+
await expect(page.locator(".react-flow__node")).toHaveCount(1, {
57+
timeout: 10000,
58+
});
9059

91-
await page.waitForSelector('[data-testid="data_sourceAPI Request"]', {
92-
timeout: 10000,
93-
state: "visible",
60+
await page
61+
.getByTestId("textarea_str_input_value")
62+
.fill("Test content to copy");
9463
});
9564

96-
await page
97-
.getByTestId("data_sourceAPI Request")
98-
.hover()
99-
.then(async () => {
100-
await page.getByTestId("add-component-button-api-request").click();
101-
102-
await page.waitForTimeout(500);
103-
104-
await page
105-
.getByTestId("popover-anchor-input-url_input")
106-
.first()
107-
.fill("https://httpbin.org/json");
65+
await test.step("run component and open output modal", async () => {
66+
await page.getByTestId("button_run_text input").click();
67+
await expect(page.getByText("built successfully").last()).toBeVisible({
68+
timeout: 30000,
10869
});
10970

110-
await page.getByTestId("button_run_api request").click();
111-
112-
await page.waitForSelector("text=Running", {
113-
timeout: 30000,
114-
state: "visible",
71+
await page
72+
.locator('[data-testid^="output-inspection-"]')
73+
.first()
74+
.click();
75+
await expect(page.getByText("Component Output").first()).toBeVisible({
76+
timeout: 30000,
77+
});
11578
});
11679

117-
await page.waitForSelector("text=built successfully", { timeout: 30000 });
118-
119-
await page
120-
.getByTestId("output-inspection-api response-apirequest")
121-
.click();
80+
await test.step("click copy and verify Check → Copy icon transition", async () => {
81+
const copyButton = page.getByTestId("copy-output-button");
82+
await expect(copyButton).toBeVisible();
83+
await copyButton.click();
12284

123-
await page.waitForSelector("text=Component Output", { timeout: 30000 });
124-
125-
// Verify the copy button exists and click it
126-
const copyButton = page.getByTestId("copy-output-button");
127-
await expect(copyButton).toBeVisible();
128-
129-
await copyButton.click();
130-
131-
// Verify the success message appears
132-
await page.waitForSelector("text=Copied to clipboard", {
133-
timeout: 5000,
85+
await expect(page.getByText("Copied to clipboard")).toBeVisible({
86+
timeout: 5000,
87+
});
88+
await expect(
89+
copyButton.locator('[data-testid="icon-Check"]'),
90+
).toBeVisible();
91+
92+
// Icon reverts to Copy after the success state expires (~2s in UI).
93+
// Web-first assertion polls until the Copy icon reappears.
94+
await expect(
95+
copyButton.locator('[data-testid="icon-Copy"]'),
96+
).toBeVisible({ timeout: 5000 });
13497
});
13598
},
13699
);

0 commit comments

Comments
 (0)