Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions scripts/e2e/run-playwright-smoke.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 12 additions & 5 deletions src/app/[lang]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,18 @@ export default async function Home({ params }: { params: Promise<{ lang: string
</p>

<div className="mt-8 flex flex-wrap items-center justify-center gap-4">
<SearchButton label={heroSearchLabel} />
<Link
href={`/${locale}/install-app`}
prefetch={false}
className="inline-flex min-h-11 items-center rounded-lg border border-border/60 bg-background/80 px-5 text-sm font-medium text-foreground backdrop-blur-sm transition-all hover:border-primary/30 hover:bg-background hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
<SearchButton label={heroSearchLabel} />
<Link
href={`/${locale}/pipeline-builder`}
prefetch={false}
className="inline-flex min-h-11 items-center rounded-lg border border-primary/35 bg-primary/12 px-5 text-sm font-medium text-primary backdrop-blur-sm transition-all hover:border-primary/50 hover:bg-primary/16 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
>
{getLocalizedToolTitle("pipeline_builder")}
</Link>
<Link
href={`/${locale}/install-app`}
prefetch={false}
className="inline-flex min-h-11 items-center rounded-lg border border-border/60 bg-background/80 px-5 text-sm font-medium text-foreground backdrop-blur-sm transition-all hover:border-primary/30 hover:bg-background hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
>
{installAppLinkLabel}
</Link>
Expand Down
15 changes: 15 additions & 0 deletions src/components/layout/navbar-mobile-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ export function NavbarMobileMenu({
>
<SheetTitle className="sr-only">{t.nav.navigation}</SheetTitle>
<div className="grid grid-cols-2 gap-2 px-1">
<SheetClose asChild>
<Link
href={`/${lang}/pipeline-builder`}
prefetch={false}
aria-current={pathname === `/${lang}/pipeline-builder` ? "page" : undefined}
className={cn(
"col-span-2 rounded-lg border px-3 py-2 text-sm font-medium",
pathname === `/${lang}/pipeline-builder`
? "border-primary/35 bg-primary/12 text-primary"
: "border-primary/30 bg-primary/10 text-primary hover:bg-primary/15"
)}
>
{(t.tools["pipeline_builder"] as { title?: string } | undefined)?.title ?? "Pipeline Builder"}
</Link>
</SheetClose>
{CATEGORY_LINKS.map((cat) => {
const href = getCategoryHref(cat.slug)
return (
Expand Down
14 changes: 13 additions & 1 deletion src/components/layout/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Link from "next/link"
import { ArrowUpRight, Search } from "lucide-react"
import { ArrowUpRight, Search, Workflow } from "lucide-react"
import { DeferredMobileNavMenu } from "./deferred-mobile-nav-menu"
import { DeferredNavbarControls } from "./deferred-navbar-controls"
import { Button } from "@/components/ui/button"
Expand All @@ -10,6 +10,7 @@ import { cn } from "@/core/utils/utils"
export type NavbarLabels = {
allTools: string
openNavigation: string
pipelineBuilder: string
search: string
}

Expand Down Expand Up @@ -64,6 +65,17 @@ export function Navbar({
</Button>

<div className="ml-auto flex shrink-0 items-center gap-1.5 max-[420px]:gap-0.5">
<Link
href={`/${lang}/pipeline-builder`}
className={cn(
"hidden min-h-10 items-center gap-1.5 rounded-lg border border-primary/30 bg-primary/10 px-3 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/45 lg:inline-flex",
"text-primary hover:border-primary/50 hover:bg-primary/15"
)}
>
<Workflow className="h-3.5 w-3.5" />
{labels.pipelineBuilder}
</Link>

<Link
href={allToolsHref}
className={cn(
Expand Down
1 change: 1 addition & 0 deletions src/components/layout/server-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function ServerNavbar({
labels={{
allTools: requireTranslationValue(translations.common.all_tools, "common.all_tools"),
openNavigation: `${requireTranslationValue(translations.common.open, "common.open")} ${requireTranslationValue(translations.nav.navigation, "nav.navigation")}`,
pipelineBuilder: requireTranslationValue(translations.tools.pipeline_builder.title, "tools.pipeline_builder.title"),
search: requireTranslationValue(translations.nav.search, "nav.search"),
}}
/>
Expand Down
23 changes: 14 additions & 9 deletions src/core/i18n/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2461,21 +2461,15 @@
"pipeline_builder": {
"title": "Pipeline-Ersteller",
"description": "Verkettet lokale Entwicklerwerkzeuge zu wiederholbaren Browser-Rezepten mit Import, Export, Teilen und lokalem Speichern.",
"privacy_note": "Rezepte laufen vollständig in diesem Browser. Geteilte URLs enthalten standardmäßig nur Workflow-Struktur und öffentliche Optionen, keine Laufzeiteingaben.",
"share_runtime_input_hint": "Share-URLs enthalten nur Schrittstruktur und öffentliche Optionen. Konstante Schritteingaben bleiben lokal, außer beim JSON-Export.",
"privacy_note": "Rezepte laufen vollständig in diesem Browser. Speichern, Export und Teilen enthalten standardmäßig nur Workflow-Struktur und öffentliche Optionen, keine Laufzeiteingaben.",
"share_runtime_input_hint": "Speichern, Export und Teilen behalten nur Schrittstruktur und öffentliche Optionen. Konstante Schritteingaben werden nur für den aktuellen Lauf verwendet und aus gespeicherten Recipe-JSON entfernt.",
"storage_unavailable": "IndexedDB ist nicht verfügbar; lokal gespeicherte Rezepte sind in diesem Browser deaktiviert.",
"templates_title": "Integrierte Rezepte",
"templates_description": "Starten Sie mit lokalen, datenschutzfreundlichen Workflows und passen Sie die Schritte vor dem Ausführen an.",
"use_template": "Nutzen",
"template_loaded": "Rezeptvorlage geladen",
"template_json_minify_base64url_title": "JSON minifizieren zu URL-sicherem Base64",
"template_json_minify_base64url_description": "Minifiziert eine JSON-Nutzlast und kodiert sie für URLs, Umgebungswerte oder Testdaten.",
"template_url_decode_json_title": "URL dekodieren und JSON formatieren",
"template_url_decode_json_description": "Dekodiert einen URL-kodierten JSON-Blob und formatiert ihn zur Prüfung.",
"template_clean_copied_config_title": "Kopierten Konfigurationstext bereinigen",
"template_clean_copied_config_description": "Entfernt unsichtbare Zeichen, normalisiert Leerzeichen und reduziert versehentliche Mehrfachabstände.",
"template_scrub_log_secrets_title": "Logs bereinigen und Secrets maskieren",
"template_scrub_log_secrets_description": "Normalisiert eingefügte Logs und maskiert Tokens, E-Mails, IPs und Zugangsdaten vor dem Teilen.",
"template_step_minify_json": "JSON minifizieren",
"template_step_base64url_encode": "Base64 URL-sicher kodieren",
"template_step_url_decode": "URL-Komponente dekodieren",
Expand Down Expand Up @@ -2566,7 +2560,18 @@
"choice_minify": "Minifizieren",
"choice_component": "Komponente",
"choice_full": "Vollständige URL",
"choice_reserved": "Reservierte Zeichen"
"choice_reserved": "Reservierte Zeichen",
"template_api_payload_cleanup_title": "API-Payload bereinigen",
"template_api_payload_cleanup_description": "Minifiziert eine JSON-Nutzlast und kodiert sie für URLs, Umgebungswerte oder Testdaten.",
"template_url_json_cleanup_title": "URL-JSON bereinigen",
"template_url_json_cleanup_description": "Dekodiert einen URL-kodierten JSON-Blob und formatiert ihn zur Prüfung.",
"template_security_token_review_title": "Security-Token prüfen",
"template_security_token_review_description": "Dekodiert ein Beispiel-JWT-Payload lokal und formatiert die Claims zur Prüfung, ohne die Signatur zu verifizieren.",
"template_log_scrub_before_sharing_title": "Logs vor dem Teilen maskieren",
"template_log_scrub_before_sharing_description": "Normalisiert eingefügte Logs und maskiert Tokens, E-Mails, IPs und Zugangsdaten vor dem Teilen.",
"template_step_decode_jwt_payload": "JWT-Payload dekodieren",
"step_io_hint": "{input}-Eingabe -> {output}-Ausgabe",
"external_request_step_notice": "Externer Request-Schritt: Netzwerkziel vor dem Ausführen prüfen."
},
"saml_decoder": {
"title": "SAML-Dekoder",
Expand Down
23 changes: 14 additions & 9 deletions src/core/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2481,21 +2481,15 @@
"pipeline_builder": {
"title": "Pipeline Builder",
"description": "Chain local developer tools into repeatable browser-only recipes with import, export, sharing, and local saves.",
"privacy_note": "Recipes run in this browser. Shared URLs include workflow structure and public options only; runtime input is excluded by default.",
"share_runtime_input_hint": "Share URLs keep step structure and public options only. Constant step inputs stay local unless exported as JSON.",
"privacy_note": "Recipes run in this browser. Save, export, and share keep workflow structure and public options only; runtime input is excluded by default.",
"share_runtime_input_hint": "Save, export, and share keep step structure and public options only. Constant step input is used only for the current run and is stripped from saved recipe JSON.",
"storage_unavailable": "IndexedDB is unavailable, so local saved recipes are disabled in this browser.",
"templates_title": "Built-in recipes",
"templates_description": "Start from local, privacy-safe workflows and edit the steps before running.",
"use_template": "Use",
"template_loaded": "Recipe template loaded",
"template_json_minify_base64url_title": "JSON minify to URL-safe Base64",
"template_json_minify_base64url_description": "Minify a JSON payload, then encode it for URLs, env values, or test fixtures.",
"template_url_decode_json_title": "URL decode and pretty-print JSON",
"template_url_decode_json_description": "Decode a URL-encoded JSON blob and format it for review.",
"template_clean_copied_config_title": "Clean copied config text",
"template_clean_copied_config_description": "Remove invisible characters, normalize spaces, and collapse accidental whitespace.",
"template_scrub_log_secrets_title": "Clean and scrub log secrets",
"template_scrub_log_secrets_description": "Normalize pasted logs, then redact tokens, emails, IPs, and credentials before sharing.",
"template_step_minify_json": "Minify JSON",
"template_step_base64url_encode": "Base64 URL-safe encode",
"template_step_url_decode": "URL component decode",
Expand Down Expand Up @@ -2586,7 +2580,18 @@
"choice_minify": "Minify",
"choice_component": "Component",
"choice_full": "Full URL",
"choice_reserved": "Reserved chars"
"choice_reserved": "Reserved chars",
"template_api_payload_cleanup_title": "API payload cleanup",
"template_api_payload_cleanup_description": "Minify a JSON payload, then encode it for URLs, env values, or test fixtures.",
"template_url_json_cleanup_title": "URL JSON cleanup",
"template_url_json_cleanup_description": "Decode a URL-encoded JSON blob and format it for review.",
"template_security_token_review_title": "Security token review",
"template_security_token_review_description": "Decode a sample JWT payload locally, then format the claims for review without verifying the signature.",
"template_log_scrub_before_sharing_title": "Log scrub before sharing",
"template_log_scrub_before_sharing_description": "Normalize pasted logs, then redact tokens, emails, IPs, and credentials before sharing.",
"template_step_decode_jwt_payload": "Decode JWT payload",
"step_io_hint": "{input} input -> {output} output",
"external_request_step_notice": "External request step: confirm the network target before running."
},
"saml_decoder": {
"title": "SAML Decoder",
Expand Down
Loading