SuperAI_Forecast / scripts /playwright_smoke.js
Thang6822
Update HF Space deployment
4106e0f
const { chromium } = require('playwright');
const WORKSPACE_STORAGE_KEY = 'aiforecast_workspace';
async function dispatchControlValue(page, selector, value) {
await page.locator(selector).evaluate((element, nextValue) => {
element.value = nextValue;
element.dispatchEvent(new Event('change', { bubbles: true }));
}, value);
}
async function waitForPaneLabel(page, selector, expectedText) {
await page.waitForFunction(
({ targetSelector, text }) => {
const element = document.querySelector(targetSelector);
return Boolean(element && String(element.textContent || '').includes(text));
},
{ targetSelector: selector, text: expectedText },
);
}
async function assertPaneCount(page, expectedCount) {
await page.waitForFunction(
(count) => document.querySelectorAll('.chart-pane').length === count,
expectedCount,
);
}
async function switchLayout(page, preset) {
await page.evaluate((nextPreset) => {
const menuButton = document.querySelector('#layoutMenuBtn');
const layoutButton = document.querySelector(`button[data-layout="${nextPreset}"]`);
menuButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
layoutButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
}, preset);
await page.waitForSelector(`.workspace-grid.layout-${preset}`);
await assertPaneCount(page, preset);
}
async function assertModelAvailabilityMatchesHealth(page) {
const health = await page.evaluate(async () => {
const response = await fetch('/api/health');
return response.json();
});
const modelButtonIds = {
kronos: '#kronosToggle',
timesfm: '#timesfmToggle',
chronos: '#chronosToggle',
};
for (const [modelKey, selector] of Object.entries(modelButtonIds)) {
const expectedAvailable = Boolean(health?.[modelKey]?.available);
await page.waitForFunction(
({ targetSelector, available }) => {
const button = document.querySelector(targetSelector);
if (!(button instanceof HTMLButtonElement)) {
return false;
}
return button.dataset.available === String(available);
},
{ targetSelector: selector, available: expectedAvailable },
);
const isDisabled = await page.locator(selector).isDisabled();
if (expectedAvailable && isDisabled) {
throw new Error(`${modelKey} should be enabled when /api/health reports available=true`);
}
if (!expectedAvailable && !isDisabled) {
throw new Error(`${modelKey} should be disabled when /api/health reports available=false`);
}
}
}
async function assertFocusMode(page, paneSelector) {
await page.locator(paneSelector).dblclick();
await page.waitForFunction(
(targetSelector) => {
const grid = document.querySelector('#workspaceGrid');
const pane = document.querySelector(targetSelector);
return Boolean(
grid?.classList.contains('pane-focus-mode')
&& pane?.classList.contains('is-maximized'),
);
},
paneSelector,
);
await page.locator(paneSelector).dblclick();
await page.waitForFunction(
() => !document.querySelector('#workspaceGrid')?.classList.contains('pane-focus-mode'),
);
await page.locator(paneSelector).dblclick();
await page.waitForFunction(
(targetSelector) => {
const grid = document.querySelector('#workspaceGrid');
const pane = document.querySelector(targetSelector);
return Boolean(
grid?.classList.contains('pane-focus-mode')
&& pane?.classList.contains('is-maximized'),
);
},
paneSelector,
);
await page.keyboard.press('Escape');
await page.waitForFunction(
() => !document.querySelector('#workspaceGrid')?.classList.contains('pane-focus-mode'),
);
}
async function assertMascotVisibility(page, shouldShow) {
await page.waitForFunction(
(expectedVisible) => {
const overlay = document.querySelector('.chart-bg-overlay');
if (!overlay) {
return false;
}
const overlayOpacity = Number.parseFloat(getComputedStyle(overlay).opacity || '0');
const mascotOpacity = Number.parseFloat(getComputedStyle(overlay, '::before').opacity || '0');
return expectedVisible
? overlayOpacity > 0.01 && mascotOpacity > 0.01
: overlayOpacity <= 0.01 || mascotOpacity <= 0.01;
},
shouldShow,
);
}
async function assertChartOnlyToggle(page) {
await page.waitForFunction(() => document.body.dataset.chartOnly === 'false');
await page.locator('#chartOnlyToggle').click();
await page.waitForFunction(() => document.body.dataset.chartOnly === 'true');
await page.waitForFunction(
() => document.querySelector('#chartOnlyToggle')?.getAttribute('aria-pressed') === 'true',
);
await page.waitForFunction(() => {
const statusWrap = document.querySelector('.status-wrap');
const chartGauges = document.querySelector('#chartGauges');
if (!(statusWrap instanceof HTMLElement) || !(chartGauges instanceof HTMLElement)) {
return false;
}
return getComputedStyle(statusWrap).display === 'none'
&& getComputedStyle(chartGauges).display === 'none';
});
await assertMascotVisibility(page, false);
await page.locator('#chartOnlyToggle').click();
await page.waitForFunction(() => document.body.dataset.chartOnly === 'false');
await page.waitForFunction(
() => document.querySelector('#chartOnlyToggle')?.getAttribute('aria-pressed') === 'false',
);
}
async function assertWorkspaceRestore(page) {
const restoredState = {
version: 4,
layoutPreset: 4,
activePaneId: 'pane-2',
forecastSettings: { contextLength: 384 },
panes: [
{
id: 'pane-0',
symbol: 'DXY',
interval: '4h',
indicator: 'none',
horizon: 24,
aiModels: { kronos: true, timesfm: true, chronos: false },
},
{
id: 'pane-1',
symbol: 'EURX',
interval: '4h',
indicator: 'none',
horizon: 24,
aiModels: { kronos: true, timesfm: false, chronos: true },
},
{
id: 'pane-2',
symbol: 'GBPX',
interval: '4h',
indicator: 'none',
horizon: 24,
aiModels: { kronos: true, timesfm: true, chronos: true },
},
{
id: 'pane-3',
symbol: 'JPYX',
interval: '4h',
indicator: 'none',
horizon: 24,
aiModels: { kronos: false, timesfm: true, chronos: true },
},
],
};
await page.evaluate((workspaceState) => {
window.localStorage.setItem(WORKSPACE_STORAGE_KEY, JSON.stringify(workspaceState));
}, restoredState);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForSelector('#workspaceGrid');
await assertPaneCount(page, 4);
await page.waitForSelector('.workspace-grid.layout-4');
await waitForPaneLabel(page, '.chart-pane[data-pane-id="pane-2"] .pane-symbol', 'GBPX');
await waitForPaneLabel(page, '.chart-pane[data-pane-id="pane-3"] .pane-symbol', 'JPYX');
await page.waitForFunction(() => document.querySelector('#timeframeSelect')?.value === '4h');
await page.waitForFunction(() => document.querySelector('#horizonInput')?.value === '24');
}
function attachForecastRequestCapture(page) {
const requests = [];
const handleRequest = (request) => {
try {
const url = new URL(request.url());
if (!url.pathname.startsWith('/api/forecast/')) {
return;
}
requests.push({
symbol: decodeURIComponent(url.pathname.split('/').pop() || ''),
contextLength: Number(url.searchParams.get('context_length') || '0'),
refresh: url.searchParams.get('refresh'),
stage: url.searchParams.get('stage') || 'interactive',
requestId: url.searchParams.get('request_id') || '',
});
} catch (_) {
// Ignore malformed request URLs captured by the browser.
}
};
page.on('request', handleRequest);
return {
requests,
dispose() {
page.off('request', handleRequest);
},
};
}
async function assertWorkspaceContextForecastRequests(page, forecastRequests, expectedSymbol) {
await page.waitForFunction(
(symbol) => {
const pane = window.Workspace?.getPane?.('pane-0');
return Boolean(
pane
&& pane.symbol === symbol
&& pane.aiSession?.committedSnapshot?.payload,
);
},
expectedSymbol,
{ timeout: 240000 },
);
const symbolRequests = forecastRequests.filter((request) => request.symbol === expectedSymbol);
const bootstrapIndex = symbolRequests.findIndex((request) => (
request.stage === 'bootstrap'
&& request.contextLength === 384
&& request.refresh === 'false'
));
if (bootstrapIndex < 0) {
throw new Error(`Missing bootstrap forecast request for ${expectedSymbol}`);
}
if (!symbolRequests[bootstrapIndex]?.requestId) {
throw new Error(`Missing request_id on bootstrap forecast request for ${expectedSymbol}`);
}
}
async function main() {
const baseUrl = process.env.AIFORECAST_BASE_URL || 'http://127.0.0.1:8000';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
try {
const forecastCapture = attachForecastRequestCapture(page);
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
await Promise.all([
page.waitForSelector('#workspaceGrid'),
page.waitForSelector('#timeframeSelect'),
page.waitForSelector('#horizonInput'),
]);
await assertModelAvailabilityMatchesHealth(page);
await switchLayout(page, 1);
await assertMascotVisibility(page, true);
await assertChartOnlyToggle(page);
await page.evaluate(() => {
if (typeof window.explorerSelectSymbol === 'function') {
window.explorerSelectSymbol('EURUSD');
}
});
await page.waitForFunction(() => document.querySelector('#symbolSearch')?.value === 'EURUSD');
await dispatchControlValue(page, '#timeframeSelect', '1h');
await page.waitForFunction(() => document.querySelector('#timeframeSelect')?.value === '1h');
await dispatchControlValue(page, '#horizonInput', '24');
await page.waitForFunction(() => document.querySelector('#horizonInput')?.value === '24');
await assertWorkspaceContextForecastRequests(page, forecastCapture.requests, 'EURUSD');
for (const preset of [2, 4, 8]) {
await switchLayout(page, preset);
await assertMascotVisibility(page, false);
}
await assertFocusMode(page, '.chart-pane[data-pane-id="pane-1"]');
await assertWorkspaceRestore(page);
forecastCapture.dispose();
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error('[playwright-smoke] failed:', error);
process.exitCode = 1;
});