From 5ebaf60f4531574ca507240218bb0600d541a91d Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Sun, 21 Jun 2026 05:16:37 -0700 Subject: [PATCH] feat: improve pipeline builder workflows --- scripts/e2e/run-playwright-smoke.js | 75 ++++++++++++++++ src/app/[lang]/page.tsx | 17 ++-- src/components/layout/navbar-mobile-menu.tsx | 15 ++++ src/components/layout/navbar.tsx | 14 ++- src/components/layout/server-navbar.tsx | 1 + src/core/i18n/translations/de.json | 23 +++-- src/core/i18n/translations/en.json | 23 +++-- src/core/i18n/translations/fr.json | 23 +++-- src/core/i18n/translations/ja.json | 23 +++-- src/core/i18n/translations/ko.json | 23 +++-- src/core/i18n/translations/zh-CN.json | 23 +++-- src/core/i18n/translations/zh-TW.json | 23 +++-- src/features/pipeline/recipe-codec.ts | 15 ++-- src/features/pipeline/recipe-import-export.ts | 3 +- src/features/pipeline/recipe-store.ts | 29 +++--- src/features/pipeline/recipe-templates.ts | 54 ++++++++---- src/features/tools/pipeline-builder/page.tsx | 11 ++- .../pipeline-builder/pipeline-step-list.tsx | 17 +++- src/lib/sitemap-lastmod.json | 14 +-- tests/component/layout-components.test.tsx | 2 + .../phase3-pipeline-builder-page.test.tsx | 67 +++++++++++++- tests/unit/pipeline-foundation.test.ts | 88 +++++++++++++++---- 22 files changed, 448 insertions(+), 135 deletions(-) diff --git a/scripts/e2e/run-playwright-smoke.js b/scripts/e2e/run-playwright-smoke.js index ed9a0309..5396e619 100644 --- a/scripts/e2e/run-playwright-smoke.js +++ b/scripts/e2e/run-playwright-smoke.js @@ -1,5 +1,6 @@ import { createServer } from "node:http"; import { existsSync } from "node:fs"; +import { readFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { chromium } from "playwright"; @@ -712,11 +713,85 @@ async function assertPipelineRecipeJourney(context, baseUrl) { await page.waitForSelector("main", { timeout: 15_000 }); await page.getByRole("button", { name: /Try Example/i }).first().click(); + await page.getByLabel("Recipe name").fill("Smoke saved recipe"); + await page.getByLabel("Initial input").fill('{ "apiKey": "runtime-secret-value-987", "ok": true }'); await page.getByRole("button", { name: /Run Recipe/i }).first().click(); await page.waitForFunction(() => { const readonlyOutput = Array.from(document.querySelectorAll("textarea")).find((node) => node.readOnly); return Boolean(readonlyOutput?.value.trim()) && document.body.innerText.includes("OK"); }, null, { timeout: 15_000 }); + + const saveButton = page.getByRole("button", { name: /^Save$/ }).first(); + await page.waitForFunction(() => { + const buttons = Array.from(document.querySelectorAll("button")); + return buttons.some((button) => button.textContent?.trim() === "Save" && !button.disabled); + }, null, { timeout: 15_000 }); + await saveButton.click(); + await page.waitForFunction(() => { + const select = document.querySelector("[aria-label='Select saved recipe']"); + if (!(select instanceof HTMLSelectElement)) return false; + return Array.from(select.options).some((option) => option.textContent?.trim() === "Smoke saved recipe"); + }, null, { timeout: 15_000 }); + + const storedRecipes = await page.evaluate(async () => { + return await new Promise((resolve, reject) => { + const request = indexedDB.open("byteflow-pipeline-recipes"); + request.onerror = () => reject(request.error ?? new Error("Unable to open recipe store.")); + request.onsuccess = () => { + const db = request.result; + const tx = db.transaction("recipes", "readonly"); + const getAll = tx.objectStore("recipes").getAll(); + getAll.onerror = () => reject(getAll.error ?? new Error("Unable to read saved recipes.")); + getAll.onsuccess = () => { + db.close(); + resolve(getAll.result); + }; + }; + }); + }); + const serializedSavedRecipes = JSON.stringify(storedRecipes); + if (!serializedSavedRecipes.includes("Smoke saved recipe")) { + throw new Error("Pipeline Builder did not save the smoke recipe locally."); + } + if (serializedSavedRecipes.includes("runtime-secret-value-987")) { + throw new Error("Pipeline Builder saved runtime input in IndexedDB."); + } + + await page.getByLabel("Recipe name").fill("Unsaved scratch recipe"); + await page.getByLabel("Select saved recipe").selectOption({ label: "Smoke saved recipe" }); + await page.getByRole("button", { name: /^Load$/ }).click(); + await page.waitForFunction(() => { + const input = document.querySelector("#recipe-name"); + return input instanceof HTMLInputElement && input.value === "Smoke saved recipe"; + }, null, { timeout: 15_000 }); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: /^Export JSON$/ }).first().click(); + const download = await downloadPromise; + const downloadPath = await download.path(); + if (!downloadPath) { + throw new Error("Pipeline Builder export did not produce a downloadable file."); + } + const exportedRecipeJson = await readFile(downloadPath, "utf8"); + const exportedRecipe = JSON.parse(exportedRecipeJson); + if (exportedRecipeJson.includes("runtime-secret-value-987")) { + throw new Error("Pipeline Builder exported runtime input in recipe JSON."); + } + if (!Array.isArray(exportedRecipe.steps) || exportedRecipe.steps.length < 2) { + throw new Error("Pipeline Builder export did not include recipe steps."); + } + + await page.locator('input[type="file"]').setInputFiles({ + name: "smoke-recipe.json", + mimeType: "application/json", + buffer: Buffer.from(exportedRecipeJson), + }); + await page.waitForFunction(() => { + const input = document.querySelector("#recipe-name"); + return input instanceof HTMLInputElement && input.value === "Smoke saved recipe"; + }, null, { timeout: 15_000 }); + await page.getByRole("button", { name: /Run Recipe/i }).first().click(); + await page.waitForFunction(() => document.body.innerText.includes("OK"), null, { timeout: 15_000 }); await assertBasicAccessibility(page, "/en/pipeline-builder recipe"); if (runtimeErrors.length > 0) { diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx index d586bed1..3ac81f43 100644 --- a/src/app/[lang]/page.tsx +++ b/src/app/[lang]/page.tsx @@ -194,11 +194,18 @@ export default async function Home({ params }: { params: Promise<{ lang: string