Initial SFERA platform baseline
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
const baseUrl = process.env.SFERA_WEB_URL ?? "http://192.168.200.60:3000";
|
||||
const projectId = process.env.SFERA_PROJECT_ID ?? "demo";
|
||||
const routine = encodeURIComponent(process.env.SFERA_ROUTINE ?? "Проведение");
|
||||
const attempts = Number(process.env.SFERA_SMOKE_ATTEMPTS ?? "5");
|
||||
|
||||
const checks = [
|
||||
{
|
||||
name: "root opens IDE workspace",
|
||||
url: `${baseUrl}/?lang=ru&project=${projectId}`,
|
||||
mustInclude: [
|
||||
"SFERA",
|
||||
"data-top-project-bar",
|
||||
"data-top-bar-logo",
|
||||
"data-top-bar-selector=\"workspace\"",
|
||||
"data-top-bar-selector=\"project\"",
|
||||
"data-top-bar-selector=\"environment\"",
|
||||
"data-top-bar-selector=\"active-task\"",
|
||||
"data-top-bar-action=\"project-settings\"",
|
||||
"data-top-bar-action=\"create-project\"",
|
||||
"data-top-bar-badge=\"api-status\"",
|
||||
"data-top-bar-badge=\"agent-status\"",
|
||||
"data-top-bar-language",
|
||||
"data-top-bar-button=\"profile\"",
|
||||
"Ctrl+K",
|
||||
"data-status-bar",
|
||||
"data-status-item=\"current-user\""
|
||||
],
|
||||
mustNotInclude: ["Открыть в редакторе"]
|
||||
},
|
||||
{
|
||||
name: "root opens IDE workspace (en)",
|
||||
url: `${baseUrl}/?lang=en&project=${projectId}`,
|
||||
mustInclude: [
|
||||
"SFERA",
|
||||
"data-top-project-bar",
|
||||
"data-top-bar-logo",
|
||||
"data-top-bar-selector=\"workspace\"",
|
||||
"data-top-bar-selector=\"project\"",
|
||||
"data-top-bar-selector=\"environment\"",
|
||||
"data-top-bar-selector=\"active-task\"",
|
||||
"data-top-bar-action=\"project-settings\"",
|
||||
"data-top-bar-action=\"create-project\"",
|
||||
"data-top-bar-badge=\"api-status\"",
|
||||
"data-top-bar-badge=\"agent-status\"",
|
||||
"data-top-bar-language",
|
||||
"data-top-bar-button=\"profile\"",
|
||||
"Ctrl+K",
|
||||
"data-status-bar",
|
||||
"data-status-item=\"current-user\""
|
||||
],
|
||||
mustNotInclude: ["Open in editor"]
|
||||
},
|
||||
{
|
||||
name: "project settings route",
|
||||
url: `${baseUrl}/project-settings`,
|
||||
mustInclude: [
|
||||
"Project Settings",
|
||||
"Import Center",
|
||||
"REFERENCE_CONFIGURATION",
|
||||
"Reference config",
|
||||
"data-import-action=\"REFERENCE_CONFIGURATION:import\"",
|
||||
"data-import-action=\"XML_DUMP:check\"",
|
||||
"data-settings-section=\"docker-runtime-adapter\"",
|
||||
"data-settings-section=\"its-documentation-access\"",
|
||||
"Пользователи и доступ",
|
||||
"Интеграции задач",
|
||||
"Docker/runtime adapter",
|
||||
"ITS/documentation access",
|
||||
"Audit",
|
||||
"Backup/restore",
|
||||
"SFERA_ITS_URL",
|
||||
"SFERA_ITS_USERNAME",
|
||||
"SFERA_ITS_PASSWORD",
|
||||
"https://its.1c.ru/db/v838doc#browse:13:-1:7",
|
||||
".env.local",
|
||||
"<set locally>"
|
||||
],
|
||||
mustNotInclude: ["Открыть в редакторе"]
|
||||
},
|
||||
{
|
||||
name: "project settings route (en)",
|
||||
url: `${baseUrl}/project-settings?lang=en`,
|
||||
mustInclude: [
|
||||
"Project Settings",
|
||||
"Import Center",
|
||||
"REFERENCE_CONFIGURATION",
|
||||
"Reference config",
|
||||
"data-import-action=\"REFERENCE_CONFIGURATION:import\"",
|
||||
"data-import-action=\"XML_DUMP:check\"",
|
||||
"data-settings-section=\"task-session-policy\"",
|
||||
"data-settings-section=\"docker-runtime-adapter\"",
|
||||
"data-settings-section=\"its-documentation-access\"",
|
||||
"Task/session policy",
|
||||
"Docker/runtime adapter",
|
||||
"ITS/documentation access",
|
||||
"Audit",
|
||||
"Backup/restore",
|
||||
"SFERA_ITS_URL",
|
||||
"SFERA_ITS_USERNAME",
|
||||
"SFERA_ITS_PASSWORD",
|
||||
".env.local",
|
||||
"<set locally>"
|
||||
],
|
||||
mustNotInclude: ["Open in editor"]
|
||||
},
|
||||
{
|
||||
name: "module mode",
|
||||
url: `${baseUrl}/editor?lang=ru&project=${projectId}&mode=module&routine=${routine}`,
|
||||
mustInclude: ["data-ide-workspace", "data-left-navigation-panel", "data-right-context-inspector", "data-open-objects-bar", "data-open-document-pin", "data-open-document-close", "data-fallback-tree-search", "data-fallback-tree-filters", "Alt+1 Alt+2 Alt+3", "Редактор BSL", "Код модуля не загружен", "Выберите реальный модуль", "Основная конфигурация", "Расширение: <Имя>", "SFERA", "Среды"]
|
||||
},
|
||||
{
|
||||
name: "form mode",
|
||||
url: `${baseUrl}/editor?lang=ru&project=${projectId}&mode=form&routine=${routine}`,
|
||||
mustInclude: ["Дизайнер формы", "Провести и закрыть", "Товары"]
|
||||
},
|
||||
{
|
||||
name: "events mode",
|
||||
url: `${baseUrl}/editor?lang=ru&project=${projectId}&mode=events&routine=${routine}`,
|
||||
mustInclude: ["Инспектор событий", "ПриСозданииНаСервере", "ПередЗаписью"]
|
||||
},
|
||||
{
|
||||
name: "learning mode",
|
||||
url: `${baseUrl}/editor?lang=ru&project=${projectId}&mode=learning&routine=${routine}`,
|
||||
mustInclude: ["Обучение", "переменные доступны", "стандарты команды"]
|
||||
}
|
||||
];
|
||||
|
||||
for (const check of checks) {
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
try {
|
||||
await runCheck(check);
|
||||
console.log(`ok ${check.name}`);
|
||||
lastError = undefined;
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < attempts) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
async function runCheck(check) {
|
||||
const response = await fetch(check.url, { headers: { Accept: "text/html" } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`${check.name}: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
for (const expected of check.mustInclude ?? []) {
|
||||
if (!html.includes(expected)) {
|
||||
throw new Error(`${check.name}: missing "${expected}"`);
|
||||
}
|
||||
}
|
||||
for (const forbidden of check.mustNotInclude ?? []) {
|
||||
if (html.includes(forbidden)) {
|
||||
throw new Error(`${check.name}: unexpected "${forbidden}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
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<HTMLElement>("[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();
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { chromium } from "playwright-core";
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
const baseUrl = process.env.SFERA_WEB_URL ?? "http://localhost:3000";
|
||||
const projectId = process.env.SFERA_PROJECT_ID ?? "default";
|
||||
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.");
|
||||
}
|
||||
|
||||
await prepareIndexedProject();
|
||||
|
||||
const runtimeErrors = [];
|
||||
const browser = await chromium.launch({
|
||||
executablePath: browserPath,
|
||||
headless: true
|
||||
});
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
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}/project-settings`, { waitUntil: "load", timeout: 60000 });
|
||||
if (!response?.ok()) {
|
||||
throw new Error(`project setup smoke: ${response?.status()} ${response?.statusText()}`);
|
||||
}
|
||||
|
||||
await page.locator("[data-project-import-center]").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByRole("heading", { name: "Project Settings" }).waitFor({ state: "visible", timeout: 15000 });
|
||||
for (const section of ["Пользователи и доступ", "Интеграции задач", "Docker/runtime adapter", "Audit", "Backup/restore"]) {
|
||||
await page.getByText(section).first().waitFor({ state: "visible", timeout: 15000 });
|
||||
}
|
||||
for (const sectionMarker of ["docker-runtime-adapter", "its-documentation-access"]) {
|
||||
await page.locator(`[data-settings-section="${sectionMarker}"]`).waitFor({ state: "visible", timeout: 15000 });
|
||||
}
|
||||
await page.getByText("ITS/documentation access").first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("SFERA_ITS_URL").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("SFERA_ITS_USERNAME").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("SFERA_ITS_PASSWORD").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator("input[value*='its.1c.ru/db/v838doc']").first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText(".env.local").first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator("input[value='<set locally>']").first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator("select").filter({ hasText: "REFERENCE_CONFIGURATION" }).first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByRole("button", { name: /Reference config/ }).waitFor({ state: "visible", timeout: 15000 });
|
||||
const hasMetadataOnlyPrivacy = await page.locator("select").evaluateAll((selects) => {
|
||||
return selects.some((select) => select.value === "METADATA_ONLY" || [...select.options].some((option) => option.value === "METADATA_ONLY"));
|
||||
});
|
||||
if (!hasMetadataOnlyPrivacy) {
|
||||
throw new Error("project setup smoke: missing METADATA_ONLY privacy mode");
|
||||
}
|
||||
await page.getByText("Project Setup / Import Center").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("Режим работы с конфигурацией 1С").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator('[data-import-mode="FULL_REPLACE"]').waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator('[data-run-import-mode="SYNC_PREVIEW"]').click();
|
||||
const importMode = await page.evaluate(() => window.localStorage.getItem("sfera.import.mode"));
|
||||
if (importMode !== "SYNC_PREVIEW") {
|
||||
throw new Error(`project setup smoke: import mode was not persisted, got ${importMode}`);
|
||||
}
|
||||
await page.locator('[data-run-import-mode="FULL_REPLACE"]').click();
|
||||
await page.locator("[data-import-validation-panel]").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("Не заполнен путь к источнику").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator('input[placeholder*="/mnt/share/project"]').fill("/tmp/nonexistent-smoke-import");
|
||||
await page.locator('[data-run-import-mode="FULL_REPLACE"]').click();
|
||||
await page.getByText("Подтвердите полное обновление").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByRole("button", { name: "Отмена" }).click();
|
||||
await page.locator('[data-run-import-mode="SYNC_PREVIEW"]').click();
|
||||
await page.locator('[data-import-action="XML_DUMP:check"]').click();
|
||||
await page.locator("[data-import-check-panel]").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("Source preflight").waitFor({ state: "visible", timeout: 15000 });
|
||||
|
||||
const englishResponse = await page.goto(`${baseUrl}/project-settings?lang=en`, { waitUntil: "load", timeout: 60000 });
|
||||
if (!englishResponse?.ok()) {
|
||||
throw new Error(`project setup smoke (en): ${englishResponse?.status()} ${englishResponse?.statusText()}`);
|
||||
}
|
||||
await page.getByRole("heading", { name: "Project Settings" }).waitFor({ state: "visible", timeout: 15000 });
|
||||
for (const section of ["Task/session policy", "Docker/runtime adapter", "ITS/documentation access", "Audit", "Backup/restore"]) {
|
||||
await page.getByText(section).first().waitFor({ state: "visible", timeout: 15000 });
|
||||
}
|
||||
for (const sectionMarker of ["task-session-policy", "docker-runtime-adapter", "its-documentation-access"]) {
|
||||
await page.locator(`[data-settings-section="${sectionMarker}"]`).waitFor({ state: "visible", timeout: 15000 });
|
||||
}
|
||||
await page.locator("select").filter({ hasText: "REFERENCE_CONFIGURATION" }).first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByRole("button", { name: /Reference config/ }).waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("SFERA_ITS_URL").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("SFERA_ITS_USERNAME").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("SFERA_ITS_PASSWORD").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText(".env.local").first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator("input[value='<set locally>']").first().waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.locator('[data-import-action="XML_DUMP:check"]').click();
|
||||
await page.locator("[data-import-check-panel]").waitFor({ state: "visible", timeout: 15000 });
|
||||
await page.getByText("Source preflight").waitFor({ state: "visible", timeout: 15000 });
|
||||
|
||||
assertNoFatalRuntimeErrors();
|
||||
console.log("ok project setup runtime");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
async function prepareIndexedProject() {
|
||||
await postJson(`/api/sfera/projects/${encodeURIComponent(projectId)}/settings`, {
|
||||
name: "SFERA Smoke Project",
|
||||
structure_source: "XML_DUMP",
|
||||
platform_version: "8.3.24",
|
||||
compatibility_mode: "8.3.20"
|
||||
});
|
||||
await postJson(`/api/sfera/projects/${encodeURIComponent(projectId)}/imports/XML_DUMP`, {
|
||||
source: "XML_DUMP",
|
||||
metadata: {
|
||||
platform_version: "8.3.24",
|
||||
compatibility_mode: "8.3.20"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function postJson(path, body) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${path}: ${response.status} ${await response.text()}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function assertNoFatalRuntimeErrors() {
|
||||
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(`project setup runtime smoke failed:\n${fatalErrors.join("\n")}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user