Compare commits
5 commits
3a4d6ef79d
...
3274ca0f98
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3274ca0f98 | ||
|
|
a381f7f88e | ||
|
|
6d8151474c | ||
|
|
4f32ea0712 | ||
|
|
ab18383bf4 |
7 changed files with 121 additions and 5 deletions
|
|
@ -15,6 +15,15 @@ FRONTEND_DIR="$PROJECT_ROOT/web/frontend"
|
||||||
|
|
||||||
echo "[rebuild-frontend] Building frontend in $FRONTEND_DIR ..."
|
echo "[rebuild-frontend] Building frontend in $FRONTEND_DIR ..."
|
||||||
cd "$FRONTEND_DIR"
|
cd "$FRONTEND_DIR"
|
||||||
|
|
||||||
|
# KIN-122: auto npm install if package.json is newer than node_modules (or node_modules missing).
|
||||||
|
# This handles the case where agents add new imports/dependencies to package.json.
|
||||||
|
if [ ! -d "node_modules" ] || [ "package.json" -nt "node_modules" ]; then
|
||||||
|
echo "[rebuild-frontend] package.json changed or node_modules missing — running npm install ..."
|
||||||
|
npm install
|
||||||
|
echo "[rebuild-frontend] npm install complete."
|
||||||
|
fi
|
||||||
|
|
||||||
npm run build
|
npm run build
|
||||||
echo "[rebuild-frontend] Build complete."
|
echo "[rebuild-frontend] Build complete."
|
||||||
|
|
||||||
|
|
|
||||||
91
tests/test_kin_122_regression.py
Normal file
91
tests/test_kin_122_regression.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
"""
|
||||||
|
KIN-122 regression: rebuild-frontend.sh должен запускать npm install
|
||||||
|
перед npm run build, если package.json изменился (новее node_modules).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
SCRIPT_PATH = Path(__file__).parent.parent / "scripts" / "rebuild-frontend.sh"
|
||||||
|
|
||||||
|
|
||||||
|
class TestKIN122RebuildFrontendNpmInstall:
|
||||||
|
"""Структурные тесты: скрипт содержит условный npm install перед npm run build."""
|
||||||
|
|
||||||
|
def test_script_exists(self):
|
||||||
|
assert SCRIPT_PATH.is_file(), f"rebuild-frontend.sh not found at {SCRIPT_PATH}"
|
||||||
|
|
||||||
|
def test_script_is_executable(self):
|
||||||
|
mode = SCRIPT_PATH.stat().st_mode
|
||||||
|
assert mode & stat.S_IXUSR, "rebuild-frontend.sh должен быть исполняемым"
|
||||||
|
|
||||||
|
def test_script_contains_npm_install_conditional(self):
|
||||||
|
"""Скрипт должен содержать условный блок npm install (KIN-122)."""
|
||||||
|
content = SCRIPT_PATH.read_text()
|
||||||
|
assert "npm install" in content, (
|
||||||
|
"rebuild-frontend.sh должен содержать 'npm install'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_script_npm_install_guarded_by_condition(self):
|
||||||
|
"""npm install должен быть внутри if-блока, а не вызываться безусловно."""
|
||||||
|
content = SCRIPT_PATH.read_text()
|
||||||
|
lines = content.splitlines()
|
||||||
|
|
||||||
|
# Найти строку с npm install
|
||||||
|
npm_install_line_idx = next(
|
||||||
|
(i for i, line in enumerate(lines) if "npm install" in line and "if" not in line),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
assert npm_install_line_idx is not None, "Строка с 'npm install' не найдена"
|
||||||
|
|
||||||
|
# Должен быть if-блок выше
|
||||||
|
preceding = "\n".join(lines[:npm_install_line_idx])
|
||||||
|
assert "if" in preceding, (
|
||||||
|
"npm install должен быть внутри условного блока"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_script_checks_node_modules_existence(self):
|
||||||
|
"""Условие должно проверять наличие node_modules."""
|
||||||
|
content = SCRIPT_PATH.read_text()
|
||||||
|
assert "node_modules" in content, (
|
||||||
|
"Скрипт должен проверять наличие node_modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_script_checks_package_json_mtime(self):
|
||||||
|
"""Условие должно сравнивать mtime package.json с node_modules (флаг -nt)."""
|
||||||
|
content = SCRIPT_PATH.read_text()
|
||||||
|
assert "-nt" in content, (
|
||||||
|
"Скрипт должен использовать '-nt' для сравнения mtime package.json и node_modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_npm_install_before_npm_run_build(self):
|
||||||
|
"""npm install должен стоять раньше npm run build в скрипте."""
|
||||||
|
content = SCRIPT_PATH.read_text()
|
||||||
|
install_pos = content.find("npm install")
|
||||||
|
build_pos = content.find("npm run build")
|
||||||
|
assert install_pos != -1, "npm install не найден в скрипте"
|
||||||
|
assert build_pos != -1, "npm run build не найден в скрипте"
|
||||||
|
assert install_pos < build_pos, (
|
||||||
|
"npm install должен стоять раньше npm run build"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_npm_run_build_always_runs(self):
|
||||||
|
"""npm run build должен вызываться вне условного блока — всегда выполняется."""
|
||||||
|
content = SCRIPT_PATH.read_text()
|
||||||
|
lines = content.splitlines()
|
||||||
|
|
||||||
|
# Найти строку с npm run build (не внутри if-блока)
|
||||||
|
# Ищем строку, которая содержит "npm run build" и не является частью if-условия
|
||||||
|
build_lines = [
|
||||||
|
line for line in lines
|
||||||
|
if "npm run build" in line and line.strip().startswith("npm run build")
|
||||||
|
]
|
||||||
|
assert len(build_lines) >= 1, (
|
||||||
|
"npm run build должен быть безусловным вызовом"
|
||||||
|
)
|
||||||
|
|
@ -110,7 +110,7 @@ describe('KIN-UI-017: TaskDetail — statusColor() для статуса revisin
|
||||||
const header = wrapper.find('h1')
|
const header = wrapper.find('h1')
|
||||||
expect(header.exists()).toBe(true)
|
expect(header.exists()).toBe(true)
|
||||||
// Ищем Badge рядом с заголовком задачи — он должен быть orange, не gray/blue
|
// Ищем Badge рядом с заголовком задачи — он должен быть orange, не gray/blue
|
||||||
const grayBadgeInHeader = wrapper.find('.text-gray-400.text-xs.rounded')
|
const _grayBadgeInHeader = wrapper.find('.text-gray-400.text-xs.rounded')
|
||||||
// text-gray-400 может встречаться в других элементах, но мы проверяем наличие orange
|
// text-gray-400 может встречаться в других элементах, но мы проверяем наличие orange
|
||||||
const orangeBadge = wrapper.find('.text-orange-400')
|
const orangeBadge = wrapper.find('.text-orange-400')
|
||||||
expect(orangeBadge.exists()).toBe(true)
|
expect(orangeBadge.exists()).toBe(true)
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ describe('KIN-127: дерево задач — отступы', () => {
|
||||||
// Обёртка корневой задачи
|
// Обёртка корневой задачи
|
||||||
const taskWrapper = wrapper.find('div[style*="padding-left"]')
|
const taskWrapper = wrapper.find('div[style*="padding-left"]')
|
||||||
if (taskWrapper.exists()) {
|
if (taskWrapper.exists()) {
|
||||||
expect(taskWrapper.element.style.paddingLeft).toBe('0px')
|
expect((taskWrapper.element as HTMLElement).style.paddingLeft).toBe('0px')
|
||||||
} else {
|
} else {
|
||||||
// Если стиль не задан явно для 0 — это тоже приемлемо
|
// Если стиль не задан явно для 0 — это тоже приемлемо
|
||||||
expect(true).toBe(true)
|
expect(true).toBe(true)
|
||||||
|
|
@ -278,7 +278,7 @@ describe('KIN-127: дерево задач — отступы', () => {
|
||||||
w.find('a[href="/task/KIN-002"]').exists()
|
w.find('a[href="/task/KIN-002"]').exists()
|
||||||
)
|
)
|
||||||
expect(child1Wrapper?.exists()).toBe(true)
|
expect(child1Wrapper?.exists()).toBe(true)
|
||||||
expect(child1Wrapper?.element.style.paddingLeft).toBe('24px')
|
expect((child1Wrapper?.element as HTMLElement | undefined)?.style.paddingLeft).toBe('24px')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Задача второго уровня имеет paddingLeft 48px', async () => {
|
it('Задача второго уровня имеет paddingLeft 48px', async () => {
|
||||||
|
|
@ -304,7 +304,7 @@ describe('KIN-127: дерево задач — отступы', () => {
|
||||||
w.find('a[href="/task/KIN-003"]').exists()
|
w.find('a[href="/task/KIN-003"]').exists()
|
||||||
)
|
)
|
||||||
expect(child2Wrapper?.exists()).toBe(true)
|
expect(child2Wrapper?.exists()).toBe(true)
|
||||||
expect(child2Wrapper?.element.style.paddingLeft).toBe('48px')
|
expect((child2Wrapper?.element as HTMLElement | undefined)?.style.paddingLeft).toBe('48px')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
{ "path": "./tsconfig.node.json" }
|
{ "path": "./tsconfig.node.json" },
|
||||||
|
{ "path": "./tsconfig.vitest.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
web/frontend/tsconfig.vitest.json
Normal file
14
web/frontend/tsconfig.vitest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.node.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["node", "vitest/globals"],
|
||||||
|
"lib": ["ES2023", "DOM"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/__tests__/**/*.ts",
|
||||||
|
"src/**/__tests__/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -13,5 +13,6 @@ export default defineConfig({
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['./src/__tests__/vitest-setup.ts'],
|
setupFiles: ['./src/__tests__/vitest-setup.ts'],
|
||||||
|
typecheck: { tsconfig: './tsconfig.vitest.json' },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue