import { chromium } from "playwright-core"; import { existsSync } from "node:fs"; const baseUrl = process.env.SFERA_WEB_URL ?? "http://192.168.200.60:3000"; const projectId = process.env.SFERA_PROJECT_ID ?? "demo"; const uniqueSuffix = Date.now().toString(36); const browserPath = process.env.SFERA_BROWSER_PATH ?? [ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe", "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe" ].find((path) => existsSync(path)); if (!browserPath) { throw new Error("No installed Chrome or Edge browser found. Set SFERA_BROWSER_PATH."); } const runtimeErrors = []; const browser = await chromium.launch({ executablePath: browserPath, headless: true }); function assertNoFatalRuntimeErrors(stage) { const fatalErrors = runtimeErrors.filter((message) => { return ( message.includes("Runtime TypeError") || message.includes("Cannot read properties") || message.includes("Unhandled Runtime Error") ); }); if (fatalErrors.length > 0) { throw new Error(`${stage} failed:\n${fatalErrors.join("\n")}`); } } async function collapseBottomPanelIfOpen(page) { if ((await page.locator("[data-bottom-tool-panel]").count()) > 0) { await page.keyboard.press("Alt+3"); await page.locator("[data-bottom-tool-panel]").waitFor({ state: "detached", timeout: 15000 }); } } try { const page = await browser.newPage(); await page.setViewportSize({ width: 1680, height: 1050 }); page.on("pageerror", (error) => { runtimeErrors.push(`pageerror: ${error.message}`); }); page.on("console", (message) => { if (message.type() === "error") { runtimeErrors.push(`console: ${message.text()}`); } }); const response = await page.goto(`${baseUrl}/editor?lang=ru&project=${encodeURIComponent(projectId)}&mode=module`, { waitUntil: "load", timeout: 60000 }); if (!response?.ok()) { throw new Error(`editor runtime smoke: ${response?.status()} ${response?.statusText()}`); } await page.getByRole("heading", { name: "Редактор BSL" }).waitFor({ state: "visible", timeout: 15000 }); await page.getByRole("heading", { name: "Семантический diff" }).waitFor({ state: "visible", timeout: 15000 }); for (const marker of ["snapshot", "agent", "parser", "diagnostics", "active-task", "privacy", "ai-tokens", "current-user"]) { await page.locator(`[data-status-item="${marker}"]`).waitFor({ state: "visible", timeout: 15000 }); } await page.locator("[data-global-search-input]").waitFor({ state: "visible", timeout: 15000 }); await page.keyboard.press("Control+K"); const globalSearchFocused = await page.locator("[data-global-search-input]").evaluate((input) => document.activeElement === input); if (!globalSearchFocused) { throw new Error("Ctrl+K did not focus global search"); } await page.evaluate(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "F5", bubbles: true, cancelable: true })); }); await page.locator("[data-run-check-status]").waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-left-navigation-panel]").waitFor({ state: "visible", timeout: 15000 }); const hasVirtualScrollContainer = (await page.locator("[data-virtual-scroll]").count()) > 0 || (await page.locator("[data-fallback-tree-scroll]").count()) > 0; if (!hasVirtualScrollContainer) { throw new Error("Left navigation panel has no virtual/fallback scroll container marker"); } await page.locator("[data-right-context-inspector]").waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-bottom-tool-panel]").waitFor({ state: "visible", timeout: 15000 }); for (const tabName of ["Проблемы", "Semantic diff", "Вывод", "История", "Тесты", "AI"]) { await page.locator("[data-bottom-tool-panel]").getByText(tabName).first().waitFor({ state: "visible", timeout: 15000 }); } await page.locator("[data-fallback-tree-search-input]").fill("SFERA"); await page.getByText("SFERA", { exact: true }).first().waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-fallback-tree-search-input]").fill(""); await page.locator("[data-fallback-tree-filter='sfera']").click(); await page.getByText("SFERA", { exact: true }).first().waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-fallback-tree-filter='all']").click(); const lazyFilterCount = await page.locator("[data-lazy-tree-filter]").count(); if (lazyFilterCount > 0) { await page.locator("[data-lazy-tree-filter='sfera']").click(); await page.getByText("SFERA", { exact: true }).first().waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-lazy-tree-filter='all']").click(); } await page.keyboard.press("Alt+1"); await page.locator("[data-left-navigation-panel]").waitFor({ state: "detached", timeout: 15000 }); await page.locator("[data-panel-rail]").first().click(); await page.locator("[data-left-navigation-panel]").waitFor({ state: "visible", timeout: 15000 }); await page.keyboard.press("Alt+2"); await page.locator("[data-right-context-inspector]").waitFor({ state: "detached", timeout: 15000 }); await page.locator("[data-panel-rail]").last().click(); await page.locator("[data-right-context-inspector]").waitFor({ state: "visible", timeout: 15000 }); await page.keyboard.press("Alt+3"); await page.locator("[data-bottom-tool-panel]").waitFor({ state: "detached", timeout: 15000 }); await page.getByRole("button", { name: "Нижняя панель" }).click(); await page.locator("[data-bottom-tool-panel]").waitFor({ state: "visible", timeout: 15000 }); await page.keyboard.press("Alt+3"); await page.locator("[data-bottom-tool-panel]").waitFor({ state: "detached", timeout: 15000 }); const activeDocumentBeforeNext = await page.locator('[data-open-document][aria-pressed="true"]').first().getAttribute("data-open-document"); const openDocumentCount = await page.locator("[data-open-document]").count(); if (openDocumentCount < 2) { throw new Error("Open Objects Bar has fewer than two switchable objects"); } await page.evaluate(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", ctrlKey: true, bubbles: true, cancelable: true })); }); const activeDocumentAfterNext = await page.locator('[data-open-document][aria-pressed="true"]').first().getAttribute("data-open-document"); if (!activeDocumentAfterNext || activeDocumentAfterNext === activeDocumentBeforeNext) { throw new Error("Ctrl+Tab did not switch the active open object"); } await page.evaluate(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab", ctrlKey: true, shiftKey: true, bubbles: true, cancelable: true })); }); await page.locator(`[data-open-document="${activeDocumentBeforeNext}"][aria-pressed="true"]`).waitFor({ state: "visible", timeout: 15000 }); await page.locator(`[data-open-document-pin="${activeDocumentAfterNext}"]`).click(); const pinnedCloseButton = page.locator(`[data-open-document-close="${activeDocumentAfterNext}"]`); if (!(await pinnedCloseButton.isDisabled())) { throw new Error("Pinned open object close button is not disabled"); } await page.locator(`[data-open-document-pin="${activeDocumentAfterNext}"]`).click(); await pinnedCloseButton.click(); await page.locator(`[data-open-document="${activeDocumentAfterNext}"]`).waitFor({ state: "detached", timeout: 15000 }); const ctrlWCandidate = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll("[data-open-document]")); const candidate = buttons.find((button) => button.getAttribute("data-open-document-mode") !== "module"); return candidate?.getAttribute("data-open-document") ?? null; }); if (ctrlWCandidate) { const targetTab = page.locator(`[data-open-document="${ctrlWCandidate}"]`); await targetTab.click(); await page.evaluate(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "w", ctrlKey: true, bubbles: true, cancelable: true })); }); await targetTab.waitFor({ state: "detached", timeout: 15000 }); const moduleTabs = page.locator('[data-open-document-mode="module"]'); const moduleTabCount = await moduleTabs.count(); if (moduleTabCount > 0) { await moduleTabs.first().click(); } } const openObjectsState = await page.evaluate((storageKey) => window.localStorage.getItem(storageKey), `sfera.open-objects.${projectId}`); if (!openObjectsState?.includes(activeDocumentAfterNext)) { throw new Error("Open Objects Bar state was not persisted"); } await page.reload({ waitUntil: "load", timeout: 60000 }); await page.getByRole("heading", { name: "Редактор BSL" }).waitFor({ state: "visible", timeout: 15000 }); const restoredState = await page.evaluate((storageKey) => window.localStorage.getItem(storageKey), `sfera.open-objects.${projectId}`); if (restoredState !== openObjectsState) { throw new Error("Open Objects Bar state was not restored after reload"); } await page.locator(`[data-open-document="${activeDocumentAfterNext}"]`).waitFor({ state: "detached", timeout: 15000 }); const restoredModuleTabs = page.locator('[data-open-document-mode="module"]'); if ((await restoredModuleTabs.count()) > 0) { await restoredModuleTabs.first().click(); } await page.locator(".monaco-editor").waitFor({ state: "visible", timeout: 15000 }); await page.locator("#symbol-search-input").fill("Проверить"); await page.locator("#symbol-search-input").press("Enter"); await page.locator("[data-symbol-result]").first().waitFor({ state: "visible", timeout: 15000 }); await page.locator('button[data-editor-action="find-usages"]').click(); await page.locator("[data-symbol-references]").waitFor({ state: "visible", timeout: 15000 }); const applyButton = page.locator('button[data-editor-action="apply-to-sfera"]'); await applyButton.waitFor({ state: "visible", timeout: 15000 }); if (await applyButton.isEnabled()) { await applyButton.click(); await page.getByText("Записано в SFERA").waitFor({ state: "visible", timeout: 15000 }); } assertNoFatalRuntimeErrors("editor module runtime smoke"); const versionsResponse = await page.goto(`${baseUrl}/editor?lang=ru&project=${encodeURIComponent(projectId)}&mode=versions`, { waitUntil: "load", timeout: 60000 }); if (!versionsResponse?.ok()) { throw new Error(`editor versions runtime smoke: ${versionsResponse?.status()} ${versionsResponse?.statusText()}`); } await page.getByRole("heading", { name: "Версии" }).waitFor({ state: "visible", timeout: 15000 }); await collapseBottomPanelIfOpen(page); await page.getByText("Линия версий").first().waitFor({ state: "visible", timeout: 15000 }); const versionButtons = page.locator("button[data-version-id]"); const versionButtonCount = await versionButtons.count(); if (versionButtonCount > 0) { await versionButtons.first().click(); await page.getByText("Полные данные").waitFor({ state: "visible", timeout: 15000 }); } assertNoFatalRuntimeErrors("editor versions runtime smoke"); const rollbackButtons = page.locator('button[data-editor-action="rollback-plan"]'); const rollbackButtonCount = await rollbackButtons.count(); if (rollbackButtonCount > 0) { await rollbackButtons.first().click(); await page.locator("[data-rollback-preview]").waitFor({ state: "visible", timeout: 15000 }); const applyRollbackButton = page.locator('button[data-editor-action="apply-rollback"]'); await applyRollbackButton.waitFor({ state: "visible", timeout: 15000 }); if (await applyRollbackButton.isEnabled()) { await applyRollbackButton.click(); await page.locator("[data-rollback-apply-message]").waitFor({ state: "visible", timeout: 15000 }); } } assertNoFatalRuntimeErrors("editor rollback runtime smoke"); const metadataResponse = await page.goto(`${baseUrl}/editor?lang=ru&project=${encodeURIComponent(projectId)}&mode=properties`, { waitUntil: "load", timeout: 60000 }); if (!metadataResponse?.ok()) { throw new Error(`editor metadata runtime smoke: ${metadataResponse?.status()} ${metadataResponse?.statusText()}`); } await page.getByRole("heading", { name: "Свойства" }).waitFor({ state: "visible", timeout: 15000 }); await collapseBottomPanelIfOpen(page); await page.getByLabel("Имя объекта").fill(`АвтоОбъект${uniqueSuffix}`); await page.getByLabel("Синоним").fill(`Авто объект ${uniqueSuffix}`); const addAttributeButton = page.locator('button[data-editor-action="add-metadata-attribute"]'); await addAttributeButton.waitFor({ state: "visible", timeout: 15000 }); await addAttributeButton.click(); await page.getByRole("textbox", { name: "Имя реквизита" }).nth(1).fill("Комментарий"); await page.getByRole("textbox", { name: "Тип" }).nth(1).fill("Строка250"); const addFormButton = page.locator('button[data-editor-action="add-metadata-form"]'); await addFormButton.waitFor({ state: "visible", timeout: 15000 }); await addFormButton.click(); await page.getByRole("textbox", { name: "Имя формы" }).last().fill(`ФормаВыбора${uniqueSuffix}`); const addCommandButton = page.locator('button[data-editor-action="add-metadata-command"]'); await addCommandButton.waitFor({ state: "visible", timeout: 15000 }); await addCommandButton.click(); await page.getByRole("textbox", { name: "Имя команды" }).last().fill(`Отправить${uniqueSuffix}`); await page.getByRole("textbox", { name: "Обработчик" }).last().fill(`Отправить${uniqueSuffix}Команда`); const metadataButton = page.locator('button[data-editor-action="apply-metadata-object"]'); await metadataButton.waitFor({ state: "visible", timeout: 15000 }); if (await metadataButton.isEnabled()) { await metadataButton.click(); const draftMessage = page.locator("[data-metadata-draft-message]"); await draftMessage.waitFor({ state: "visible", timeout: 15000 }); try { await page.locator("[data-metadata-draft-preview]").waitFor({ state: "visible", timeout: 15000 }); } catch (error) { throw new Error(`metadata draft preview did not appear: ${await draftMessage.textContent()}`); } } assertNoFatalRuntimeErrors("editor metadata runtime smoke"); const englishResponse = await page.goto(`${baseUrl}/editor?lang=en&project=${encodeURIComponent(projectId)}&mode=module`, { waitUntil: "load", timeout: 60000 }); if (!englishResponse?.ok()) { throw new Error(`editor english runtime smoke: ${englishResponse?.status()} ${englishResponse?.statusText()}`); } await page.getByRole("heading", { name: "BSL Editor" }).waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-top-project-bar]").waitFor({ state: "visible", timeout: 15000 }); for (const selector of ["workspace", "project", "environment", "active-task"]) { await page.locator(`[data-top-bar-selector="${selector}"]`).waitFor({ state: "visible", timeout: 15000 }); } for (const action of ["project-settings", "create-project"]) { await page.locator(`[data-top-bar-action="${action}"]`).waitFor({ state: "visible", timeout: 15000 }); } for (const badge of ["api-status", "agent-status"]) { await page.locator(`[data-top-bar-badge="${badge}"]`).waitFor({ state: "visible", timeout: 15000 }); } await page.locator("[data-top-bar-language]").waitFor({ state: "visible", timeout: 15000 }); await page.locator('[data-top-bar-button="profile"]').waitFor({ state: "visible", timeout: 15000 }); await page.locator("[data-bottom-tool-panel]").waitFor({ state: "visible", timeout: 15000 }); for (const tabName of ["Problems", "Semantic diff", "Output", "Change history", "Tests", "AI"]) { await page.locator("[data-bottom-tool-panel]").getByText(tabName).first().waitFor({ state: "visible", timeout: 15000 }); } for (const statusMarker of ["snapshot", "agent", "parser", "diagnostics", "active-task", "privacy", "ai-tokens", "current-user"]) { await page.locator(`[data-status-item="${statusMarker}"]`).waitFor({ state: "visible", timeout: 15000 }); } assertNoFatalRuntimeErrors("editor english runtime smoke"); console.log("ok editor runtime"); } finally { await browser.close(); }