mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
e2e: added script to auto test adapters
This commit is contained in:
206
app/e2e/adapters.ts
Normal file
206
app/e2e/adapters.ts
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./node_modules", "./__test__"]
|
||||
"exclude": ["./node_modules", "./__test__", "./e2e"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
5
examples/astro/.gitignore
vendored
5
examples/astro/.gitignore
vendored
@@ -22,4 +22,7 @@ pnpm-debug.log*
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
*.db
|
||||
*.db
|
||||
|
||||
# Public uploads
|
||||
/public/uploads/*
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: local({
|
||||
path: "./public",
|
||||
path: "./public/uploads",
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
3
examples/bun/.gitignore
vendored
3
examples/bun/.gitignore
vendored
@@ -173,3 +173,6 @@ dist
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
*.db
|
||||
uploads/*
|
||||
@@ -8,6 +8,15 @@ const config: BunBkndConfig = {
|
||||
connection: {
|
||||
url: "file:data.db",
|
||||
},
|
||||
initialConfig: {
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: {
|
||||
type: "local",
|
||||
config: { path: "./uploads" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
serve(config);
|
||||
|
||||
3
examples/nextjs/.gitignore
vendored
3
examples/nextjs/.gitignore
vendored
@@ -39,3 +39,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Public uploads
|
||||
/public/uploads/*
|
||||
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: local({
|
||||
path: "./public",
|
||||
path: "./public/uploads",
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
2
examples/node/.gitignore
vendored
Normal file
2
examples/node/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.db
|
||||
uploads/*
|
||||
@@ -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);
|
||||
|
||||
3
examples/react-router/.gitignore
vendored
3
examples/react-router/.gitignore
vendored
@@ -7,3 +7,6 @@
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
|
||||
# Public uploads
|
||||
/public/uploads/*
|
||||
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
media: {
|
||||
enabled: true,
|
||||
adapter: local({
|
||||
path: "./public",
|
||||
path: "./public/uploads",
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user