feat: Add Happy DOM location mocking for Playwright tests

- Enhanced setup-browser-globals.js with configurable URL and full location mock
- Created LocationMock helper class for dynamic location mocking in tests
- Integrated LocationMock into ComfyPage fixture with optional setup
- Added example test file demonstrating location mock usage
- Support for mocking location.assign, location.replace, and location.reload methods
This commit is contained in:
snomiao
2025-09-13 09:14:37 +00:00
parent 29d22454f4
commit ead43312f8
4 changed files with 279 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { ComfyTemplates } from '../helpers/templates'
import { LocationMock } from '../helpers/locationMock'
import { ComfyMouse } from './ComfyMouse'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { SettingDialog } from './components/SettingDialog'
@@ -144,6 +145,7 @@ export class ComfyPage {
public readonly templates: ComfyTemplates
public readonly settingDialog: SettingDialog
public readonly confirmDialog: ConfirmDialog
public readonly locationMock: LocationMock
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -172,6 +174,7 @@ export class ComfyPage {
this.templates = new ComfyTemplates(page)
this.settingDialog = new SettingDialog(page, this)
this.confirmDialog = new ConfirmDialog(page)
this.locationMock = new LocationMock(page)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {
@@ -272,11 +275,19 @@ export class ComfyPage {
async setup({
clearStorage = true,
mockReleases = true
mockReleases = true,
mockLocation = false
}: {
clearStorage?: boolean
mockReleases?: boolean
mockLocation?: boolean | Parameters<LocationMock['setupLocationMock']>[0]
} = {}) {
// Setup location mock if requested
if (mockLocation) {
const config = typeof mockLocation === 'boolean' ? undefined : mockLocation
await this.locationMock.setupLocationMock(config)
}
await this.goto()
// Mock release endpoint to prevent changelog popups

View File

@@ -0,0 +1,142 @@
import type { Page } from '@playwright/test'
/**
* Mock location object for testing navigation and URL manipulation
*/
export class LocationMock {
constructor(private page: Page) {}
/**
* Mock the location object in the page context
* @param mockConfig Configuration for the mock location
*/
async setupLocationMock(mockConfig?: {
href?: string
origin?: string
pathname?: string
search?: string
hash?: string
hostname?: string
port?: string
protocol?: string
}) {
await this.page.addInitScript((config) => {
const defaultUrl = config?.href || window.location.href
const url = new URL(defaultUrl)
// Create a mock location object
const mockLocation = {
href: config?.href || url.href,
origin: config?.origin || url.origin,
protocol: config?.protocol || url.protocol,
host: url.host,
hostname: config?.hostname || url.hostname,
port: config?.port || url.port,
pathname: config?.pathname || url.pathname,
search: config?.search || url.search,
hash: config?.hash || url.hash,
assign: (newUrl: string) => {
console.log(`[Mock] location.assign called with: ${newUrl}`)
mockLocation.href = newUrl
// Trigger navigation event if needed
window.dispatchEvent(new Event('popstate'))
},
replace: (newUrl: string) => {
console.log(`[Mock] location.replace called with: ${newUrl}`)
mockLocation.href = newUrl
// Trigger navigation event if needed
window.dispatchEvent(new Event('popstate'))
},
reload: () => {
console.log('[Mock] location.reload called')
// Trigger reload event if needed
window.dispatchEvent(new Event('beforeunload'))
},
toString: () => mockLocation.href
}
// Override window.location
Object.defineProperty(window, 'location', {
value: mockLocation,
writable: true,
configurable: true
})
// Also override document.location
Object.defineProperty(document, 'location', {
value: mockLocation,
writable: true,
configurable: true
})
}, mockConfig)
}
/**
* Update the mock location during test execution
*/
async updateLocation(updates: Partial<{
href: string
pathname: string
search: string
hash: string
}>) {
await this.page.evaluate((updates) => {
const location = window.location as any
Object.keys(updates).forEach((key) => {
if (location[key] !== undefined) {
location[key] = updates[key as keyof typeof updates]
}
})
}, updates)
}
/**
* Get the current mock location values
*/
async getLocation() {
return await this.page.evaluate(() => {
const loc = window.location
return {
href: loc.href,
origin: loc.origin,
protocol: loc.protocol,
host: loc.host,
hostname: loc.hostname,
port: loc.port,
pathname: loc.pathname,
search: loc.search,
hash: loc.hash
}
})
}
/**
* Simulate navigation to a new URL
*/
async navigateTo(url: string) {
await this.page.evaluate((url) => {
const location = window.location as any
location.assign(url)
}, url)
}
/**
* Simulate location.replace
*/
async replaceTo(url: string) {
await this.page.evaluate((url) => {
const location = window.location as any
location.replace(url)
}, url)
}
/**
* Simulate location.reload
*/
async reload() {
await this.page.evaluate(() => {
const location = window.location as any
location.reload()
})
}
}

View File

@@ -0,0 +1,96 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
import { LocationMock } from './helpers/locationMock'
test.describe('Location Mock Example', () => {
test('should mock location object', async ({ page, comfyPage }) => {
const locationMock = new LocationMock(page)
// Setup location mock before navigating to the page
await locationMock.setupLocationMock({
href: 'http://example.com/test',
pathname: '/test',
search: '?query=value',
hash: '#section'
})
// Navigate to your app
await comfyPage.goto()
// Verify the mock is working
const location = await locationMock.getLocation()
expect(location.pathname).toBe('/test')
expect(location.search).toBe('?query=value')
expect(location.hash).toBe('#section')
// Test navigation
await locationMock.navigateTo('http://example.com/new-page')
const newLocation = await locationMock.getLocation()
expect(newLocation.href).toBe('http://example.com/new-page')
// Test updating specific properties
await locationMock.updateLocation({
pathname: '/updated-path',
search: '?new=param'
})
const updatedLocation = await locationMock.getLocation()
expect(updatedLocation.pathname).toBe('/updated-path')
expect(updatedLocation.search).toBe('?new=param')
})
test('should handle location methods', async ({ page, comfyPage }) => {
const locationMock = new LocationMock(page)
await locationMock.setupLocationMock({
href: 'http://localhost:5173/'
})
await comfyPage.goto()
// Test location.assign
await page.evaluate(() => {
window.location.assign('/new-route')
})
// Check console for mock output
const consoleMessages: string[] = []
page.on('console', (msg) => {
if (msg.text().includes('[Mock]')) {
consoleMessages.push(msg.text())
}
})
await locationMock.navigateTo('/another-route')
await locationMock.replaceTo('/replaced-route')
await locationMock.reload()
// Verify mock methods were called
expect(consoleMessages.some((msg) => msg.includes('location.assign'))).toBeTruthy()
expect(consoleMessages.some((msg) => msg.includes('location.replace'))).toBeTruthy()
expect(consoleMessages.some((msg) => msg.includes('location.reload'))).toBeTruthy()
})
test('should work with Happy DOM globals', async ({ page, comfyPage }) => {
// Set environment variable for Happy DOM URL
process.env.HAPPY_DOM_URL = 'http://custom-domain.com/'
const locationMock = new LocationMock(page)
await locationMock.setupLocationMock()
await comfyPage.goto()
// Verify location is mocked correctly
const location = await page.evaluate(() => ({
href: window.location.href,
origin: window.location.origin,
canAssign: typeof window.location.assign === 'function',
canReplace: typeof window.location.replace === 'function',
canReload: typeof window.location.reload === 'function'
}))
expect(location.canAssign).toBeTruthy()
expect(location.canReplace).toBeTruthy()
expect(location.canReload).toBeTruthy()
})
})

View File

@@ -6,17 +6,43 @@ if (typeof globalThis.__USE_PROD_CONFIG__ === 'undefined') {
globalThis.__USE_PROD_CONFIG__ = false;
}
// Create a happy-dom window instance
// Create a happy-dom window instance with configurable URL
const defaultUrl = (typeof globalThis.process !== 'undefined' && globalThis.process.env?.HAPPY_DOM_URL) || 'http://localhost:5173/';
const window = new Window({
url: 'http://localhost:5173/',
url: defaultUrl,
width: 1024,
height: 768
});
// Mock location with additional properties for testing
const mockLocation = {
...window.location,
href: defaultUrl,
origin: new URL(defaultUrl).origin,
protocol: new URL(defaultUrl).protocol,
host: new URL(defaultUrl).host,
hostname: new URL(defaultUrl).hostname,
port: new URL(defaultUrl).port,
pathname: new URL(defaultUrl).pathname,
search: new URL(defaultUrl).search,
hash: new URL(defaultUrl).hash,
assign: (url) => {
console.log(`[Mock] location.assign called with: ${url}`);
mockLocation.href = url;
},
replace: (url) => {
console.log(`[Mock] location.replace called with: ${url}`);
mockLocation.href = url;
},
reload: () => {
console.log('[Mock] location.reload called');
}
};
// Expose DOM globals (only set if not already defined)
if (!globalThis.window) globalThis.window = window;
if (!globalThis.document) globalThis.document = window.document;
if (!globalThis.location) globalThis.location = window.location;
if (!globalThis.location) globalThis.location = mockLocation;
if (!globalThis.navigator) {
try {
globalThis.navigator = window.navigator;