From a12d4e13d0e3950441eac89d5144e86781be73c4 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 3 Apr 2025 16:40:51 +0200 Subject: [PATCH] e2e: added script to auto test adapters --- app/e2e/adapters.ts | 206 +++++++++++++++++++++++++++ app/e2e/base.e2e-spec.ts | 9 +- app/e2e/inc/adapters.ts | 37 +++-- app/e2e/media.e2e-spec.ts | 10 +- app/tsconfig.build.json | 2 +- app/tsconfig.json | 9 +- examples/astro/.gitignore | 5 +- examples/astro/bknd.config.ts | 2 +- examples/bun/.gitignore | 3 + examples/bun/index.ts | 9 ++ examples/nextjs/.gitignore | 3 + examples/nextjs/bknd.config.ts | 2 +- examples/node/.gitignore | 2 + examples/node/index.js | 15 +- examples/react-router/.gitignore | 3 + examples/react-router/bknd.config.ts | 2 +- 16 files changed, 295 insertions(+), 24 deletions(-) create mode 100644 app/e2e/adapters.ts create mode 100644 examples/node/.gitignore diff --git a/app/e2e/adapters.ts b/app/e2e/adapters.ts new file mode 100644 index 0000000..cac87e1 --- /dev/null +++ b/app/e2e/adapters.ts @@ -0,0 +1,206 @@ +import { $ } from "bun"; +import path from "node:path"; +import c from "picocolors"; + +const basePath = new URL(import.meta.resolve("../../")).pathname.slice(0, -1); + +async function run( + cmd: string[] | string, + opts: Bun.SpawnOptions.OptionsObject & {}, + onChunk: (chunk: string, resolve: (data: any) => void, reject: (err: Error) => void) => void, +): Promise<{ proc: Bun.Subprocess; data: any }> { + return new Promise((resolve, reject) => { + const proc = Bun.spawn(Array.isArray(cmd) ? cmd : cmd.split(" "), { + ...opts, + stdout: "pipe", + stderr: "pipe", + }); + + // Read from stdout + const reader = proc.stdout.getReader(); + const decoder = new TextDecoder(); + + // Function to read chunks + let resolveCalled = false; + (async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const text = decoder.decode(value); + if (!resolveCalled) { + console.log(c.dim(text.replace(/\n$/, ""))); + } + onChunk( + text, + (data) => { + resolve({ proc, data }); + resolveCalled = true; + }, + reject, + ); + } + } catch (err) { + reject(err); + } + })(); + + proc.exited.then((code) => { + if (code !== 0 && code !== 130) { + throw new Error(`Process exited with code ${code}`); + } + }); + }); +} + +const adapters = { + node: { + dir: path.join(basePath, "examples/node"), + clean: async function () { + const cwd = path.relative(process.cwd(), this.dir); + await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`; + }, + start: async function () { + return await run( + "npm run start", + { + cwd: this.dir, + }, + (chunk, resolve, reject) => { + const regex = /running on (http:\/\/.*)\n/; + if (regex.test(chunk)) { + resolve(chunk.match(regex)?.[1]); + } + }, + ); + }, + }, + bun: { + dir: path.join(basePath, "examples/bun"), + clean: async function () { + const cwd = path.relative(process.cwd(), this.dir); + await $`cd ${cwd} && rm -rf uploads data.db && mkdir -p uploads`; + }, + start: async function () { + return await run( + "npm run start", + { + cwd: this.dir, + }, + (chunk, resolve, reject) => { + const regex = /running on (http:\/\/.*)\n/; + if (regex.test(chunk)) { + resolve(chunk.match(regex)?.[1]); + } + }, + ); + }, + }, + cloudflare: { + dir: path.join(basePath, "examples/cloudflare-worker"), + clean: async function () { + const cwd = path.relative(process.cwd(), this.dir); + await $`cd ${cwd} && rm -rf .wrangler node_modules/.cache node_modules/.mf`; + }, + start: async function () { + return await run( + "npm run dev", + { + cwd: this.dir, + }, + (chunk, resolve, reject) => { + const regex = /Ready on (http:\/\/.*)/; + if (regex.test(chunk)) { + resolve(chunk.match(regex)?.[1]); + } + }, + ); + }, + }, + "react-router": { + dir: path.join(basePath, "examples/react-router"), + clean: async function () { + const cwd = path.relative(process.cwd(), this.dir); + await $`cd ${cwd} && rm -rf .react-router data.db`; + await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`; + }, + start: async function () { + return await run( + "npm run dev", + { + cwd: this.dir, + }, + (chunk, resolve, reject) => { + const regex = /Local.*?(http:\/\/.*)\//; + if (regex.test(chunk)) { + resolve(chunk.match(regex)?.[1]); + } + }, + ); + }, + }, + nextjs: { + dir: path.join(basePath, "examples/nextjs"), + clean: async function () { + const cwd = path.relative(process.cwd(), this.dir); + await $`cd ${cwd} && rm -rf .nextjs data.db`; + await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`; + }, + start: async function () { + return await run( + "npm run dev", + { + cwd: this.dir, + }, + (chunk, resolve, reject) => { + const regex = /Local.*?(http:\/\/.*)\n/; + if (regex.test(chunk)) { + resolve(chunk.match(regex)?.[1]); + } + }, + ); + }, + }, + astro: { + dir: path.join(basePath, "examples/astro"), + clean: async function () { + const cwd = path.relative(process.cwd(), this.dir); + await $`cd ${cwd} && rm -rf .astro data.db`; + await $`cd ${cwd} && rm -rf public/uploads && mkdir -p public/uploads`; + }, + start: async function () { + return await run( + "npm run dev", + { + cwd: this.dir, + }, + (chunk, resolve, reject) => { + const regex = /Local.*?(http:\/\/.*)\//; + if (regex.test(chunk)) { + resolve(chunk.match(regex)?.[1]); + } + }, + ); + }, + }, +} as const; + +for (const [name, config] of Object.entries(adapters)) { + console.log("adapter", c.cyan(name)); + await config.clean(); + + const { proc, data } = await config.start(); + console.log("proc:", proc.pid, "data:", c.cyan(data)); + //proc.kill();process.exit(0); + + await $`TEST_URL=${data} TEST_ADAPTER=${name} bun run test:e2e`; + console.log("DONE!"); + + while (!proc.killed) { + proc.kill("SIGINT"); + await Bun.sleep(250); + console.log("Waiting for process to exit..."); + } + //process.exit(0); +} diff --git a/app/e2e/base.e2e-spec.ts b/app/e2e/base.e2e-spec.ts index 8ed1469..20bbd70 100644 --- a/app/e2e/base.e2e-spec.ts +++ b/app/e2e/base.e2e-spec.ts @@ -2,13 +2,16 @@ import { test, expect } from "@playwright/test"; import { testIds } from "../src/ui/lib/config"; +import { getAdapterConfig } from "./inc/adapters"; +const config = getAdapterConfig(); + test("start page has expected title", async ({ page }) => { - await page.goto("/"); + await page.goto(config.base_path); await expect(page).toHaveTitle(/BKND/); }); test("start page has expected heading", async ({ page }) => { - await page.goto("/"); + await page.goto(config.base_path); // Example of checking if a heading with "No entity selected" exists and is visible const heading = page.getByRole("heading", { name: /No entity selected/i }); @@ -16,7 +19,7 @@ test("start page has expected heading", async ({ page }) => { }); test("modal opens on button click", async ({ page }) => { - await page.goto("/"); + await page.goto(config.base_path); await page.getByTestId(testIds.data.btnCreateEntity).click(); await expect(page.getByRole("dialog")).toBeVisible(); }); diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts index 2a8eff0..347d23b 100644 --- a/app/e2e/inc/adapters.ts +++ b/app/e2e/inc/adapters.ts @@ -1,23 +1,44 @@ const adapter = process.env.TEST_ADAPTER; const default_config = { - media_adapter: "local" + media_adapter: "local", + base_path: "", } as const; const configs = { cloudflare: { - media_adapter: "r2" - } -} + media_adapter: "r2", + }, + "react-router": { + base_path: "/admin", + }, + nextjs: { + base_path: "/admin", + }, + astro: { + base_path: "/admin", + }, + node: { + base_path: "", + }, + bun: { + base_path: "", + }, +}; export function getAdapterConfig(): typeof default_config { if (adapter) { if (!configs[adapter]) { - throw new Error(`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`); + console.warn( + `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`, + ); + } else { + return { + ...default_config, + ...configs[adapter], + }; } - - return configs[adapter] as typeof default_config; } return default_config; -} \ No newline at end of file +} diff --git a/app/e2e/media.e2e-spec.ts b/app/e2e/media.e2e-spec.ts index 72f4e09..307ba39 100644 --- a/app/e2e/media.e2e-spec.ts +++ b/app/e2e/media.e2e-spec.ts @@ -7,10 +7,10 @@ import { getAdapterConfig } from "./inc/adapters"; // Annotate entire file as serial. test.describe.configure({ mode: "serial" }); -const adapterConfig = getAdapterConfig(); +const config = getAdapterConfig(); test("can enable media", async ({ page }) => { - await page.goto("/media/settings"); + await page.goto(`${config.base_path}/media/settings`); // enable const enableToggle = page.locator("css=button#enabled"); @@ -20,7 +20,7 @@ test("can enable media", async ({ page }) => { await expect(enableToggle).toHaveAttribute("aria-checked", "true"); // select local - const adapterChoice = page.locator(`css=button#adapter-${adapterConfig.media_adapter}`); + const adapterChoice = page.locator(`css=button#adapter-${config.media_adapter}`); await expect(adapterChoice).toBeVisible(); await adapterChoice.click(); @@ -37,12 +37,12 @@ test("can enable media", async ({ page }) => { expect(response?.status(), "fresh config 200").toBe(200); const body = (await response?.json()) as SchemaResponse; expect(body.config.media.enabled, "media is enabled").toBe(true); - expect(body.config.media.adapter.type, "correct adapter").toBe(adapterConfig.media_adapter); + expect(body.config.media.adapter?.type, "correct adapter").toBe(config.media_adapter); } }); test("can upload a file", async ({ page }) => { - await page.goto("/media"); + await page.goto(`${config.base_path}/media`); // check any text to contain "Upload files" await expect(page.getByText(/Upload files/i)).toBeVisible(); diff --git a/app/tsconfig.build.json b/app/tsconfig.build.json index f3a19cb..14c4f27 100644 --- a/app/tsconfig.build.json +++ b/app/tsconfig.build.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.json", "include": ["./src/**/*.ts", "./src/**/*.tsx"], - "exclude": ["./node_modules", "./__test__"] + "exclude": ["./node_modules", "./__test__", "./e2e"] } diff --git a/app/tsconfig.json b/app/tsconfig.json index 7a108bb..26f06ea 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -33,6 +33,13 @@ "bknd": ["./src/*"] } }, - "include": ["./src/**/*.ts", "./src/**/*.tsx", "vite.dev.ts", "build.ts", "__test__"], + "include": [ + "./src/**/*.ts", + "./src/**/*.tsx", + "vite.dev.ts", + "build.ts", + "__test__", + "e2e/**/*.ts" + ], "exclude": ["node_modules", "dist", "dist/types", "**/*.d.ts"] } diff --git a/examples/astro/.gitignore b/examples/astro/.gitignore index 6dbc2b5..1f80652 100644 --- a/examples/astro/.gitignore +++ b/examples/astro/.gitignore @@ -22,4 +22,7 @@ pnpm-debug.log* # jetbrains setting folder .idea/ -*.db \ No newline at end of file +*.db + +# Public uploads +/public/uploads/* diff --git a/examples/astro/bknd.config.ts b/examples/astro/bknd.config.ts index ebe3f2a..8ddea7e 100644 --- a/examples/astro/bknd.config.ts +++ b/examples/astro/bknd.config.ts @@ -42,7 +42,7 @@ export default { media: { enabled: true, adapter: local({ - path: "./public", + path: "./public/uploads", }), }, }, diff --git a/examples/bun/.gitignore b/examples/bun/.gitignore index 9b1ee42..45c5a18 100644 --- a/examples/bun/.gitignore +++ b/examples/bun/.gitignore @@ -173,3 +173,6 @@ dist # Finder (MacOS) folder config .DS_Store + +*.db +uploads/* \ No newline at end of file diff --git a/examples/bun/index.ts b/examples/bun/index.ts index 44f9e8f..ee830ac 100644 --- a/examples/bun/index.ts +++ b/examples/bun/index.ts @@ -8,6 +8,15 @@ const config: BunBkndConfig = { connection: { url: "file:data.db", }, + initialConfig: { + media: { + enabled: true, + adapter: { + type: "local", + config: { path: "./uploads" }, + }, + }, + }, }; serve(config); diff --git a/examples/nextjs/.gitignore b/examples/nextjs/.gitignore index 5ef6a52..9163d5f 100644 --- a/examples/nextjs/.gitignore +++ b/examples/nextjs/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Public uploads +/public/uploads/* diff --git a/examples/nextjs/bknd.config.ts b/examples/nextjs/bknd.config.ts index 68d15b5..39a12a7 100644 --- a/examples/nextjs/bknd.config.ts +++ b/examples/nextjs/bknd.config.ts @@ -51,7 +51,7 @@ export default { media: { enabled: true, adapter: local({ - path: "./public", + path: "./public/uploads", }), }, }, diff --git a/examples/node/.gitignore b/examples/node/.gitignore new file mode 100644 index 0000000..4856302 --- /dev/null +++ b/examples/node/.gitignore @@ -0,0 +1,2 @@ +*.db +uploads/* \ No newline at end of file diff --git a/examples/node/index.js b/examples/node/index.js index 30010c8..1464017 100644 --- a/examples/node/index.js +++ b/examples/node/index.js @@ -7,8 +7,19 @@ import { serve } from "bknd/adapter/node"; /** @type {import("bknd/adapter/node").NodeBkndConfig} */ const config = { connection: { - url: "file:data.db" - } + url: "file:data.db", + }, + initialConfig: { + media: { + enabled: true, + adapter: { + type: "local", + config: { + path: "./uploads", + }, + }, + }, + }, }; serve(config); diff --git a/examples/react-router/.gitignore b/examples/react-router/.gitignore index 261e24f..c44b37c 100644 --- a/examples/react-router/.gitignore +++ b/examples/react-router/.gitignore @@ -7,3 +7,6 @@ # React Router /.react-router/ /build/ + +# Public uploads +/public/uploads/* diff --git a/examples/react-router/bknd.config.ts b/examples/react-router/bknd.config.ts index 4cfa714..06239b3 100644 --- a/examples/react-router/bknd.config.ts +++ b/examples/react-router/bknd.config.ts @@ -41,7 +41,7 @@ export default { media: { enabled: true, adapter: local({ - path: "./public", + path: "./public/uploads", }), }, },