@rayners/foundry-test-utils
Version:
Shared testing utilities and mocks for FoundryVTT modules
458 lines (457 loc) • 14.1 kB
JavaScript
/**
* Comprehensive Foundry VTT Mock Setup
*
* This file provides a complete mock environment for Foundry VTT testing.
* It can be shared between multiple projects that need Foundry mocks.
*
* Usage:
* ```typescript
* import './foundry-mocks';
* // Or import specific parts:
* import { setupFoundryMocks, mockFoundryDocuments } from './foundry-mocks';
* ```
*/
/// <reference types="@rayners/foundry-dev-tools/types" />
import { vi } from 'vitest';
// ============================================================================
// MOCK FACTORIES
// ============================================================================
export function createMockScene(options = {}) {
const regions = options.regions || new Map();
// Add filter method to regions map to match Foundry's Collection interface
regions.filter = function (callback) {
const results = [];
for (const [id, region] of this.entries()) {
if (callback(region)) {
results.push(region);
}
}
return results;
};
return {
id: options.id || 'test-scene',
name: options.name || 'Test Scene',
width: options.width || 4000,
height: options.height || 3000,
regions: regions,
getFlag: vi.fn(),
setFlag: vi.fn(),
unsetFlag: vi.fn(),
createEmbeddedDocuments: vi.fn().mockResolvedValue([]),
deleteEmbeddedDocuments: vi.fn().mockResolvedValue([]),
grid: {
units: 'feet',
type: 1, // SQUARE
...(options.grid || {})
}
};
}
export function createMockRegion(options = {}) {
return {
id: options.id || 'test-region',
name: options.name || 'Test Region',
shapes: options.shapes || [],
flags: options.flags || {},
getFlag: vi.fn().mockImplementation((scope, key) => {
return options.flags?.[scope]?.[key];
}),
setFlag: vi.fn(),
unsetFlag: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
testPoint: vi.fn().mockReturnValue(true)
};
}
export function createMockActor(options = {}) {
return {
id: options.id || 'test-actor',
name: options.name || 'Test Actor',
type: options.type || 'character',
uuid: options.uuid || `Actor.${options.id || 'test-actor'}`,
system: options.system || {},
items: options.items || [],
flags: options.flags || {},
getFlag: vi.fn(),
setFlag: vi.fn(),
unsetFlag: vi.fn(),
update: vi.fn(),
delete: vi.fn()
};
}
export function createMockUser(options = {}) {
return {
id: options.id || 'test-user',
isGM: options.isGM ?? false,
getFlag: vi.fn(),
setFlag: vi.fn(),
unsetFlag: vi.fn()
};
}
export function createMockRollTable(options = {}) {
return {
id: options.id || 'test-table',
name: options.name || 'Test Table',
folder: options.folder || null,
results: options.results || [],
flags: options.flags || {},
roll: vi.fn().mockResolvedValue({
results: [{ text: 'Test Result', name: 'Test Result' }],
total: 1
}),
getFlag: vi.fn(),
setFlag: vi.fn()
};
}
export function createMockFolder(options = {}) {
return {
id: options.id || 'test-folder',
name: options.name || 'Test Folder',
type: options.type || 'RollTable',
color: options.color || '#000000',
description: options.description || 'Test folder'
};
}
// ============================================================================
// FOUNDRY DOCUMENT CLASSES
// ============================================================================
export class MockActorClass {
static async create(data) {
return createMockActor(data);
}
static async createDocuments(data) {
return data.map(d => createMockActor(d));
}
}
export class MockRollTableClass {
static async create(data) {
return createMockRollTable(data);
}
static async createDocuments(data) {
return data.map(d => createMockRollTable(d));
}
}
export class MockFolderClass {
static async create(data) {
return createMockFolder(data);
}
static async createDocuments(data) {
return data.map(d => createMockFolder(d));
}
}
export class MockDialogClass {
constructor(options = {}) {
this.options = options;
}
static async confirm(options = {}) {
return options.defaultYes !== false;
}
static async prompt(options = {}) {
return options.defaultValue || null;
}
render(force) {
// Mock render
}
close() {
// Mock close
}
}
// ============================================================================
// FOUNDRY GLOBALS SETUP
// ============================================================================
export function setupFoundryGlobals() {
// Foundry utility functions
globalThis.foundry = {
abstract: {
TypeDataModel: class MockTypeDataModel {
constructor(data = {}) {
Object.assign(this, data);
}
prepareDerivedData() { }
}
},
data: {
fields: {
HTMLField: class {
constructor(options = {}) {
this.options = options;
}
},
StringField: class {
constructor(options = {}) {
this.options = options;
}
},
NumberField: class {
constructor(options = {}) {
this.options = options;
if (typeof options.initial === 'function') {
this.initial = options.initial();
}
else {
this.initial = options.initial || 0;
}
}
},
BooleanField: class {
constructor(options = {}) {
this.options = options;
this.initial = options.initial || false;
}
},
ObjectField: class {
constructor(options = {}) {
this.options = options;
}
},
SchemaField: class {
constructor(schema = {}) {
this.schema = schema;
}
},
ArrayField: class {
constructor(element) {
this.element = element;
}
},
DocumentIdField: class {
constructor(options = {}) {
this.options = options;
}
},
FilePathField: class {
constructor(options = {}) {
this.options = options;
}
}
}
},
utils: {
mergeObject: vi.fn((original, other, options = {}) => ({ ...original, ...other })),
duplicate: vi.fn(obj => JSON.parse(JSON.stringify(obj))),
setProperty: vi.fn(),
getProperty: vi.fn(),
hasProperty: vi.fn(),
expandObject: vi.fn(),
flattenObject: vi.fn(),
isNewerVersion: vi.fn(),
randomID: vi.fn(() => Math.random().toString(36).substr(2, 9))
},
documents: {
BaseRegion: class MockBaseRegion {
constructor(data = {}) {
Object.assign(this, data);
}
static defineSchema() {
return {};
}
}
},
canvas: {
layers: {
RegionLayer: class MockRegionLayer {
constructor(options = {}) {
this.options = options;
}
activate() { }
deactivate() { }
draw() { }
tearDown() { }
}
}
}
};
const g = globalThis;
// Template functions
g.loadTemplates = vi.fn().mockResolvedValue({});
g.renderTemplate = vi.fn().mockResolvedValue('<div>Mock Template</div>');
g.getTemplate = vi.fn().mockResolvedValue(() => '<div>Mock Template</div>');
// Document lookup functions
g.fromUuid = vi.fn();
g.fromUuidSync = vi.fn();
// Text editor
g.TextEditor = {
enrichHTML: vi.fn(content => content)
};
// Handlebars
g.Handlebars = {
registerHelper: vi.fn(),
registerPartial: vi.fn()
};
// Hooks system
g.Hooks = {
on: vi.fn(),
once: vi.fn(),
off: vi.fn(),
call: vi.fn(),
callAll: vi.fn()
};
// PIXI Graphics (for canvas-based tests)
g.PIXI = {
Graphics: vi.fn(() => ({
beginFill: vi.fn(),
drawPolygon: vi.fn(),
endFill: vi.fn(),
clear: vi.fn(),
destroy: vi.fn()
})),
Point: vi.fn((x, y) => ({ x, y }))
};
// Canvas Layer base class
g.CanvasLayer = class MockCanvasLayer {
constructor(options = {}) {
this.options = options;
this.name = options.name || 'mock';
}
static get layerOptions() {
return {};
}
activate() { }
deactivate() { }
draw() { }
tearDown() { }
};
// CONST object
g.CONST = {
KEYBINDING_SCOPES: {
GLOBAL: 'global',
CLIENT: 'client'
}
};
}
export function setupFoundryDocuments() {
const g = globalThis;
// Document classes
g.Actor = MockActorClass;
g.RollTable = MockRollTableClass;
g.Folder = MockFolderClass;
g.Dialog = MockDialogClass;
// Sheet classes
g.ActorSheet = class MockActorSheet {
};
g.Application = class MockApplication {
};
g.FormApplication = class MockFormApplication {
};
// Other document classes
g.ChatMessage = class MockChatMessage {
static async create() { }
};
// Roll class
g.Roll = class MockRoll {
constructor(formula) {
this.formula = formula;
this.total = 10; // Default total
}
async evaluate() {
return this;
}
};
}
export function setupFoundryGame(options = {}) {
const mockUser = createMockUser(options.user);
const mockScenes = options.scenes || [createMockScene()];
globalThis.game = {
user: mockUser,
userId: mockUser.id,
users: new Map([[mockUser.id, mockUser]]),
actors: new Map(),
scenes: new Map(mockScenes.map(s => [s.id, s])),
tables: new Map(),
folders: new Map(),
modules: new Map(),
settings: {
get: vi.fn(),
set: vi.fn(),
register: vi.fn()
},
i18n: {
localize: vi.fn((key) => key),
format: vi.fn((key, data) => key)
},
system: {
id: options.systemId || 'test-system',
title: 'Test System',
data: {}
},
version: '13.331',
documentTypes: {
Actor: ['character', 'npc'],
Item: ['weapon', 'armor'],
RollTable: ['RollTable']
}
};
}
export function setupFoundryUI() {
globalThis.ui = {
notifications: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
}
};
}
export function setupFoundryCanvas(scene) {
const mockScene = scene || createMockScene();
globalThis.canvas = {
scene: mockScene,
regions: {
activate: vi.fn(),
deactivate: vi.fn()
},
ready: true
};
}
export function setupFoundryConfig() {
globalThis.CONFIG = {
Actor: {
documentClass: MockActorClass,
typeLabels: {}
},
debug: {
hooks: false
},
DND5E: {
skills: {
acr: { label: 'Acrobatics' },
ani: { label: 'Animal Handling' },
arc: { label: 'Arcana' },
ath: { label: 'Athletics' },
dec: { label: 'Deception' },
his: { label: 'History' },
ins: { label: 'Insight' },
inti: { label: 'Intimidation' },
inv: { label: 'Investigation' },
med: { label: 'Medicine' },
nat: { label: 'Nature' },
prc: { label: 'Perception' },
per: { label: 'Performance' },
prs: { label: 'Persuasion' },
rel: { label: 'Religion' },
slt: { label: 'Sleight of Hand' },
ste: { label: 'Stealth' },
sur: { label: 'Survival' }
}
}
};
}
// ============================================================================
// COMPLETE SETUP FUNCTION
// ============================================================================
/**
* Set up a complete Foundry VTT mock environment
*/
export function setupFoundryMocks(options = {}) {
setupFoundryGlobals();
setupFoundryDocuments();
setupFoundryGame(options);
setupFoundryUI();
setupFoundryConfig();
if (options.includeCanvas !== false) {
setupFoundryCanvas(options.scenes?.[0]);
}
}
// ============================================================================
// AUTO-SETUP (when imported)
// ============================================================================
// Automatically set up basic mocks when this file is imported
setupFoundryMocks();