Files
DndGamePlayer/e2e/fixtures/electron.ts
T
Ivan Fontosh 101f595bac feat(players): app Players library and circular NPC tokens on scenes
Add userData players/teams, scene npcTokens with hex-inscribed sizing, session scale synced to presentation, and Playwright e2e coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 13:38:35 +08:00

122 lines
3.8 KiB
TypeScript

import fs from 'node:fs/promises';
import { createRequire } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { test as base, expect, _electron as electron, type ElectronApplication, type Page } from '@playwright/test';
const require = createRequire(import.meta.url);
const electronExecutable = require('electron') as string;
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const mainEntry = path.join(root, 'dist/main/index.cjs');
const fixturePng = path.join(root, 'e2e/fixtures/sample.png');
const SAMPLE_PNG_B64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
export type ElectronFixtures = {
electronApp: ElectronApplication;
editorWindow: Page;
userDataDir: string;
sampleImagePath: string;
};
async function ensureBuilt() {
try {
await fs.access(mainEntry);
const editorHtml = await fs.readFile(path.join(root, 'dist/renderer/editor.html'), 'utf8');
// Production Vite build uses relative asset URLs (required for Electron file://).
if (editorHtml.includes('src="/assets/') || editorHtml.includes("src='/assets/")) {
throw new Error('dev-base');
}
} catch (err) {
const reason = err instanceof Error && err.message === 'dev-base' ? 'dev-base' : 'missing';
throw new Error(
reason === 'dev-base'
? 'dist/renderer was built with Vite base "/". Run "npm run build" (production) before e2e.'
: `Missing ${mainEntry}. Run "npm run build" (or "npm run test:e2e:build") before e2e.`,
);
}
}
export const test = base.extend<ElectronFixtures>({
userDataDir: async ({}, use) => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'dnd-e2e-'));
await use(dir);
await fs.rm(dir, { recursive: true, force: true }).catch(() => undefined);
},
sampleImagePath: async ({}, use) => {
await fs.mkdir(path.dirname(fixturePng), { recursive: true });
await fs.writeFile(fixturePng, Buffer.from(SAMPLE_PNG_B64, 'base64'));
await use(fixturePng);
},
electronApp: async ({ userDataDir }, use) => {
await ensureBuilt();
const app = await electron.launch({
executablePath: electronExecutable,
// Launch the package root so app.getAppPath() is the repo (not dist/main).
args: ['.', `--user-data-dir=${userDataDir}`],
cwd: root,
env: {
...process.env,
NODE_ENV: 'production',
DND_SKIP_LICENSE: '1',
DND_SKIP_BOOT: '1',
ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',
},
});
await use(app);
try {
await app.evaluate(({ app: electronApp }) => {
electronApp.exit(0);
});
} catch {
/* ignore */
}
try {
await Promise.race([
app.close(),
new Promise<void>((resolve) => {
setTimeout(resolve, 3_000);
}),
]);
} catch {
/* ignore */
}
},
editorWindow: async ({ electronApp }, use) => {
const page = await electronApp.firstWindow();
await page.waitForLoadState('domcontentloaded');
await expect(page.getByTestId('players-header-btn')).toBeVisible({ timeout: 60_000 });
await use(page);
},
});
export { expect };
/** Stub native open-dialog to return a fixed image path. */
export async function stubOpenImageDialog(app: ElectronApplication, filePath: string) {
await app.evaluate(async ({ dialog }, chosen) => {
dialog.showOpenDialog = async () => ({ canceled: false, filePaths: [chosen] });
}, filePath);
}
export async function invokeInRenderer<T>(
page: Page,
channel: string,
payload: unknown = {},
): Promise<T> {
return page.evaluate(
async ({ channel: ch, payload: pl }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (window as any).dnd.invoke(ch, pl);
},
{ channel, payload },
);
}