This commit is contained in:
2025-12-11 21:06:25 +01:00
parent c49ea2598d
commit ec3ddc3a5c
227 changed files with 3496 additions and 2083 deletions

View File

@@ -63,8 +63,11 @@ export class PlaywrightAuthSessionService implements AuthenticationServicePort {
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
// ===== Helpers =====

View File

@@ -609,8 +609,11 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
private syncSessionStateFromBrowser(): void {
@@ -758,7 +761,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
const stepNumber = stepId.value;
const skipFixtureNavigation =
(config as any).__skipFixtureNavigation === true;
(config as { __skipFixtureNavigation?: unknown }).__skipFixtureNavigation === true;
if (!skipFixtureNavigation) {
if (!this.isRealMode() && this.config.baseUrl) {
@@ -2292,9 +2295,9 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
await this.page.evaluate(({ sel, val }) => {
const el = document.querySelector(sel) as HTMLInputElement | HTMLTextAreaElement | null;
if (!el) return;
(el as any).value = val;
(el as any).dispatchEvent(new Event('input', { bubbles: true }));
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
el.value = val;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}, { sel: selector, val: value });
return { success: true, fieldName, valueSet: value };
} catch (evalErr) {
@@ -2492,11 +2495,11 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
// If element is a checkbox/input, set checked; otherwise try to toggle aria-checked or click
if ('checked' in el) {
(el as HTMLInputElement).checked = Boolean(should);
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
// Fallback: set aria-checked attribute and dispatch click
(el as HTMLElement).setAttribute('aria-checked', String(Boolean(should)));
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
try { (el as HTMLElement).click(); } catch { /* ignore */ }
}
} catch {
@@ -2997,7 +3000,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, Authenti
* Get the source of the browser mode configuration.
*/
getBrowserModeSource(): 'env' | 'file' | 'default' {
return this.browserSession.getBrowserModeSource() as any;
return this.browserSession.getBrowserModeSource() as 'env' | 'file' | 'default';
}
/**

View File

@@ -48,8 +48,11 @@ export class PlaywrightBrowserSession {
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
private isRealMode(): boolean {
@@ -122,8 +125,10 @@ export class PlaywrightBrowserSession {
this.browserModeSource = currentConfig.source as BrowserModeSource;
const effectiveMode = forceHeaded ? 'headed' : currentConfig.mode;
const adapterAny = PlaywrightAutomationAdapter as any;
const launcher = adapterAny.testLauncher ?? chromium;
const adapterWithLauncher = PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: typeof chromium;
};
const launcher = adapterWithLauncher.testLauncher ?? chromium;
this.log('debug', 'Effective browser mode at connect', {
effectiveMode,

View File

@@ -108,8 +108,11 @@ export class WizardStepOrchestrator {
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
private async waitIfPaused(): Promise<void> {
@@ -345,7 +348,7 @@ export class WizardStepOrchestrator {
{ selector: raceInfoFallback },
);
const inner = await this.page!.evaluate(() => {
const doc = (globalThis as any).document as any;
const doc = (globalThis as { document?: Document }).document;
return (
doc?.querySelector('#create-race-wizard')?.innerHTML || ''
);
@@ -428,32 +431,32 @@ export class WizardStepOrchestrator {
const page = this.page;
if (page) {
await page.evaluate((term) => {
const doc = (globalThis as any).document as any;
const doc = (globalThis as { document?: Document }).document;
if (!doc) {
return;
}
const root =
(doc.querySelector('#set-admins') as any) ?? doc.body;
(doc.querySelector('#set-admins') as HTMLElement | null) ?? doc.body;
if (!root) {
return;
}
const rows = Array.from(
(root as any).querySelectorAll(
root.querySelectorAll<HTMLTableRowElement>(
'tbody[data-testid="admin-display-name-list"] tr',
),
) as any[];
);
if (rows.length === 0) {
return;
}
const needle = String(term).toLowerCase();
for (const r of rows) {
const text = String((r as any).textContent || '').toLowerCase();
const text = String(r.textContent || '').toLowerCase();
if (text.includes(needle)) {
(r as any).setAttribute('data-selected-admin', 'true');
r.setAttribute('data-selected-admin', 'true');
return;
}
}
(rows[0] as any).setAttribute('data-selected-admin', 'true');
rows[0]?.setAttribute('data-selected-admin', 'true');
}, String(adminSearch));
}
}
@@ -975,7 +978,7 @@ export class WizardStepOrchestrator {
{ selector: weatherFallbackSelector },
);
const inner = await this.page!.evaluate(() => {
const doc = (globalThis as any).document as any;
const doc = (globalThis as { document?: Document }).document;
return (
doc?.querySelector('#create-race-wizard')?.innerHTML || ''
);
@@ -1130,7 +1133,7 @@ export class WizardStepOrchestrator {
} else {
const valueStr = String(config.trackState);
await this.page!.evaluate((trackStateValue) => {
const doc = (globalThis as any).document as any;
const doc = (globalThis as { document?: Document }).document;
if (!doc) {
return;
}
@@ -1145,27 +1148,24 @@ export class WizardStepOrchestrator {
};
const numeric = map[trackStateValue] ?? null;
const inputs = Array.from(
doc.querySelectorAll(
doc.querySelectorAll<HTMLInputElement>(
'input[id*="starting-track-state"], input[id*="track-state"], input[data-value]',
),
) as any[];
);
if (numeric !== null && inputs.length > 0) {
for (const inp of inputs) {
try {
(inp as any).value = String(numeric);
const ds =
(inp as any).dataset || ((inp as any).dataset = {});
ds.value = String(numeric);
(inp as any).setAttribute?.(
inp.value = String(numeric);
inp.dataset.value = String(numeric);
inp.setAttribute(
'data-value',
String(numeric),
);
const Ev = (globalThis as any).Event;
(inp as any).dispatchEvent?.(
new Ev('input', { bubbles: true }),
inp.dispatchEvent(
new Event('input', { bubbles: true }),
);
(inp as any).dispatchEvent?.(
new Ev('change', { bubbles: true }),
inp.dispatchEvent(
new Event('change', { bubbles: true }),
);
} catch {
}

View File

@@ -22,8 +22,11 @@ export class IRacingDomInteractor {
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
private isRealMode(): boolean {
@@ -86,7 +89,7 @@ export class IRacingDomInteractor {
});
const el = document.querySelector(sel) as HTMLInputElement | HTMLTextAreaElement | null;
if (!el) return;
(el as any).value = val;
el.value = val;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}, { sel: selector, val: value });
@@ -194,9 +197,9 @@ export class IRacingDomInteractor {
await page.evaluate(({ sel, val }) => {
const el = document.querySelector(sel) as HTMLInputElement | HTMLTextAreaElement | null;
if (!el) return;
(el as any).value = val;
(el as any).dispatchEvent(new Event('input', { bubbles: true }));
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
el.value = val;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}, { sel: selector, val: value });
return { success: true, fieldName, valueSet: value };
} catch (evalErr) {
@@ -372,8 +375,8 @@ export class IRacingDomInteractor {
const tag = await page
.locator(h)
.first()
.evaluate((el: any) =>
String((el as any).tagName || '').toLowerCase(),
.evaluate((el: Element) =>
String(el.tagName || '').toLowerCase(),
)
.catch(() => '');
if (tag === 'select') {
@@ -511,10 +514,10 @@ export class IRacingDomInteractor {
try {
if ('checked' in el) {
(el as HTMLInputElement).checked = Boolean(should);
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
(el as HTMLElement).setAttribute('aria-checked', String(Boolean(should)));
(el as any).dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
try {
(el as HTMLElement).click();
} catch {
@@ -544,8 +547,8 @@ export class IRacingDomInteractor {
if (count === 0) continue;
const tagName = await locator
.evaluate((el: any) =>
String((el as any).tagName || '').toLowerCase(),
.evaluate((el: Element) =>
String(el.tagName || '').toLowerCase(),
)
.catch(() => '');
const type = await locator.getAttribute('type').catch(() => '');
@@ -682,8 +685,8 @@ export class IRacingDomInteractor {
if (count === 0) continue;
const tagName = await locator
.evaluate((el: any) =>
String((el as any).tagName || '').toLowerCase(),
.evaluate((el: Element) =>
String(el.tagName || '').toLowerCase(),
)
.catch(() => '');
if (tagName === 'input') {

View File

@@ -32,8 +32,11 @@ export class IRacingDomNavigator {
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
private isRealMode(): boolean {

View File

@@ -15,8 +15,11 @@ export class SafeClickService {
if (!this.logger) {
return;
}
const logger: any = this.logger;
logger[level](message, context as any);
const logger = this.logger as Record<
'debug' | 'info' | 'warn' | 'error',
(msg: string, ctx?: Record<string, unknown>) => void
>;
logger[level](message, context);
}
private isRealMode(): boolean {

View File

@@ -6,6 +6,7 @@ import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO'
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
import type { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../IAutomationLifecycleEmitter';
interface MockConfig {
simulateFailures?: boolean;
@@ -24,9 +25,10 @@ interface StepExecutionResult {
};
}
export class MockBrowserAutomationAdapter implements IBrowserAutomation {
export class MockBrowserAutomationAdapter implements IBrowserAutomation, IAutomationLifecycleEmitter {
private config: MockConfig;
private connected: boolean = false;
private lifecycleCallbacks: Set<LifecycleCallback> = new Set();
constructor(config: MockConfig = {}) {
this.config = {
@@ -105,6 +107,13 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
}
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
// Emit a simple lifecycle event for tests/overlay sync
await this.emitLifecycle({
type: 'action-started',
actionId: String(stepId.value),
timestamp: Date.now(),
payload: { config },
});
if (this.shouldSimulateFailure()) {
throw new Error(`Simulated failure at step ${stepId.value}`);
}
@@ -154,4 +163,18 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
}
return Math.random() < (this.config.failureRate || 0.1);
}
onLifecycle(cb: LifecycleCallback): void {
this.lifecycleCallbacks.add(cb);
}
offLifecycle(cb: LifecycleCallback): void {
this.lifecycleCallbacks.delete(cb);
}
private async emitLifecycle(event: Parameters<LifecycleCallback>[0]): Promise<void> {
for (const cb of Array.from(this.lifecycleCallbacks)) {
await cb(event);
}
}
}