Release flow Tools v0.1.0

This commit is contained in:
2026-09-01 14:10:51 +02:00
commit 12d0ac0155
58 changed files with 8809 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
import { expect, test } from "@playwright/test";
const ORIGIN = "http://127.0.0.1:4221";
test("runs stages nested and locally", async ({ page }) => {
const external: string[] = [];
await page.route("**/*", async (r) => {
if (new URL(r.request().url()).origin !== ORIGIN) {
external.push(r.request().url());
await r.abort();
} else await r.continue();
});
await page.setViewportSize({ width: 1800, height: 900 });
await page.goto("/deep/nested/flow/");
await expect(page.getByRole("heading", { name: "Flow Tools" })).toBeVisible();
await page.getByRole("button", { name: "Run complete pipeline" }).click();
await expect(page.getByRole("status")).toContainText("2 stages");
await expect(page.getByRole("cell", { name: "Ada" })).toBeVisible();
expect(external).toEqual([]);
expect(
await page
.locator(".toolbox-shell__main")
.evaluate((n) => getComputedStyle(n).width),
).toBe("1440px");
});
test("exports recipe and keeps snapshots after error", async ({ page }) => {
await page.goto("/deep/nested/flow/");
const pending = page.waitForEvent("download");
await page.getByRole("button", { name: "Recipe JSON" }).click();
expect((await pending).suggestedFilename()).toBe("flow-recipe.json");
await page.getByLabel("Field", { exact: true }).first().fill("");
await page.getByRole("button", { name: "Run complete pipeline" }).click();
await expect(page.getByRole("status")).toContainText("remain visible");
});
test("PWA theme, offline reload, and manifest", async ({
page,
context,
request,
}) => {
await page.goto("/deep/nested/flow/");
await page.getByRole("button", { name: "Personalize" }).click();
await page.getByRole("button", { name: "Dark" }).click();
await expect(page.locator(".toolbox-shell").first()).toHaveAttribute(
"data-toolbox-theme",
"dark",
);
expect(
await page.evaluate(async () =>
Boolean(await navigator.serviceWorker.ready),
),
).toBe(true);
await context.setOffline(true);
await page.reload();
await expect(page.getByRole("heading", { name: "Flow Tools" })).toBeVisible();
await context.setOffline(false);
const response = await request.get("/deep/nested/flow/");
expect(response.headers()["content-security-policy"]).toContain(
"connect-src 'self'",
);
const m = await request.get("/deep/nested/flow/toolbox-app.json");
await expect(m.json()).resolves.toMatchObject({
id: "de.add-ideas.flow-tools",
version: "0.1.0",
});
});
+18
View File
@@ -0,0 +1,18 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { Workbench } from "../../src/components/Workbench";
describe("Flow Workbench", () => {
it("runs the visible pipeline and exposes its result", async () => {
const user = userEvent.setup();
render(<Workbench />);
await user.click(
screen.getByRole("button", { name: "Run complete pipeline" }),
);
expect(screen.getByRole("status")).toHaveTextContent("Completed 2 stages");
expect(screen.getByRole("cell", { name: "Ada" })).toBeVisible();
});
});
+59
View File
@@ -0,0 +1,59 @@
import { describe, expect, it } from "vitest";
import {
importRecipe,
records,
runPipeline,
type Stage,
} from "../../src/core/pipeline";
describe("pipeline", () => {
const input = records([
{ name: "Ada", team: "a", score: 2 },
{ name: "Grace", team: "a", score: 4 },
{ name: "Linus", team: "b", score: 9 },
]);
it("filters sorts groups and exposes snapshots", () => {
const stages: Stage[] = [
{
id: "1",
type: "filter",
enabled: true,
config: { field: "score", operator: ">=", value: "4" },
},
{
id: "2",
type: "group",
enabled: true,
config: { key: "team", fn: "avg", field: "score", output: "mean" },
},
];
const result = runPipeline(input, stages);
expect(result.rows).toEqual([
{ team: "a", mean: 4 },
{ team: "b", mean: 9 },
]);
expect(result.snapshots).toHaveLength(3);
});
it("joins with prefixed right values", () => {
const result = runPipeline(
input,
[
{
id: "j",
type: "join",
enabled: true,
config: { left: "team", right: "id", mode: "left", prefix: "r_" },
},
],
records([{ id: "a", lead: "Mina" }]),
);
expect(result.rows[0]?.r_lead).toBe("Mina");
expect(result.rows[2]?.r_lead).toBeUndefined();
});
it("validates recipes and rejects code stages", () => {
expect(() =>
importRecipe(
'{"schema":"de.add-ideas.flow-tools.recipe.v1","stages":[{"type":"javascript","config":{}}]}',
),
).toThrow(/Invalid recipe/iu);
});
});