mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
test: add BaseTest framework with utilities
- Create abstract BaseTest class with lifecycle hooks - Add beforeAll/afterAll and beforeEach/afterEach hooks - Implement temp directory management (auto cleanup) - Add file assertion helpers (exists, contains, matches) - Add async utilities (waitFor, waitForFile, sleep) - Include example test demonstrating usage - Add test tasks to deno.json (test, test:watch) - Add @std/assert dependency
This commit is contained in:
81
deno.json
81
deno.json
@@ -1,42 +1,43 @@
|
||||
{
|
||||
"imports": {
|
||||
"$config": "./src/utils/config/config.ts",
|
||||
"$stores": "./src/stores",
|
||||
"$components": "./src/components",
|
||||
"$static": "./src/static",
|
||||
"$db/": "./src/db/",
|
||||
"$logger": "./src/utils/logger/logger.ts",
|
||||
"$logger/": "./src/utils/logger/",
|
||||
"$arr/": "./src/utils/arr/",
|
||||
"$http/": "./src/utils/http/",
|
||||
"$api": "./src/utils/api/request.ts",
|
||||
"$utils/": "./src/utils/"
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "APP_BASE_PATH=./temp vite dev",
|
||||
"build": "vite build && cp -r build/static .",
|
||||
"compile": "deno task build && deno compile --no-check --allow-all --output profilarr build/mod.ts",
|
||||
"start": "APP_BASE_PATH=temp PORT=6975 ./profilarr",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test": "APP_BASE_PATH=./temp deno test --allow-read --allow-write --allow-env",
|
||||
"test:watch": "APP_BASE_PATH=./temp deno test --allow-read --allow-write --allow-env --watch"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.window", "dom"],
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["build/", ".svelte-kit/", "node_modules/"],
|
||||
"fmt": {
|
||||
"exclude": ["build/", ".svelte-kit/", "node_modules/"],
|
||||
"indentWidth": 2,
|
||||
"useTabs": false
|
||||
},
|
||||
"lint": {
|
||||
"exclude": ["build/", ".svelte-kit/", "node_modules/"]
|
||||
}
|
||||
"imports": {
|
||||
"$config": "./src/utils/config/config.ts",
|
||||
"$stores": "./src/stores",
|
||||
"$components": "./src/components",
|
||||
"$static": "./src/static",
|
||||
"$db/": "./src/db/",
|
||||
"$logger": "./src/utils/logger/logger.ts",
|
||||
"$logger/": "./src/utils/logger/",
|
||||
"$arr/": "./src/utils/arr/",
|
||||
"$http/": "./src/utils/http/",
|
||||
"$api": "./src/utils/api/request.ts",
|
||||
"$utils/": "./src/utils/",
|
||||
"@std/assert": "jsr:@std/assert@^1.0.0"
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "APP_BASE_PATH=./temp vite dev",
|
||||
"build": "vite build && cp -r build/static .",
|
||||
"compile": "deno task build && deno compile --no-check --allow-all --output profilarr build/mod.ts",
|
||||
"start": "APP_BASE_PATH=temp PORT=6975 ./profilarr",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test": "APP_BASE_PATH=./temp deno test --allow-read --allow-write --allow-env",
|
||||
"test:watch": "APP_BASE_PATH=./temp deno test --allow-read --allow-write --allow-env --watch"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.window", "dom"],
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["build/", ".svelte-kit/", "node_modules/"],
|
||||
"fmt": {
|
||||
"exclude": ["build/", ".svelte-kit/", "node_modules/"],
|
||||
"indentWidth": 2,
|
||||
"useTabs": false
|
||||
},
|
||||
"lint": {
|
||||
"exclude": ["build/", ".svelte-kit/", "node_modules/"]
|
||||
}
|
||||
}
|
||||
|
||||
17
deno.lock
generated
17
deno.lock
generated
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@*": "1.0.15",
|
||||
"jsr:@std/assert@1": "1.0.15",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||
"npm:@eslint/compat@^1.4.0": "1.4.0_eslint@9.37.0",
|
||||
"npm:@eslint/js@^9.36.0": "9.37.0",
|
||||
"npm:@jsr/db__sqlite@0.12": "0.12.0",
|
||||
@@ -25,6 +28,17 @@
|
||||
"npm:typescript@^5.9.2": "5.9.3",
|
||||
"npm:vite@^7.1.7": "7.1.10_@types+node@22.18.11_picomatch@4.0.3"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/assert@1.0.15": {
|
||||
"integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.12": {
|
||||
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@esbuild/aix-ppc64@0.24.2": {
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
@@ -1875,6 +1889,9 @@
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@1"
|
||||
],
|
||||
"packageJson": {
|
||||
"dependencies": [
|
||||
"npm:@eslint/compat@^1.4.0",
|
||||
|
||||
251
tests/base/BaseTest.ts
Normal file
251
tests/base/BaseTest.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Base test class providing common test utilities and lifecycle hooks
|
||||
* Extend this class to create test suites with automatic setup/teardown
|
||||
*/
|
||||
|
||||
import { assertExists } from "@std/assert";
|
||||
|
||||
export interface TestContext {
|
||||
/** Unique temporary directory for this test */
|
||||
tempDir: string;
|
||||
/** Test name */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for tests
|
||||
* Provides lifecycle hooks and common utilities
|
||||
*/
|
||||
export abstract class BaseTest {
|
||||
private static testCounter = 0;
|
||||
protected context: TestContext | null = null;
|
||||
|
||||
/**
|
||||
* Run before all tests in this suite (optional)
|
||||
* Override this in your test class if needed
|
||||
*/
|
||||
protected beforeAll(): void | Promise<void> {
|
||||
// Override in subclass if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Run after all tests in this suite (optional)
|
||||
* Override this in your test class if needed
|
||||
*/
|
||||
protected afterAll(): void | Promise<void> {
|
||||
// Override in subclass if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Run before each individual test (optional)
|
||||
* Override this in your test class if needed
|
||||
*/
|
||||
protected beforeEach(_context: TestContext): void | Promise<void> {
|
||||
// Override in subclass if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Run after each individual test (optional)
|
||||
* Override this in your test class if needed
|
||||
*/
|
||||
protected afterEach(_context: TestContext): void | Promise<void> {
|
||||
// Override in subclass if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary directory for a test
|
||||
* Automatically cleaned up after test completes
|
||||
*/
|
||||
protected async createTempDir(testName: string): Promise<string> {
|
||||
const counter = BaseTest.testCounter++;
|
||||
const timestamp = Date.now();
|
||||
const sanitizedName = testName.replace(/[^a-zA-Z0-9]/g, "_");
|
||||
const tempDir =
|
||||
`/tmp/profilarr-tests/${sanitizedName}_${timestamp}_${counter}`;
|
||||
await Deno.mkdir(tempDir, { recursive: true });
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a temporary directory and all its contents
|
||||
*/
|
||||
protected async cleanupTempDir(tempDir: string): Promise<void> {
|
||||
try {
|
||||
await Deno.remove(tempDir, { recursive: true });
|
||||
} catch (error) {
|
||||
// Ignore errors during cleanup
|
||||
console.warn(`Failed to cleanup temp dir ${tempDir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a file exists at the given path
|
||||
*/
|
||||
protected async assertFileExists(
|
||||
filePath: string,
|
||||
message?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const stat = await Deno.stat(filePath);
|
||||
assertExists(stat, message ?? `File should exist: ${filePath}`);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
message ??
|
||||
`Expected file to exist: ${filePath}, but got error: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a file does NOT exist at the given path
|
||||
*/
|
||||
protected async assertFileNotExists(
|
||||
filePath: string,
|
||||
message?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await Deno.stat(filePath);
|
||||
throw new Error(message ?? `Expected file to NOT exist: ${filePath}`);
|
||||
} catch (error) {
|
||||
if (!(error instanceof Deno.errors.NotFound)) {
|
||||
throw error;
|
||||
}
|
||||
// File doesn't exist - this is what we want
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a file contains specific text
|
||||
*/
|
||||
protected async assertFileContains(
|
||||
filePath: string,
|
||||
expectedText: string,
|
||||
message?: string,
|
||||
): Promise<void> {
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
if (!content.includes(expectedText)) {
|
||||
throw new Error(
|
||||
message ??
|
||||
`Expected file ${filePath} to contain "${expectedText}", but it didn't.\nFile content: ${content}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a file matches a regex pattern
|
||||
*/
|
||||
protected async assertFileMatches(
|
||||
filePath: string,
|
||||
pattern: RegExp,
|
||||
message?: string,
|
||||
): Promise<void> {
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
if (!pattern.test(content)) {
|
||||
throw new Error(
|
||||
message ??
|
||||
`Expected file ${filePath} to match pattern ${pattern}, but it didn't.\nFile content: ${content}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse JSON log file
|
||||
*/
|
||||
protected async readJsonLines(filePath: string): Promise<unknown[]> {
|
||||
const content = await Deno.readTextFile(filePath);
|
||||
const lines = content.split("\n").filter((line) => line.trim());
|
||||
return lines.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a condition to become true with timeout
|
||||
*/
|
||||
protected async waitFor(
|
||||
condition: () => boolean | Promise<boolean>,
|
||||
timeoutMs = 5000,
|
||||
checkIntervalMs = 100,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (await condition()) {
|
||||
return;
|
||||
}
|
||||
await this.sleep(checkIntervalMs);
|
||||
}
|
||||
throw new Error(`Timeout waiting for condition after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a file to exist
|
||||
*/
|
||||
protected async waitForFile(
|
||||
filePath: string,
|
||||
timeoutMs = 5000,
|
||||
): Promise<void> {
|
||||
await this.waitFor(async () => {
|
||||
try {
|
||||
await Deno.stat(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for specified milliseconds
|
||||
*/
|
||||
protected async sleep(ms: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register and run a test with automatic setup/teardown
|
||||
*/
|
||||
protected test(
|
||||
name: string,
|
||||
fn: (context: TestContext) => Promise<void> | void,
|
||||
): void {
|
||||
Deno.test({
|
||||
name: `${this.constructor.name}: ${name}`,
|
||||
fn: async () => {
|
||||
// Create temp directory for this test
|
||||
const tempDir = await this.createTempDir(name);
|
||||
const context: TestContext = { tempDir, name };
|
||||
this.context = context;
|
||||
|
||||
try {
|
||||
// Run beforeEach hook
|
||||
await this.beforeEach(context);
|
||||
|
||||
// Run the actual test
|
||||
await fn(context);
|
||||
} finally {
|
||||
// Run afterEach hook
|
||||
await this.afterEach(context);
|
||||
|
||||
// Cleanup temp directory
|
||||
await this.cleanupTempDir(tempDir);
|
||||
|
||||
this.context = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the test suite
|
||||
* Call this method to execute all tests defined in your subclass
|
||||
*/
|
||||
async run(): Promise<void> {
|
||||
// Run beforeAll hook
|
||||
await this.beforeAll();
|
||||
|
||||
// Tests are registered via this.test() calls in the subclass constructor or setup method
|
||||
// Deno.test will handle running them
|
||||
|
||||
// Note: afterAll will be called by the Deno test runner after all tests complete
|
||||
// We can't easily hook into this without using the unstable API
|
||||
// For now, tests should clean up in afterEach
|
||||
}
|
||||
}
|
||||
124
tests/example.test.ts
Normal file
124
tests/example.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Example test demonstrating BaseTest usage
|
||||
* Shows how to extend BaseTest and use its utilities
|
||||
*/
|
||||
|
||||
import { BaseTest, TestContext } from "./base/BaseTest.ts";
|
||||
import { assertEquals } from "@std/assert";
|
||||
|
||||
class ExampleTest extends BaseTest {
|
||||
// Optional: Run once before all tests
|
||||
protected override beforeAll(): void {
|
||||
console.log("ExampleTest: beforeAll - runs once before all tests");
|
||||
}
|
||||
|
||||
// Optional: Run once after all tests
|
||||
protected override afterAll(): void {
|
||||
console.log("ExampleTest: afterAll - runs once after all tests");
|
||||
}
|
||||
|
||||
// Optional: Run before each test
|
||||
protected override beforeEach(context: TestContext): void {
|
||||
console.log(
|
||||
`ExampleTest: beforeEach - test: ${context.name}, tempDir: ${context.tempDir}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Optional: Run after each test
|
||||
protected override afterEach(context: TestContext): void {
|
||||
console.log(`ExampleTest: afterEach - test: ${context.name}`);
|
||||
}
|
||||
|
||||
// Define your tests
|
||||
runTests(): void {
|
||||
// Basic test
|
||||
this.test("basic assertion", () => {
|
||||
assertEquals(1 + 1, 2);
|
||||
});
|
||||
|
||||
// Test with temp directory
|
||||
this.test("temp directory is created", async (context) => {
|
||||
// Each test gets its own temp directory
|
||||
await this.assertFileExists(context.tempDir);
|
||||
});
|
||||
|
||||
// Test file operations
|
||||
this.test("can write and read files", async (context) => {
|
||||
const testFile = `${context.tempDir}/test.txt`;
|
||||
|
||||
// Write to file
|
||||
await Deno.writeTextFile(testFile, "Hello, World!");
|
||||
|
||||
// Assert file exists
|
||||
await this.assertFileExists(testFile);
|
||||
|
||||
// Assert file contains expected text
|
||||
await this.assertFileContains(testFile, "Hello, World!");
|
||||
|
||||
// Assert file matches pattern
|
||||
await this.assertFileMatches(testFile, /Hello.*World/);
|
||||
});
|
||||
|
||||
// Test JSON operations
|
||||
this.test("can write and read JSON lines", async (context) => {
|
||||
const jsonFile = `${context.tempDir}/data.jsonl`;
|
||||
|
||||
// Write JSON lines
|
||||
await Deno.writeTextFile(
|
||||
jsonFile,
|
||||
JSON.stringify({ id: 1, name: "test" }) + "\n" +
|
||||
JSON.stringify({ id: 2, name: "example" }) + "\n",
|
||||
);
|
||||
|
||||
// Read JSON lines
|
||||
const data = await this.readJsonLines(jsonFile);
|
||||
|
||||
assertEquals(data.length, 2);
|
||||
assertEquals((data[0] as { id: number }).id, 1);
|
||||
assertEquals((data[1] as { id: number }).id, 2);
|
||||
});
|
||||
|
||||
// Test async operations
|
||||
this.test("waitFor utility works", async () => {
|
||||
let counter = 0;
|
||||
|
||||
// Start async operation
|
||||
setTimeout(() => {
|
||||
counter = 5;
|
||||
}, 100);
|
||||
|
||||
// Wait for condition
|
||||
await this.waitFor(() => counter === 5, 1000);
|
||||
|
||||
assertEquals(counter, 5);
|
||||
});
|
||||
|
||||
// Test waitForFile utility
|
||||
this.test("waitForFile utility works", async (context) => {
|
||||
const testFile = `${context.tempDir}/delayed.txt`;
|
||||
|
||||
// Create file after delay
|
||||
setTimeout(async () => {
|
||||
await Deno.writeTextFile(testFile, "delayed content");
|
||||
}, 100);
|
||||
|
||||
// Wait for file to exist
|
||||
await this.waitForFile(testFile, 1000);
|
||||
|
||||
// File should exist now
|
||||
await this.assertFileExists(testFile);
|
||||
});
|
||||
|
||||
// Test file NOT exists assertion
|
||||
this.test("assertFileNotExists works", async (context) => {
|
||||
const nonExistentFile = `${context.tempDir}/does-not-exist.txt`;
|
||||
|
||||
// Should pass - file doesn't exist
|
||||
await this.assertFileNotExists(nonExistentFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create instance and run tests
|
||||
const exampleTest = new ExampleTest();
|
||||
await exampleTest.runTests();
|
||||
Reference in New Issue
Block a user