Add self-contained e2e test harness with ephemeral PocketBase
Previously, 15 e2e tests were skipped because TEST_USER_EMAIL and TEST_USER_PASSWORD env vars weren't set. Now the test harness: - Starts a fresh PocketBase instance in /tmp on port 8091 - Creates admin user, collections, and API rules automatically - Seeds test user with period data for authenticated tests - Cleans up temp directory after tests complete Also fixes: - Override toggle tests now use checkbox role (not button) - Adds proper wait for OVERRIDES section before testing toggles - Suppresses document.cookie lint warning with explanation Test results: 64 e2e tests pass, 1014 unit tests pass Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,8 +47,11 @@ test.describe("calendar", () => {
|
||||
});
|
||||
|
||||
test("displays calendar page with heading", async ({ page }) => {
|
||||
// Check for calendar heading
|
||||
const heading = page.getByRole("heading", { name: /calendar/i });
|
||||
// Check for the main calendar heading (h1)
|
||||
const heading = page.getByRole("heading", {
|
||||
name: "Calendar",
|
||||
exact: true,
|
||||
});
|
||||
await expect(heading).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -81,45 +81,60 @@ test.describe("dashboard", () => {
|
||||
test("shows override toggles when user has period data", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Wait for dashboard data to load
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Override toggles should be visible if user has period data
|
||||
const overrideSection = page.getByRole("button", {
|
||||
name: /flare|stress|sleep|pms/i,
|
||||
const overrideCheckbox = page.getByRole("checkbox", {
|
||||
name: /flare mode|high stress|poor sleep|pms/i,
|
||||
});
|
||||
|
||||
// These may not be visible if user hasn't set up period date
|
||||
const hasOverrides = await overrideSection
|
||||
const hasOverrides = await overrideCheckbox
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
if (hasOverrides) {
|
||||
await expect(overrideSection.first()).toBeVisible();
|
||||
await expect(overrideCheckbox.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("can toggle override buttons", async ({ page }) => {
|
||||
// Find an override toggle button
|
||||
const toggleButton = page
|
||||
.getByRole("button", { name: /flare|stress|sleep|pms/i })
|
||||
test("can toggle override checkboxes", async ({ page }) => {
|
||||
// Wait for the OVERRIDES section to appear (indicates dashboard data loaded)
|
||||
const overridesHeading = page.getByRole("heading", { name: "OVERRIDES" });
|
||||
const hasOverridesSection = await overridesHeading
|
||||
.waitFor({ timeout: 10000 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!hasOverridesSection) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find an override toggle checkbox (Flare Mode, High Stress, etc.)
|
||||
const toggleCheckbox = page
|
||||
.getByRole("checkbox", {
|
||||
name: /flare mode|high stress|poor sleep|pms/i,
|
||||
})
|
||||
.first();
|
||||
|
||||
const hasToggle = await toggleButton.isVisible().catch(() => false);
|
||||
const hasToggle = await toggleCheckbox.isVisible().catch(() => false);
|
||||
|
||||
if (hasToggle) {
|
||||
// Get initial state
|
||||
const initialPressed = await toggleButton.getAttribute("aria-pressed");
|
||||
const initialChecked = await toggleCheckbox.isChecked();
|
||||
|
||||
// Click the toggle
|
||||
await toggleButton.click();
|
||||
await toggleCheckbox.click();
|
||||
|
||||
// Wait a moment for the API call
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Toggle should change state (or show error)
|
||||
const afterPressed = await toggleButton.getAttribute("aria-pressed");
|
||||
|
||||
// Either state changed or we should see some feedback
|
||||
expect(afterPressed !== initialPressed || true).toBe(true);
|
||||
// Toggle should change state
|
||||
const afterChecked = await toggleCheckbox.isChecked();
|
||||
expect(afterChecked).not.toBe(initialChecked);
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
|
||||
32
e2e/global-setup.ts
Normal file
32
e2e/global-setup.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// ABOUTME: Playwright global setup - starts PocketBase and sets test environment variables.
|
||||
// ABOUTME: Runs before all e2e tests to provide a fresh database with test data.
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { DEFAULT_CONFIG, start } from "./pocketbase-harness";
|
||||
|
||||
const STATE_FILE = path.join(__dirname, ".harness-state.json");
|
||||
|
||||
export default async function globalSetup(): Promise<void> {
|
||||
console.log("Starting PocketBase for e2e tests...");
|
||||
|
||||
const state = await start(DEFAULT_CONFIG);
|
||||
|
||||
// Save state for teardown
|
||||
fs.writeFileSync(
|
||||
STATE_FILE,
|
||||
JSON.stringify({
|
||||
dataDir: state.dataDir,
|
||||
url: state.url,
|
||||
pid: state.process.pid,
|
||||
}),
|
||||
);
|
||||
|
||||
// Set environment variables for the test process
|
||||
process.env.NEXT_PUBLIC_POCKETBASE_URL = state.url;
|
||||
process.env.POCKETBASE_URL = state.url;
|
||||
process.env.TEST_USER_EMAIL = DEFAULT_CONFIG.testUserEmail;
|
||||
process.env.TEST_USER_PASSWORD = DEFAULT_CONFIG.testUserPassword;
|
||||
|
||||
console.log(`PocketBase running at ${state.url}`);
|
||||
console.log(`Test user: ${DEFAULT_CONFIG.testUserEmail}`);
|
||||
}
|
||||
54
e2e/global-teardown.ts
Normal file
54
e2e/global-teardown.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
// ABOUTME: Playwright global teardown - stops PocketBase and cleans up temp data.
|
||||
// ABOUTME: Runs after all e2e tests to ensure clean shutdown.
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
const STATE_FILE = path.join(__dirname, ".harness-state.json");
|
||||
|
||||
interface HarnessStateFile {
|
||||
dataDir: string;
|
||||
url: string;
|
||||
pid: number;
|
||||
}
|
||||
|
||||
export default async function globalTeardown(): Promise<void> {
|
||||
console.log("Stopping PocketBase...");
|
||||
|
||||
// Read the saved state
|
||||
if (!fs.existsSync(STATE_FILE)) {
|
||||
console.log("No harness state file found, nothing to clean up.");
|
||||
return;
|
||||
}
|
||||
|
||||
const state: HarnessStateFile = JSON.parse(
|
||||
fs.readFileSync(STATE_FILE, "utf-8"),
|
||||
);
|
||||
|
||||
// Kill the PocketBase process
|
||||
if (state.pid) {
|
||||
try {
|
||||
process.kill(state.pid, "SIGTERM");
|
||||
// Wait for graceful shutdown
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
// Force kill if still running
|
||||
try {
|
||||
process.kill(state.pid, "SIGKILL");
|
||||
} catch {
|
||||
// Process already dead, which is fine
|
||||
}
|
||||
} catch {
|
||||
// Process might already be dead
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the temporary data directory
|
||||
if (state.dataDir && fs.existsSync(state.dataDir)) {
|
||||
fs.rmSync(state.dataDir, { recursive: true, force: true });
|
||||
console.log(`Cleaned up temp directory: ${state.dataDir}`);
|
||||
}
|
||||
|
||||
// Remove the state file
|
||||
fs.unlinkSync(STATE_FILE);
|
||||
|
||||
console.log("PocketBase stopped and cleaned up.");
|
||||
}
|
||||
@@ -73,18 +73,20 @@ test.describe("period logging", () => {
|
||||
test("period history shows table or empty state", async ({ page }) => {
|
||||
await page.goto("/period-history");
|
||||
|
||||
// Wait for loading to complete
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Look for either table or empty state message
|
||||
const table = page.getByRole("table");
|
||||
const emptyState = page.getByText(/no period|no data|start tracking/i);
|
||||
const emptyState = page.getByText("No period history found");
|
||||
const totalText = page.getByText(/\d+ periods/);
|
||||
|
||||
const hasTable = await table.isVisible().catch(() => false);
|
||||
const hasEmpty = await emptyState
|
||||
.first()
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const hasEmpty = await emptyState.isVisible().catch(() => false);
|
||||
const hasTotal = await totalText.isVisible().catch(() => false);
|
||||
|
||||
// Either should be present
|
||||
expect(hasTable || hasEmpty).toBe(true);
|
||||
// Either table, empty state, or total count should be present
|
||||
expect(hasTable || hasEmpty || hasTotal).toBe(true);
|
||||
});
|
||||
|
||||
test("period history shows average cycle length if data exists", async ({
|
||||
|
||||
140
e2e/pocketbase-harness.test.ts
Normal file
140
e2e/pocketbase-harness.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
// ABOUTME: Integration tests for the PocketBase e2e test harness.
|
||||
// ABOUTME: Verifies the harness can start, setup, and stop PocketBase instances.
|
||||
// @vitest-environment node
|
||||
|
||||
import PocketBase from "pocketbase";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
DEFAULT_CONFIG,
|
||||
getState,
|
||||
type HarnessState,
|
||||
start,
|
||||
stop,
|
||||
} from "./pocketbase-harness";
|
||||
|
||||
describe("pocketbase-harness", () => {
|
||||
describe("start/stop lifecycle", () => {
|
||||
let state: HarnessState;
|
||||
|
||||
beforeAll(async () => {
|
||||
state = await start();
|
||||
}, 60000);
|
||||
|
||||
afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
it("returns a valid harness state", () => {
|
||||
expect(state).toBeDefined();
|
||||
expect(state.url).toBe(`http://127.0.0.1:${DEFAULT_CONFIG.port}`);
|
||||
expect(state.dataDir).toContain("pocketbase-e2e-");
|
||||
expect(state.process).toBeDefined();
|
||||
});
|
||||
|
||||
it("getState returns the current state while running", () => {
|
||||
const currentState = getState();
|
||||
expect(currentState).toBe(state);
|
||||
});
|
||||
|
||||
it("PocketBase is accessible at the expected URL", async () => {
|
||||
const response = await fetch(`${state.url}/api/health`);
|
||||
expect(response.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("admin can authenticate", async () => {
|
||||
const pb = new PocketBase(state.url);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
const auth = await pb
|
||||
.collection("_superusers")
|
||||
.authWithPassword(
|
||||
DEFAULT_CONFIG.adminEmail,
|
||||
DEFAULT_CONFIG.adminPassword,
|
||||
);
|
||||
|
||||
expect(auth.token).toBeDefined();
|
||||
});
|
||||
|
||||
it("test user can authenticate", async () => {
|
||||
const pb = new PocketBase(state.url);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
const auth = await pb
|
||||
.collection("users")
|
||||
.authWithPassword(
|
||||
DEFAULT_CONFIG.testUserEmail,
|
||||
DEFAULT_CONFIG.testUserPassword,
|
||||
);
|
||||
|
||||
expect(auth.token).toBeDefined();
|
||||
expect(auth.record.email).toBe(DEFAULT_CONFIG.testUserEmail);
|
||||
});
|
||||
|
||||
it("test user has period data configured", async () => {
|
||||
const pb = new PocketBase(state.url);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
await pb
|
||||
.collection("users")
|
||||
.authWithPassword(
|
||||
DEFAULT_CONFIG.testUserEmail,
|
||||
DEFAULT_CONFIG.testUserPassword,
|
||||
);
|
||||
|
||||
const user = pb.authStore.record;
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.lastPeriodDate).toBeDefined();
|
||||
expect(user?.cycleLength).toBe(28);
|
||||
expect(user?.timezone).toBe("UTC");
|
||||
});
|
||||
|
||||
it("period_logs collection exists with test data", async () => {
|
||||
const pb = new PocketBase(state.url);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
await pb
|
||||
.collection("users")
|
||||
.authWithPassword(
|
||||
DEFAULT_CONFIG.testUserEmail,
|
||||
DEFAULT_CONFIG.testUserPassword,
|
||||
);
|
||||
|
||||
const userId = pb.authStore.record?.id;
|
||||
const logs = await pb
|
||||
.collection("period_logs")
|
||||
.getList(1, 10, { filter: `user="${userId}"` });
|
||||
|
||||
expect(logs.totalItems).toBeGreaterThan(0);
|
||||
expect(logs.items[0].startDate).toBeDefined();
|
||||
});
|
||||
|
||||
it("dailyLogs collection exists", async () => {
|
||||
const pb = new PocketBase(state.url);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
await pb
|
||||
.collection("_superusers")
|
||||
.authWithPassword(
|
||||
DEFAULT_CONFIG.adminEmail,
|
||||
DEFAULT_CONFIG.adminPassword,
|
||||
);
|
||||
|
||||
const collections = await pb.collections.getFullList();
|
||||
const collectionNames = collections.map((c) => c.name);
|
||||
|
||||
expect(collectionNames).toContain("period_logs");
|
||||
expect(collectionNames).toContain("dailyLogs");
|
||||
});
|
||||
});
|
||||
|
||||
describe("after stop", () => {
|
||||
it("getState returns null after stop", async () => {
|
||||
// Start and immediately stop
|
||||
await start({ ...DEFAULT_CONFIG, port: 8092 });
|
||||
await stop();
|
||||
|
||||
const state = getState();
|
||||
expect(state).toBeNull();
|
||||
}, 60000);
|
||||
});
|
||||
});
|
||||
308
e2e/pocketbase-harness.ts
Normal file
308
e2e/pocketbase-harness.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
// ABOUTME: PocketBase test harness for e2e tests - starts, configures, and stops PocketBase.
|
||||
// ABOUTME: Provides ephemeral PocketBase instances with test data for Playwright tests.
|
||||
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import PocketBase from "pocketbase";
|
||||
import {
|
||||
createCollection,
|
||||
getExistingCollectionNames,
|
||||
getMissingCollections,
|
||||
} from "../scripts/setup-db";
|
||||
|
||||
/**
|
||||
* Configuration for the test harness.
|
||||
*/
|
||||
export interface HarnessConfig {
|
||||
port: number;
|
||||
adminEmail: string;
|
||||
adminPassword: string;
|
||||
testUserEmail: string;
|
||||
testUserPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration for e2e tests.
|
||||
*/
|
||||
export const DEFAULT_CONFIG: HarnessConfig = {
|
||||
port: 8091,
|
||||
adminEmail: "admin@e2e-test.local",
|
||||
adminPassword: "admin-password-e2e-123",
|
||||
testUserEmail: "e2e-test@phaseflow.local",
|
||||
testUserPassword: "e2e-test-password-123",
|
||||
};
|
||||
|
||||
/**
|
||||
* State of a running PocketBase harness instance.
|
||||
*/
|
||||
export interface HarnessState {
|
||||
process: ChildProcess;
|
||||
dataDir: string;
|
||||
url: string;
|
||||
config: HarnessConfig;
|
||||
}
|
||||
|
||||
let currentState: HarnessState | null = null;
|
||||
|
||||
/**
|
||||
* Gets the URL for the PocketBase instance.
|
||||
*/
|
||||
function getPocketBaseUrl(port: number): string {
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary directory for PocketBase data.
|
||||
*/
|
||||
function createTempDataDir(): string {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pocketbase-e2e-"));
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for PocketBase to be ready by polling the health endpoint.
|
||||
*/
|
||||
async function waitForReady(url: string, timeoutMs = 30000): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
const healthUrl = `${url}/api/health`;
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
try {
|
||||
const response = await fetch(healthUrl);
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Server not ready yet, continue polling
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
throw new Error(`PocketBase did not become ready within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the admin superuser using the PocketBase CLI.
|
||||
*/
|
||||
function createAdminUser(
|
||||
dataDir: string,
|
||||
email: string,
|
||||
password: string,
|
||||
): void {
|
||||
execSync(
|
||||
`pocketbase superuser upsert ${email} ${password} --dir=${dataDir}`,
|
||||
{
|
||||
stdio: "pipe",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds custom fields to the users collection.
|
||||
*/
|
||||
async function addUserFields(pb: PocketBase): Promise<void> {
|
||||
const usersCollection = await pb.collections.getOne("users");
|
||||
|
||||
// Define the custom user fields
|
||||
const customFields = [
|
||||
{ name: "garminConnected", type: "bool" },
|
||||
{ name: "garminOauth1Token", type: "text" },
|
||||
{ name: "garminOauth2Token", type: "text" },
|
||||
{ name: "garminTokenExpiresAt", type: "date" },
|
||||
{ name: "calendarToken", type: "text" },
|
||||
{ name: "lastPeriodDate", type: "date" },
|
||||
{ name: "cycleLength", type: "number" },
|
||||
{ name: "notificationTime", type: "text" },
|
||||
{ name: "timezone", type: "text" },
|
||||
{ name: "activeOverrides", type: "json" },
|
||||
];
|
||||
|
||||
// Get existing field names
|
||||
const existingFieldNames = new Set(
|
||||
(usersCollection.fields || []).map((f: { name: string }) => f.name),
|
||||
);
|
||||
|
||||
// Filter to only new fields
|
||||
const newFields = customFields.filter((f) => !existingFieldNames.has(f.name));
|
||||
|
||||
if (newFields.length > 0) {
|
||||
// Combine existing fields with new ones
|
||||
const allFields = [...(usersCollection.fields || []), ...newFields];
|
||||
|
||||
await pb.collections.update(usersCollection.id, {
|
||||
fields: allFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up API rules for collections to allow user access.
|
||||
*/
|
||||
async function setupApiRules(pb: PocketBase): Promise<void> {
|
||||
// Allow users to read/write their own period_logs
|
||||
const periodLogs = await pb.collections.getOne("period_logs");
|
||||
await pb.collections.update(periodLogs.id, {
|
||||
listRule: "user = @request.auth.id",
|
||||
viewRule: "user = @request.auth.id",
|
||||
createRule: "user = @request.auth.id",
|
||||
updateRule: "user = @request.auth.id",
|
||||
deleteRule: "user = @request.auth.id",
|
||||
});
|
||||
|
||||
// Allow users to read/write their own dailyLogs
|
||||
const dailyLogs = await pb.collections.getOne("dailyLogs");
|
||||
await pb.collections.update(dailyLogs.id, {
|
||||
listRule: "user = @request.auth.id",
|
||||
viewRule: "user = @request.auth.id",
|
||||
createRule: "user = @request.auth.id",
|
||||
updateRule: "user = @request.auth.id",
|
||||
deleteRule: "user = @request.auth.id",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the database collections using the SDK.
|
||||
*/
|
||||
async function setupCollections(pb: PocketBase): Promise<void> {
|
||||
// Add custom fields to users collection
|
||||
await addUserFields(pb);
|
||||
|
||||
// Create period_logs and dailyLogs collections
|
||||
const existingNames = await getExistingCollectionNames(pb);
|
||||
const missing = getMissingCollections(existingNames);
|
||||
|
||||
for (const collection of missing) {
|
||||
await createCollection(pb, collection);
|
||||
}
|
||||
|
||||
// Set up API rules
|
||||
await setupApiRules(pb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the test user with period data.
|
||||
*/
|
||||
async function createTestUser(
|
||||
pb: PocketBase,
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<string> {
|
||||
// Calculate date 14 days ago for mid-cycle test data
|
||||
const fourteenDaysAgo = new Date();
|
||||
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
|
||||
const lastPeriodDate = fourteenDaysAgo.toISOString().split("T")[0];
|
||||
|
||||
// Create the test user
|
||||
const user = await pb.collection("users").create({
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true,
|
||||
verified: true,
|
||||
lastPeriodDate,
|
||||
cycleLength: 28,
|
||||
timezone: "UTC",
|
||||
});
|
||||
|
||||
// Create a period log entry
|
||||
await pb.collection("period_logs").create({
|
||||
user: user.id,
|
||||
startDate: lastPeriodDate,
|
||||
});
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a fresh PocketBase instance for e2e testing.
|
||||
*/
|
||||
export async function start(
|
||||
config: HarnessConfig = DEFAULT_CONFIG,
|
||||
): Promise<HarnessState> {
|
||||
if (currentState) {
|
||||
throw new Error(
|
||||
"PocketBase harness is already running. Call stop() first.",
|
||||
);
|
||||
}
|
||||
|
||||
const dataDir = createTempDataDir();
|
||||
const url = getPocketBaseUrl(config.port);
|
||||
|
||||
// Start PocketBase process
|
||||
const pbProcess = spawn(
|
||||
"pocketbase",
|
||||
["serve", `--dir=${dataDir}`, `--http=127.0.0.1:${config.port}`],
|
||||
{
|
||||
stdio: "pipe",
|
||||
detached: false,
|
||||
},
|
||||
);
|
||||
|
||||
// Handle process errors
|
||||
pbProcess.on("error", (err) => {
|
||||
console.error("PocketBase process error:", err);
|
||||
});
|
||||
|
||||
// Wait for PocketBase to be ready
|
||||
await waitForReady(url);
|
||||
|
||||
// Create admin user via CLI
|
||||
createAdminUser(dataDir, config.adminEmail, config.adminPassword);
|
||||
|
||||
// Connect to PocketBase as admin
|
||||
const pb = new PocketBase(url);
|
||||
pb.autoCancellation(false);
|
||||
await pb
|
||||
.collection("_superusers")
|
||||
.authWithPassword(config.adminEmail, config.adminPassword);
|
||||
|
||||
// Set up collections
|
||||
await setupCollections(pb);
|
||||
|
||||
// Create test user with period data
|
||||
await createTestUser(pb, config.testUserEmail, config.testUserPassword);
|
||||
|
||||
currentState = {
|
||||
process: pbProcess,
|
||||
dataDir,
|
||||
url,
|
||||
config,
|
||||
};
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the running PocketBase instance and cleans up.
|
||||
*/
|
||||
export async function stop(): Promise<void> {
|
||||
if (!currentState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { process: pbProcess, dataDir } = currentState;
|
||||
|
||||
// Kill the PocketBase process
|
||||
pbProcess.kill("SIGTERM");
|
||||
|
||||
// Wait a moment for graceful shutdown
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Force kill if still running
|
||||
if (!pbProcess.killed) {
|
||||
pbProcess.kill("SIGKILL");
|
||||
}
|
||||
|
||||
// Clean up the temporary data directory
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
|
||||
currentState = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current harness state if running.
|
||||
*/
|
||||
export function getState(): HarnessState | null {
|
||||
return currentState;
|
||||
}
|
||||
Reference in New Issue
Block a user