mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
implement/init e2e tests (#135)
* init e2e * updated/moved vitest, finished merge * fix bun picking up e2e tests * e2e: overwrite webserver config with env * e2e: added adapter configs * e2e: replaced image
This commit is contained in:
2
app/.gitignore
vendored
2
app/.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
test-results
|
||||
playwright-report
|
||||
test-results
|
||||
bknd.config.*
|
||||
12
app/__test__/vitest/base.vi-test.ts
Normal file
12
app/__test__/vitest/base.vi-test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("Example Test Suite", () => {
|
||||
it("should pass basic arithmetic", () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
it("should handle async operations", async () => {
|
||||
const result = await Promise.resolve(42);
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
});
|
||||
8
app/__test__/vitest/setup.ts
Normal file
8
app/__test__/vitest/setup.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import "@testing-library/jest-dom";
|
||||
import { afterEach } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
|
||||
// Automatically cleanup after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
BIN
app/e2e/assets/image.jpg
Normal file
BIN
app/e2e/assets/image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
22
app/e2e/base.e2e-spec.ts
Normal file
22
app/e2e/base.e2e-spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// @ts-check
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { testIds } from "../src/ui/lib/config";
|
||||
|
||||
test("start page has expected title", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveTitle(/BKND/);
|
||||
});
|
||||
|
||||
test("start page has expected heading", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Example of checking if a heading with "No entity selected" exists and is visible
|
||||
const heading = page.getByRole("heading", { name: /No entity selected/i });
|
||||
await expect(heading).toBeVisible();
|
||||
});
|
||||
|
||||
test("modal opens on button click", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId(testIds.data.btnCreateEntity).click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
});
|
||||
23
app/e2e/inc/adapters.ts
Normal file
23
app/e2e/inc/adapters.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
const adapter = process.env.TEST_ADAPTER;
|
||||
|
||||
const default_config = {
|
||||
media_adapter: "local"
|
||||
} as const;
|
||||
|
||||
const configs = {
|
||||
cloudflare: {
|
||||
media_adapter: "r2"
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdapterConfig(): typeof default_config {
|
||||
if (adapter) {
|
||||
if (!configs[adapter]) {
|
||||
throw new Error(`Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`);
|
||||
}
|
||||
|
||||
return configs[adapter] as typeof default_config;
|
||||
}
|
||||
|
||||
return default_config;
|
||||
}
|
||||
55
app/e2e/media.e2e-spec.ts
Normal file
55
app/e2e/media.e2e-spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// @ts-check
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { testIds } from "../src/ui/lib/config";
|
||||
import type { SchemaResponse } from "../src/modules/server/SystemController";
|
||||
import { getAdapterConfig } from "./inc/adapters";
|
||||
|
||||
// Annotate entire file as serial.
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
const adapterConfig = getAdapterConfig();
|
||||
|
||||
test("can enable media", async ({ page }) => {
|
||||
await page.goto("/media/settings");
|
||||
|
||||
// enable
|
||||
const enableToggle = page.locator("css=button#enabled");
|
||||
if ((await enableToggle.getAttribute("aria-checked")) !== "true") {
|
||||
await expect(enableToggle).toBeVisible();
|
||||
await enableToggle.click();
|
||||
await expect(enableToggle).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
// select local
|
||||
const adapterChoice = page.locator(`css=button#adapter-${adapterConfig.media_adapter}`);
|
||||
await expect(adapterChoice).toBeVisible();
|
||||
await adapterChoice.click();
|
||||
|
||||
// save
|
||||
const saveBtn = page.getByRole("button", { name: /Update/i });
|
||||
await expect(saveBtn).toBeVisible();
|
||||
|
||||
// intercept network request, wait for it to finish and get the response
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest((request) => request.url().includes("api/system/schema")),
|
||||
saveBtn.click(),
|
||||
]);
|
||||
const response = await request.response();
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
test("can upload a file", async ({ page }) => {
|
||||
await page.goto("/media");
|
||||
// check any text to contain "Upload files"
|
||||
await expect(page.getByText(/Upload files/i)).toBeVisible();
|
||||
|
||||
// upload a file from disk
|
||||
// Start waiting for file chooser before clicking. Note no await.
|
||||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||||
await page.getByText("Upload file").click();
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles("./e2e/assets/image.jpg");
|
||||
});
|
||||
@@ -15,12 +15,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"test:all": "bun run test && bun run test:node",
|
||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
|
||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||
"build": "NODE_ENV=production bun run build.ts --minify --types",
|
||||
"build:all": "rm -rf dist && bun run build:static && NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||
"build:ci": "mkdir -p dist/static/.vite && echo '{}' > dist/static/.vite/manifest.json && NODE_ENV=production bun run build.ts",
|
||||
@@ -33,7 +27,20 @@
|
||||
"updater": "bun x npm-check-updates -ui",
|
||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||
"prepublishOnly": "bun run types && bun run test && bun run test:node && bun run build:all && cp ../README.md ./",
|
||||
"postpublish": "rm -f README.md"
|
||||
"postpublish": "rm -f README.md",
|
||||
"test": "ALL_TESTS=1 bun test --bail",
|
||||
"test:all": "bun run test && bun run test:node",
|
||||
"test:bun": "ALL_TESTS=1 bun test --bail",
|
||||
"test:node": "tsx --test $(find . -type f -name '*.native-spec.ts')",
|
||||
"test:adapters": "bun test src/adapter/**/*.adapter.spec.ts --bail",
|
||||
"test:coverage": "ALL_TESTS=1 bun test --bail --coverage",
|
||||
"test:vitest": "vitest run",
|
||||
"test:vitest:watch": "vitest",
|
||||
"test:vitest:coverage": "vitest run --coverage",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"license": "FSL-1.1-MIT",
|
||||
"dependencies": {
|
||||
@@ -76,18 +83,23 @@
|
||||
"@libsql/kysely-libsql": "^0.4.1",
|
||||
"@mantine/modals": "^7.17.1",
|
||||
"@mantine/notifications": "^7.17.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rjsf/core": "5.22.2",
|
||||
"@tabler/icons-react": "3.18.0",
|
||||
"@tailwindcss/postcss": "^4.0.12",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"jotai": "^2.12.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"kysely-d1": "^0.3.0",
|
||||
"open": "^10.1.0",
|
||||
"openapi-types": "^12.1.3",
|
||||
@@ -109,6 +121,7 @@
|
||||
"tsx": "^4.19.3",
|
||||
"vite": "^6.2.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.9",
|
||||
"wouter": "^3.6.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
||||
41
app/playwright.config.ts
Normal file
41
app/playwright.config.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const baseUrl = process.env.TEST_URL || "http://localhost:28623";
|
||||
const startCommand = process.env.TEST_START_COMMAND || "bun run dev";
|
||||
const autoStart = ["1", "true", undefined].includes(process.env.TEST_AUTO_START);
|
||||
|
||||
export default defineConfig({
|
||||
testMatch: "**/*.e2e-spec.ts",
|
||||
testDir: "./e2e",
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: "html",
|
||||
use: {
|
||||
baseURL: baseUrl,
|
||||
trace: "on-first-retry",
|
||||
video: "on-first-retry",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
/* {
|
||||
name: "firefox",
|
||||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
}, */
|
||||
],
|
||||
webServer: autoStart
|
||||
? {
|
||||
command: startCommand,
|
||||
url: baseUrl,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import { Controller } from "modules/Controller";
|
||||
import {
|
||||
MODULE_NAMES,
|
||||
type ModuleConfigs,
|
||||
type ModuleSchemas,
|
||||
type ModuleKey,
|
||||
getDefaultConfig,
|
||||
} from "modules/ModuleManager";
|
||||
@@ -36,6 +37,12 @@ export type ConfigUpdate<Key extends ModuleKey = ModuleKey> = {
|
||||
export type ConfigUpdateResponse<Key extends ModuleKey = ModuleKey> =
|
||||
| ConfigUpdate<Key>
|
||||
| { success: false; type: "type-invalid" | "error" | "unknown"; error?: any; errors?: any };
|
||||
export type SchemaResponse = {
|
||||
version: string;
|
||||
schema: ModuleSchemas;
|
||||
config: ModuleConfigs;
|
||||
permissions: string[];
|
||||
};
|
||||
|
||||
export class SystemController extends Controller {
|
||||
constructor(private readonly app: App) {
|
||||
|
||||
@@ -36,6 +36,7 @@ export type BaseProps = {
|
||||
size?: keyof typeof sizes;
|
||||
variant?: keyof typeof styles;
|
||||
labelClassName?: string;
|
||||
"data-testid"?: string;
|
||||
};
|
||||
|
||||
const Base = ({
|
||||
|
||||
8
app/src/ui/lib/config.ts
Normal file
8
app/src/ui/lib/config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const config = {};
|
||||
|
||||
export const testIds = {
|
||||
data: {
|
||||
btnCreateEntity: "data-btns-create-entity",
|
||||
},
|
||||
media: {},
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import { Link, isLinkActive } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { routes, useNavigate, useRouteNavigate } from "ui/lib/routes";
|
||||
import { testIds } from "ui/lib/config";
|
||||
|
||||
export function DataRoot({ children }) {
|
||||
// @todo: settings routes should be centralized
|
||||
@@ -269,6 +270,7 @@ export function DataEmpty() {
|
||||
}}
|
||||
primary={{
|
||||
children: "Create entity",
|
||||
"data-testid": testIds.data.btnCreateEntity,
|
||||
onClick: $data.modals.createEntity,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -139,6 +139,7 @@ function Adapters() {
|
||||
<Button
|
||||
key={i}
|
||||
onClick={() => ctx.select(i)}
|
||||
id={`adapter-${schema.properties.type.const}`}
|
||||
variant={ctx.selected === i ? "primary" : "outline"}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center justify-center gap-3 border",
|
||||
|
||||
18
app/vitest.config.ts
Normal file
18
app/vitest.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./__test__/vitest/setup.ts"],
|
||||
include: ["**/*.vi-test.ts"],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["node_modules/", "**/*.d.ts", "**/*.test.ts", "**/*.config.ts"],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user