101f595bac
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>
122 lines
3.8 KiB
TypeScript
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 },
|
|
);
|
|
}
|