테스트 파일 삭제

This commit is contained in:
2026-09-18 16:35:21 +09:00
parent 91576f401b
commit 49ace44c9b
15 changed files with 0 additions and 2341 deletions
-100
View File
@@ -1,100 +0,0 @@
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
const APP_DIR = path.resolve("apps/sn/app");
const POPUP_DEFINITIONS = path.resolve(
"apps/sn/src/features/windowPopup/model/popupDefinitions.ts",
);
const POPUP_REGISTRY = path.resolve(
"apps/sn/src/features/windowPopup/registry.tsx",
);
const routeAliases = new Set([
"/map/agedPipePopup",
"/map2",
]);
const developmentRoutes = new Set([
"/ii/fileupload",
"/ii/test",
"/oper/operStore",
]);
const infrastructureRoutes = new Set(["/", "/popup/[popupKey]"]);
async function collectPageFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = await Promise.all(
entries.map(async (entry) => {
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) return collectPageFiles(absolutePath);
return entry.name === "page.tsx" ? [absolutePath] : [];
}),
);
return nested.flat();
}
function toRoute(filePath) {
const relative = path.relative(APP_DIR, path.dirname(filePath));
const segments = relative
.split(path.sep)
.filter(Boolean)
.filter((segment) => !/^\(.+\)$/.test(segment));
return `/${segments.join("/")}`.replace(/\/$/, "") || "/";
}
function extractDefinitionKeys(source) {
const objectStart = source.indexOf("export const windowPopupDefinitions = {");
const objectEnd = source.indexOf("} as const satisfies", objectStart);
return [...source.slice(objectStart, objectEnd).matchAll(/^ ([A-Za-z][A-Za-z0-9]*): \{/gm)]
.map((match) => match[1]);
}
function extractRegistryKeys(source) {
const objectStart = source.indexOf("export const windowPopupRegistry = {");
const objectEnd = source.indexOf("} satisfies", objectStart);
return [...source.slice(objectStart, objectEnd).matchAll(/^ ([A-Za-z][A-Za-z0-9]*):/gm)]
.map((match) => match[1]);
}
const pageFiles = await collectPageFiles(APP_DIR);
const routes = pageFiles.map(toRoute).sort();
const definitions = extractDefinitionKeys(
await readFile(POPUP_DEFINITIONS, "utf8"),
);
const registry = extractRegistryKeys(await readFile(POPUP_REGISTRY, "utf8"));
const popupMissingInRegistry = definitions.filter((key) => !registry.includes(key));
const registryMissingInDefinitions = registry.filter(
(key) => !definitions.includes(key),
);
const routeCategory = (route) => {
if (route === "/auth/login") return "authentication";
if (routeAliases.has(route)) return "alias-or-preview";
if (developmentRoutes.has(route)) return "development";
if (infrastructureRoutes.has(route)) return "infrastructure";
return "main-content";
};
const routeCounts = routes.reduce((groups, route) => {
const category = routeCategory(route);
groups[category] ??= [];
groups[category].push(route);
return groups;
}, {});
const report = {
routeFileCount: routes.length,
routeCounts: Object.fromEntries(
Object.entries(routeCounts).map(([key, value]) => [key, value.length]),
),
routes: routes.map((route) => ({ route, category: routeCategory(route) })),
popupDefinitionCount: definitions.length,
popupRegistryCount: registry.length,
popupMissingInRegistry,
registryMissingInDefinitions,
popupKeys: definitions,
};
console.log(JSON.stringify(report, null, 2));
if (popupMissingInRegistry.length || registryMissingInDefinitions.length) {
process.exitCode = 1;
}
-165
View File
@@ -1,165 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const ts = require("typescript");
// 2~4차가 아직 사용하는 기존 기반 helper만 파일·모듈 쌍으로 좁게 허용한다.
const LEGACY_DEPENDENCIES = {
"src/features/dredging/api/dredging-api.ts": ["@/components/lib/api"],
"src/features/dredging/api/photo-upload.ts": ["@/components/lib/utils"],
"src/features/dredging/lib/use-dredging-common-codes.ts": [
"@/components/lib/common/apiHelpers",
],
};
const LEGACY_DIRECTORY = "app/(with-base-layout)/oper/dredging/lib/";
const LEGACY_FILES = [
"client",
"types",
"uuid",
"date",
"exif",
"registration-request",
"useDredgingCommonCodes",
].map((name) => `${LEGACY_DIRECTORY}${name}.ts`);
/** 정적·동적 import, export-from, require의 문자열 모듈 참조를 수집한다. */
function moduleSpecifiers(sourceFile) {
const modules = [];
function visit(node) {
if (
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
modules.push(node.moduleSpecifier.text);
}
if (
ts.isCallExpression(node) &&
node.arguments.length &&
ts.isStringLiteral(node.arguments[0]) &&
(node.expression.kind === ts.SyntaxKind.ImportKeyword ||
(ts.isIdentifier(node.expression) &&
node.expression.text === "require"))
) {
modules.push(node.arguments[0].text);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return modules;
}
/** 새 feature의 모델·기반과 legacy 진입점에만 검증 범위를 한정한다. */
function validateSource(relativePath, content) {
const sourceFile = ts.createSourceFile(
relativePath,
content,
ts.ScriptTarget.Latest,
true,
);
const errors = [];
if (LEGACY_FILES.includes(relativePath)) {
if (
!sourceFile.statements.length ||
sourceFile.statements.some(
(statement) =>
!ts.isExportDeclaration(statement) ||
!statement.moduleSpecifier ||
!ts.isStringLiteral(statement.moduleSpecifier) ||
!statement.moduleSpecifier.text.startsWith("@/features/dredging/"),
)
) {
errors.push(
`${relativePath}: legacy 파일은 준설 feature 재수출만 허용합니다.`,
);
}
}
for (const specifier of moduleSpecifiers(sourceFile)) {
const resolved = specifier.startsWith("@/")
? `src/${specifier.slice(2)}`
: specifier.startsWith(".")
? path.posix.normalize(
path.posix.join(path.posix.dirname(relativePath), specifier),
)
: specifier;
if (
/^src\/features\/(dredging|mapLayers|gisManagement|gisEdit)\//.test(relativePath)
) {
if (specifier.startsWith("@/app/") || resolved.startsWith("app/")) {
errors.push(`${relativePath}: app 라우트 역참조 금지 (${specifier})`);
}
if (
specifier.startsWith("@/components/") &&
!(LEGACY_DEPENDENCIES[relativePath] ?? []).includes(specifier)
) {
errors.push(
`${relativePath}: 승인되지 않은 legacy 기반 의존 (${specifier})`,
);
}
if (
relativePath.includes("/model/") &&
/(?:^|\/)(?:api|ui|app|components)(?:\/|$)/.test(resolved)
) {
errors.push(
`${relativePath}: model이 API/UI/라우트에 의존할 수 없습니다 (${specifier})`,
);
}
}
if (
relativePath === "src/shared/api/http-transport.ts" &&
/^(?:src\/)?(?:features|entities|app|components)\//.test(resolved)
) {
errors.push(
`${relativePath}: transport의 상위 계층 역참조 금지 (${specifier})`,
);
}
}
return errors;
}
/** 신규 feature만 재귀 검사한다. 타 앱·공용 패키지의 부채는 이 게이트에 혼합하지 않는다. */
function sourceFiles(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const filename = path.join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(filename)
: /\.tsx?$/.test(entry.name)
? [filename]
: [];
});
}
function checkWorkspace(root) {
const files = [
...LEGACY_FILES.map((relative) => path.join(root, relative)),
...sourceFiles(path.join(root, "src/features/dredging")),
...sourceFiles(path.join(root, "src/features/mapLayers")),
...sourceFiles(path.join(root, "src/features/gisManagement")),
...sourceFiles(path.join(root, "src/features/gisEdit")),
path.join(root, "src/shared/api/http-transport.ts"),
];
const errors = files.flatMap((filename) =>
validateSource(
path.relative(root, filename).split(path.sep).join("/"),
fs.readFileSync(filename, "utf8"),
),
);
return {
checkedFiles: files.length,
legacyDependencies: LEGACY_DEPENDENCIES,
errors,
};
}
if (require.main === module) {
const result = checkWorkspace(path.resolve(__dirname, ".."));
console.log(
`SN module boundaries: ${result.checkedFiles} files, ${result.errors.length} violations`,
);
console.log(
"Retained legacy bridges: dredging-api -> API utility; photo-upload -> TUS utility; common-code hook -> CMS helper",
);
for (const error of result.errors) console.error(error);
if (result.errors.length) process.exitCode = 1;
}
module.exports = { validateSource, checkWorkspace };
-18
View File
@@ -1,18 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
// Windows 셸의 glob 확장 여부와 무관하게 실제 테스트 파일을 Node runner에 전달한다.
const root = path.resolve(__dirname, "..");
const files = fs
.readdirSync(path.join(root, "tests"))
.filter((name) => name.endsWith(".test.cjs"))
.sort()
.map((name) => path.join(root, "tests", name));
if (!files.length) throw new Error("No refactor tests found");
const result = spawnSync(process.execPath, ["--test", ...files], {
cwd: root,
stdio: "inherit",
});
if (result.error) throw result.error;
process.exitCode = result.status ?? 1;
-189
View File
@@ -1,189 +0,0 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
// Legacy facade를 실제로 실행하고 인증 외부 의존성만 고정한다.
const transport = loadTypeScript("src/shared/api/http-transport.ts");
const api = loadTypeScript("app/components/lib/api/index.ts", {
"@/shared/api/http-transport": transport,
"@/shared/config": { SITE_ID: "SN-TEST", CLAIM_NAME: "Authorization" },
"@/entities/auth/lib/token-manager": { tokenManager: {} },
"@common/auth": { getRemainingMs: () => 0 },
});
for (const method of [
"getData",
"postData",
"putData",
"deleteData",
"getRawJson",
"postRawJson",
]) {
test(`${method}: HTTP 500 JSON 오류를 status/body와 함께 reject`, async (t) => {
const body = { message: "저장 실패", data: null };
t.mock.method(globalThis, "fetch", async () =>
Response.json(body, { status: 500 }),
);
await assert.rejects(api[method]("v1/test", {}), (error) => {
assert.equal(error.status, 500);
assert.equal(error.message, "저장 실패");
assert.deepEqual(error.body, body);
return true;
});
});
}
test("HTML 502 오류가 JSON 파싱 오류로 바뀌지 않는다", async (t) => {
t.mock.method(
globalThis,
"fetch",
async () => new Response("<html>Bad Gateway</html>", { status: 502 }),
);
await assert.rejects(api.postRawJson("v1/test", {}), (error) => {
assert.equal(error.status, 502);
assert.equal(error.body, "<html>Bad Gateway</html>");
assert.match(error.message, /502/);
assert.doesNotMatch(error.message, /<html>/);
return true;
});
});
test("nested 오류 메시지와 비어 있는 오류 응답", async (t) => {
const fetchMock = t.mock.method(globalThis, "fetch", async () =>
Response.json({ error: { message: "권한 없음" } }, { status: 403 }),
);
await assert.rejects(api.deleteData("v1/test"), {
message: "권한 없음",
status: 403,
});
fetchMock.mock.mockImplementation(
async () => new Response(null, { status: 503 }),
);
await assert.rejects(api.postData("v1/test", {}), {
status: 503,
body: null,
});
});
test("POST 성공 래퍼를 해제하고 URL·사이트 헤더·JSON body 유지", async (t) => {
const calls = [];
t.mock.method(globalThis, "fetch", async (url, init) => {
calls.push({ url, init });
return Response.json({ status: 201, data: { id: 9 } }, { status: 201 });
});
assert.deepEqual(await api.postData("v1/test", { fcltMngNo: "P-1" }), {
id: 9,
});
assert.equal(calls[0].url, "/sewerage-service/api/v1/test");
assert.equal(calls[0].init.headers.get("X-Site-Id"), "SN-TEST");
assert.equal(calls[0].init.headers.get("Content-Type"), "application/json");
assert.deepEqual(JSON.parse(calls[0].init.body), { fcltMngNo: "P-1" });
await api.postData("/attach-service/custom", {});
assert.equal(calls[1].url, "/attach-service/custom");
});
test("204 정상: JSON 함수 null, DELETE true", async (t) => {
t.mock.method(
globalThis,
"fetch",
async () => new Response(null, { status: 204 }),
);
for (const method of [
"getData",
"postData",
"putData",
"getRawJson",
"postRawJson",
]) {
assert.equal(await api[method]("v1/test", {}), null);
}
assert.equal(await api.deleteData("v1/test"), true);
});
test("DELETE 200 성공 Response는 본문을 읽지 않은 채 반환", async (t) => {
t.mock.method(globalThis, "fetch", async () =>
Response.json({ deleted: true }),
);
const result = await api.deleteData("v1/test");
assert.ok(result instanceof Response);
assert.equal(result.status, 200);
assert.equal(result.bodyUsed, false);
assert.deepEqual(await result.json(), { deleted: true });
});
test("실패한 삭제 이후 성공 알림·두 번째 삭제를 실행하지 않는다", async (t) => {
let calls = 0;
let success = false;
t.mock.method(globalThis, "fetch", async () => {
calls++;
return new Response(null, { status: 500 });
});
await assert.rejects(async () => {
await api.deleteData("v1/first");
await api.deleteData("v1/second");
success = true;
});
assert.equal(calls, 1);
assert.equal(success, false);
});
test("raw GeoJSON·필드 이름 및 PUT raw/wrapper 계약 유지", async (t) => {
const geojson = {
type: "FeatureCollection",
features: [],
fclt_mng_no: "P",
fcltMngNo: "Q",
};
const fetchMock = t.mock.method(globalThis, "fetch", async () =>
Response.json(geojson),
);
assert.deepEqual(await api.getRawJson("/geoserver/test"), geojson);
assert.deepEqual(await api.putData("v1/test", {}), geojson);
fetchMock.mock.mockImplementation(async () => Response.json({ data: null }));
assert.equal(await api.putData("v1/test", {}), null);
});
test("정상 HTTP에서 HTML 본문은 성공 데이터로 취급하지 않는다", async (t) => {
t.mock.method(
globalThis,
"fetch",
async () => new Response("<html>login</html>"),
);
await assert.rejects(api.getData("v1/test"), {
status: 200,
body: "<html>login</html>",
});
});
test("Headers 객체, 사이트 헤더 및 AbortSignal을 보존하고 취소 예외를 유지", async (t) => {
const controller = new AbortController();
const abort = new DOMException("cancelled", "AbortError");
t.mock.method(globalThis, "fetch", async (_url, init) => {
assert.equal(init.signal, controller.signal);
assert.equal(init.headers.get("X-Custom"), "retained");
assert.equal(init.headers.get("X-Site-Id"), "SN-TEST");
throw abort;
});
await assert.rejects(
api.getRawJson("v1/test", {
signal: controller.signal,
headers: new Headers({ "X-Custom": "retained" }),
}),
(error) => error === abort,
);
});
test("401에서 주입한 인증 callback을 한 번 실행한 뒤 reject", async (t) => {
let count = 0;
const client = transport.createHttpTransport({
baseUrl: "/api",
onUnauthorized: () => count++,
});
t.mock.method(
globalThis,
"fetch",
async () => new Response(null, { status: 401 }),
);
await assert.rejects(client.postData("test", {}), { status: 401 });
assert.equal(count, 1);
});
-146
View File
@@ -1,146 +0,0 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const root = path.resolve(__dirname, "..");
// 제거된 서버 alias가 실제 SN 호출 코드·URL 주석에 다시 들어오지 않도록 검증한다.
const retiredSegments = [
"gis-upload",
"ppl-analysis",
"flood-dngr-map",
"sbmrg-dngr-map",
"rain-anls",
"depth-map",
"grid-map",
"aged-status-by-ctpv",
"swage-prcs-zones",
"repair-histories",
"savewithfile",
"fcltppl",
"fcltmhl",
"fcltpump",
"fcltstofcbr",
];
function sourceFiles(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const filename = path.join(directory, entry.name);
return entry.isDirectory()
? sourceFiles(filename)
: /\.(?:tsx?|jsx?)$/.test(entry.name)
? [filename]
: [];
});
}
test("SN 실서비스와 business_components 호출은 단일 API 경로만 사용", () => {
const violations = [];
const sources = [];
for (const filename of [
...sourceFiles(path.join(root, "app")),
...sourceFiles(path.join(root, "src")),
...sourceFiles(
path.resolve(root, "../../packages/common/business_components/src"),
),
]) {
const source = fs.readFileSync(filename, "utf8");
sources.push(source);
for (const segment of retiredSegments) {
if (source.includes(`/${segment}`))
violations.push(`${path.relative(root, filename)}: ${segment}`);
}
}
assert.deepEqual(violations, []);
const allSources = sources.join("\n");
for (const segment of [
"pplAnalysis",
"floodDngrMap",
"sbmrgDngrMap",
"rainAnls",
"depthMap",
"gridMap",
"agedStatusByCtpv",
"saveWithFile",
"fcltPpl",
"fcltMhl",
"fcltPump",
"fcltStofcbr",
]) {
assert.ok(
allSources.includes(`/${segment}`),
`canonical API missing: ${segment}`,
);
}
});
test("침수 상세 API는 rainAnls/depthMap을 호출하고 기존 응답 필드를 보존", async () => {
const calls = [];
const client = loadTypeScript(
"app/(map-layout)/map/components/fldngHstry/fldngHstryClient.ts",
{
"@/components/lib/api": {
getData: async (url) => {
calls.push(url);
return url.includes("/rainAnls/")
? { rain_bgng_tm: "2026-09-11 01:00", rain_tot: 0 }
: {
depthMaps: [{ fid: "depth" }],
waterLevelMaps: [{ fid: "level" }],
};
},
},
},
);
const result = await client.loadFldngHstryDetail("F/1");
assert.deepEqual(calls, [
"v1/fldng/hstry/rainAnls/F%2F1",
"v1/fldng/hstry/depthMap/F%2F1",
]);
assert.equal(result.rainAnalysis.rain_bgng_tm, "2026-09-11 01:00");
assert.equal(result.rainAnalysis.rainBgngTm, "2026-09-11 01:00");
assert.equal(result.rainAnalysis.rain_tot, 0);
assert.equal(result.rainAnalysis.rainTot, 0);
assert.deepEqual(result.fldngHstryDepthRows, [{ fid: "depth" }]);
assert.deepEqual(result.fldngHstryWtlvRows, [{ fid: "level" }]);
assert.equal(result.complete, true);
});
test("SN proxy는 gisUpload 단일 route이며 BE GET 목록·POST·하위 작업을 구분", () => {
const screen = fs.readFileSync(
path.join(
root,
"app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx",
),
"utf8",
);
const proxy = fs.readFileSync(
path.join(root, "app/api/gisUpload/route.ts"),
"utf8",
);
assert.ok(screen.includes("v1/map/gisUploadHistories?"));
assert.ok(screen.includes('"/api/gisUpload"'));
assert.ok(!screen.includes('"/api/gis-upload"'));
assert.equal(fs.existsSync(path.join(root, "app/api/gis-upload/route.ts")), false);
assert.ok(!screen.includes("v1/map/gisUploads?"));
assert.ok(screen.includes("v1/map/gisUploads/swagePpl/downloadJobs?"));
assert.ok(proxy.includes('"/sewerage-service/api/v1/map/gisUploads"'));
assert.ok(proxy.includes("export async function POST"));
});
test("서비스 식별자·CMS 계약은 보존되고 fclt+suffix 동적 조합이 없다", () => {
const api = fs.readFileSync(
path.join(root, "app/components/lib/api/index.ts"),
"utf8",
);
const helpers = fs.readFileSync(
path.join(root, "app/components/lib/common/apiHelpers.ts"),
"utf8",
);
assert.ok(api.includes("/sewerage-service/api"));
assert.ok(api.includes("/cms-service"));
assert.ok(api.includes("/geoserver"));
assert.ok(helpers.includes("/api/v1/code/cache/"));
assert.doesNotMatch(helpers, /fclt\$\{|["']fclt["']\s*\+/);
});
@@ -1,88 +0,0 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const ts = require("typescript");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const root = path.resolve(__dirname, "..");
const legacy = "app/(with-base-layout)/oper/dredging/lib/";
test("기존 준설 lib 7파일은 feature 구현을 재수출하며 중복 구현을 보유하지 않는다", () => {
for (const name of [
"client",
"types",
"uuid",
"date",
"exif",
"registration-request",
"useDredgingCommonCodes",
]) {
const source = ts.createSourceFile(
name,
fs.readFileSync(path.join(root, legacy, `${name}.ts`), "utf8"),
ts.ScriptTarget.Latest,
true,
);
assert.ok(source.statements.length > 0);
for (const statement of source.statements) {
assert.ok(
ts.isExportDeclaration(statement),
`${name} contains an implementation`,
);
assert.match(statement.moduleSpecifier.text, /^@\/features\/dredging\//);
}
}
});
test("feature 내부는 옛 준설 라우트·UI 모듈을 역참조하지 않는다", () => {
const featureRoot = path.join(root, "src/features/dredging");
for (const entry of fs.readdirSync(featureRoot, {
recursive: true,
withFileTypes: true,
})) {
if (!entry.isFile() || !entry.name.endsWith(".ts")) continue;
const filename = path.join(entry.parentPath, entry.name);
const source = fs.readFileSync(filename, "utf8");
assert.doesNotMatch(
source,
/(?:@\/app\/|oper\/dredging|DredgingRegisterModal|DredgingDetailPopupPage)/,
filename,
);
}
const popup = fs.readFileSync(
path.join(root, "src/features/windowPopup/ui/dredgingPopupRecord.ts"),
"utf8",
);
assert.match(popup, /from "@\/features\/dredging"/);
});
test("기존 date/uuid/EXIF 경로에서 이동된 구현을 호출할 수 있다", async () => {
const date = loadTypeScript(`${legacy}date.ts`);
assert.equal(
date.formatDredgingCapturedDate("2024-02-29T10:11:12"),
"2024-02-29",
);
assert.equal(date.formatDredgingCapturedDate("2025-02-29"), null);
const uuid = loadTypeScript(`${legacy}uuid.ts`);
assert.match(
uuid.createDredgingClientUuid(),
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
const exif = loadTypeScript(`${legacy}exif.ts`);
assert.equal(
(await exif.readPhotoMetadata({ name: "test.png", type: "image/png" }))
.status,
"UNSUPPORTED",
);
assert.equal(
(
await exif.readPhotoMetadata({
name: "test.jpg",
type: "image/jpeg",
arrayBuffer: async () =>
Uint8Array.from([0xff, 0xd8, 0xff, 0xd9]).buffer,
})
).status,
"MISSING_BOTH",
);
});
@@ -1,163 +0,0 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const base = "app/(with-base-layout)/oper/dredging/lib/";
const { createDredgingRegistrationSession } = loadTypeScript(
`${base}registration-request.ts`,
);
/** 실제 wire 필드 외에 화면 전용 ID·관경·거리까지 포함해 구분을 검증한다. */
const fixture = () => [
{
localId: "ui-group",
registrationMode: "PHOTO",
dredgingDate: "2026-09-01",
locationSource: "EXIF",
location: { longitude: 127.1, latitude: 37.1 },
pipe: {
fid: "F-1",
mngInstCd: "4173000",
fcltMngNo: "P-1",
pipeDiameterMm: 600,
distanceMeters: 1,
},
photos: [
{
key: "photo-1",
file: { name: "관로.jpg" },
metadata: {
capturedAt: "2026-09-01T09:10:00",
location: { longitude: 127.1, latitude: 37.1 },
},
},
],
},
];
const attachments = () => new Map([["photo-1", { atflCd: "A", atflId: "B" }]]);
test("HTTP 응답 유실 후 재시도는 동일 payload와 clientRequestId를 전송", async () => {
let issued = 0;
const session = createDredgingRegistrationSession(
() => `request-${++issued}`,
);
const requests = [];
const client = loadTypeScript(`${base}client.ts`, {
"@/components/lib/api": {
postRawJson: async (url, body) => {
requests.push({ url, body });
if (requests.length === 1) throw new TypeError("network lost");
return { alreadyRegisteredCount: 1 };
},
},
"@/components/lib/utils": {},
"@/shared/lib/fetch-all-pages": {},
});
await assert.rejects(
client.saveDredgingRegistrations(session.prepare(fixture(), attachments())),
);
await client.saveDredgingRegistrations(
session.prepare(fixture(), attachments()),
);
assert.equal(issued, 1);
assert.deepEqual(requests[0], requests[1]);
assert.equal(requests[0].url, "v1/dredging/registrations");
assert.deepEqual(requests[0].body.groups[0].photos[0], {
atflCd: "A",
atflId: "B",
originalFileName: "관로.jpg",
clientCapturedAt: "2026-09-01T09:10:00",
clientLongitude: 127.1,
clientLatitude: 37.1,
});
});
test("날짜·위치·관로·사진 변경은 새 ID, 원래 입력 복원은 기존 ID", () => {
let issued = 0;
const session = createDredgingRegistrationSession(
() => `request-${++issued}`,
);
const first = session.prepare(fixture(), attachments()).clientRequestId;
const edits = [
(group) => {
group.dredgingDate = "2026-09-02";
},
(group) => {
group.location.latitude = 37.2;
},
(group) => {
group.pipe.fid = "F-2";
},
(group) => {
group.photos[0].metadata.capturedAt = "2026-09-01T10:00:00";
},
];
for (const edit of edits) {
const groups = fixture();
edit(groups[0]);
assert.notEqual(
session.prepare(groups, attachments()).clientRequestId,
first,
);
}
const changedAttachments = attachments();
changedAttachments.set("photo-1", { atflCd: "C", atflId: "D" });
assert.notEqual(
session.prepare(fixture(), changedAttachments).clientRequestId,
first,
);
assert.equal(
session.prepare(fixture(), attachments()).clientRequestId,
first,
);
});
test("화면 전용 그룹 ID·관경·거리 변화는 중복방지 키를 바꾸지 않는다", () => {
let issued = 0;
const session = createDredgingRegistrationSession(
() => `request-${++issued}`,
);
const first = session.prepare(fixture(), attachments());
const groups = fixture();
groups[0].localId = "new-ui";
groups[0].pipe.pipeDiameterMm = 900;
groups[0].pipe.distanceMeters = 3;
assert.deepEqual(session.prepare(groups, attachments()), first);
});
test("새 등록 세션은 같은 입력이라도 새 UUID를 발급", () => {
const first = createDredgingRegistrationSession().prepare(
fixture(),
attachments(),
).clientRequestId;
const second = createDredgingRegistrationSession().prepare(
fixture(),
attachments(),
).clientRequestId;
assert.notEqual(first, second);
assert.match(
first,
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
});
test("첨부 객체의 속성 순서가 달라도 같은 등록 ID를 재사용", () => {
let issued = 0;
const session = createDredgingRegistrationSession(() => `request-${++issued}`);
const first = session.prepare(fixture(), attachments());
const reordered = new Map([["photo-1", { atflId: "B", atflCd: "A" }]]);
assert.deepEqual(session.prepare(fixture(), reordered), first);
assert.equal(issued, 1);
});
test("실제 저장의 누락 첨부는 키 발급 전 거부하며 선행 모의 모드는 허용", () => {
let issued = 0;
const session = createDredgingRegistrationSession(
() => `request-${++issued}`,
);
assert.throws(() => session.prepare(fixture(), new Map()), /첨부 식별자/);
assert.equal(issued, 0);
assert.equal(
session.prepare(fixture(), new Map(), false).clientRequestId,
"request-1",
);
});
@@ -1,171 +0,0 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const apiPath = "src/features/dredging/api/dredging-api.ts";
const adapters = loadTypeScript(
"src/features/dredging/api/response-adapters.ts",
);
test("준설 페이지 raw/wrapper의 page0·total0·items 참조와 필드를 보존", () => {
const page = { items: [], total: 0, page: 0, size: 2000, extra: "retained" };
assert.equal(adapters.toDredgingRecordPage(page), page);
assert.equal(adapters.toDredgingRecordPage({ data: page }), page);
assert.deepEqual(page, {
items: [],
total: 0,
page: 0,
size: 2000,
extra: "retained",
});
});
test("준설사진의 원본 촬영일시·첨부필드는 유지하고 기존 null fallback 적용", () => {
const photos = [
{ photoSn: 1, capturedAt: "2026-09-01T12:13:14", atflCd: "A", atflId: "B" },
];
assert.equal(adapters.toDredgingPhotos(photos), photos);
assert.equal(adapters.toDredgingPhotos({ data: photos }), photos);
assert.deepEqual(adapters.toDredgingPhotos(null), []);
assert.deepEqual(adapters.toDredgingPhotos(undefined), []);
});
test("최근접관로의 식별자·위치·0값·nullable 기본값과 raw/wrapper를 유지", () => {
const row = {
locationKey: "photo-1",
selected: {
fid: "F-1",
mngInstCd: "4173000",
fcltMngNo: "P-1",
distanceMeters: 0,
snappedLongitude: 127,
snappedLatitude: 37,
},
};
const expected = {
fid: "F-1",
mngInstCd: "4173000",
fcltMngNo: "P-1",
pipeDiameterMm: 0,
pipeLengthM: 0,
pipeMaterial: "",
installationYear: null,
swtrmtZoneFid: null,
swtrmtZoneNm: "",
distanceMeters: 0,
snappedLongitude: 127,
snappedLatitude: 37,
isMock: false,
};
assert.deepEqual(
adapters.toNearestPipeMatches([row]).get("photo-1"),
expected,
);
assert.deepEqual(
adapters.toNearestPipeMatches({ data: [row] }).get("photo-1"),
expected,
);
assert.equal(adapters.toNearestPipeMatches(null).size, 0);
});
test("최근접관로 부분 실패는 성공 Map을 반환하지 않고 중복 안내를 합친다", () => {
const rows = [
{ locationKey: "A", failureCode: "PIPE_NOT_FOUND" },
{ locationKey: "B", failureCode: "PIPE_NOT_FOUND" },
];
assert.throws(() => adapters.toNearestPipeMatches(rows), {
message:
"50m 이내에 관로가 없습니다. 지도에서 준설위치를 다시 확인해주세요.",
});
assert.throws(
() =>
adapters.toNearestPipeMatches([
{ locationKey: "A", failureMessage: " 원인 안내 " },
]),
{ message: "원인 안내" },
);
});
test("실제 API 목록/사진이 기존 URL과 query를 사용하고 adapter로 응답 반환", async () => {
const calls = [];
const page = { items: [{ id: "one" }], page: 0, size: 10, total: 1 };
const api = loadTypeScript(apiPath, {
"@/components/lib/api": {
getRawJson: async (url) => {
calls.push(url);
return { data: url.includes("/photos?") ? [] : page };
},
},
"@/shared/lib/fetch-all-pages": {},
});
assert.equal(
await api.fetchDredgingRecords({
fromDate: "2026-09-01",
toDate: "2026-09-10",
ctpv: " 경기 ",
page: 0,
size: 10,
}),
page,
);
assert.equal(
calls[0],
"v1/dredging/records?fromDate=2026-09-01&toDate=2026-09-10&page=0&size=10&ctpv=%EA%B2%BD%EA%B8%B0",
);
assert.deepEqual(await api.fetchDredgingPhotos("job/1", "pipe 1"), []);
assert.equal(calls[1], "v1/dredging/records/job%2F1/photos?fid=pipe%201");
});
test("실제 전체페이지 조회는 page0부터 순회하며 row·total을 유지", async () => {
const pages = [];
const api = loadTypeScript(apiPath, {
"@/components/lib/api": {
getRawJson: async (url) => {
const query = new URL(url, "http://fixture").searchParams;
const page = Number(query.get("page"));
pages.push(page);
return {
data: {
items: page === 0 ? [{ id: "A" }, { id: "B" }] : [{ id: "C" }],
total: 3,
page,
size: 2000,
},
};
},
},
"@/shared/lib/fetch-all-pages": loadTypeScript(
"src/shared/lib/fetch-all-pages.ts",
),
});
const result = await api.fetchAllDredgingRecords({
fromDate: "2026-09-01",
toDate: "2026-09-10",
});
assert.deepEqual(result, {
items: [{ id: "A" }, { id: "B" }, { id: "C" }],
total: 3,
});
assert.deepEqual(pages, [0, 1]);
});
test("BE409 충돌은 자동 재시도하지 않고 기존 준설 실패 경로로 전달", async (t) => {
t.mock.method(console, "warn", () => {});
let calls = 0;
const api = loadTypeScript(apiPath, {
"@/components/lib/api": {
postRawJson: async () => {
calls++;
throw Object.assign(new Error("conflict"), {
status: 409,
body: { error: { code: "DREDGING_REQUEST_CONFLICT" } },
});
},
},
"@/shared/lib/fetch-all-pages": {},
});
await assert.rejects(
api.saveDredgingRegistrations({ clientRequestId: "same-key", groups: [] }),
{ message: "준설결과를 저장하지 못했습니다. 잠시 후 다시 시도해주세요." },
);
assert.equal(calls, 1);
});
-171
View File
@@ -1,171 +0,0 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const { uploadPhotosWithReferences } = loadTypeScript(
"src/features/dredging/lib/photo-upload-queue.ts",
);
const photos = () =>
["one", "two", "three"].map((key) => ({ key, file: { name: `${key}.jpg` } }));
test("부분 업로드 성공 참조는 실패 후에도 원본 Map에 남고 재시도는 실패 사진부터 진행", async () => {
const references = new Map();
const calls = [];
let failSecond = true;
const upload = async (file) => {
calls.push(file.name);
if (file.name === "two.jpg" && failSecond) {
failSecond = false;
throw new Error("upload failed");
}
return { atflCd: file.name, atflId: `id-${file.name}` };
};
await assert.rejects(
uploadPhotosWithReferences(photos(), references, upload),
);
assert.deepEqual([...references.keys()], ["one"]);
assert.equal(
await uploadPhotosWithReferences(photos(), references, upload),
references,
);
assert.deepEqual(calls, ["one.jpg", "two.jpg", "two.jpg", "three.jpg"]);
assert.deepEqual([...references.keys()], ["one", "two", "three"]);
});
test("기존 성공·빈 목록·같은 사진 키의 중복은 추가 업로드를 발생시키지 않는다", async () => {
let calls = 0;
const references = new Map([["one", { atflCd: "A", atflId: "B" }]]);
const upload = async () => {
calls++;
return { atflCd: "C", atflId: "D" };
};
assert.equal(
await uploadPhotosWithReferences([], references, upload),
references,
);
await uploadPhotosWithReferences(
[photos()[0], photos()[1], photos()[1]],
references,
upload,
);
assert.equal(calls, 1);
assert.deepEqual(references.get("one"), { atflCd: "A", atflId: "B" });
});
/** 실제 브라우저 API 경계만 모의해 TUS callback과 타이머 정리를 검증한다. */
function browserStubs(t) {
const originalWindow = globalThis.window;
const originalTransfer = globalThis.DataTransfer;
const timers = new Map();
let timerId = 0;
globalThis.window = {
setTimeout(callback, delay) {
assert.equal(delay, 90_000);
timers.set(++timerId, callback);
return timerId;
},
clearTimeout(id) {
timers.delete(id);
},
};
globalThis.DataTransfer = class {
constructor() {
this.files = [];
this.items = { add: (file) => this.files.push(file) };
}
};
t.after(() => {
if (originalWindow === undefined) delete globalThis.window;
else globalThis.window = originalWindow;
if (originalTransfer === undefined) delete globalThis.DataTransfer;
else globalThis.DataTransfer = originalTransfer;
});
t.mock.method(console, "warn", () => {});
return timers;
}
test("실제 photo adapter의 TUS 비동기 오류는 즉시 reject하고 성공 참조·재시도를 보존", async (t) => {
const timers = browserStubs(t);
const calls = [];
let failSecond = true;
const { uploadDredgingPhotoDrafts } = loadTypeScript(
"src/features/dredging/api/photo-upload.ts",
{
"@/components/lib/utils": {
handleUpload(files, options) {
const name = files[0].name;
calls.push(name);
queueMicrotask(() => {
if (name === "two.jpg" && failSecond) {
failSecond = false;
options.handleError(new Error("tus failure"));
return;
}
options.handleResponse({
getHeader: () =>
JSON.stringify({ atflCd: name, atflId: `id-${name}` }),
});
});
},
},
},
);
const references = new Map();
await assert.rejects(
uploadDredgingPhotoDrafts(photos(), references),
/준설사진을 업로드하지 못했습니다/,
);
assert.deepEqual([...references.keys()], ["one"]);
assert.equal(timers.size, 0);
assert.equal(
await uploadDredgingPhotoDrafts(photos(), references),
references,
);
assert.deepEqual(calls, ["one.jpg", "two.jpg", "two.jpg", "three.jpg"]);
assert.equal(timers.size, 0);
});
test("잘못된 X-File-Info는 성공 참조를 만들지 않고 타이머를 해제", async (t) => {
const timers = browserStubs(t);
const { uploadDredgingPhotoDrafts } = loadTypeScript(
"src/features/dredging/api/photo-upload.ts",
{
"@/components/lib/utils": {
handleUpload(_files, options) {
options.handleResponse({ getHeader: () => '{"atflCd":"A"}' });
},
},
},
);
const references = new Map();
await assert.rejects(uploadDredgingPhotoDrafts([photos()[0]], references));
assert.equal(references.size, 0);
assert.equal(timers.size, 0);
});
test("FE 선행 모의 모드에서는 TUS를 호출하거나 참조를 추가하지 않는다", async () => {
const previous = process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED;
process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED = "false";
try {
const { uploadDredgingPhotoDrafts } = loadTypeScript(
"src/features/dredging/api/photo-upload.ts",
{
"@/components/lib/utils": {
handleUpload() {
assert.fail("mock mode must not upload");
},
},
},
);
const references = new Map();
assert.equal(
await uploadDredgingPhotoDrafts(photos(), references),
references,
);
assert.equal(references.size, 0);
} finally {
if (previous === undefined)
delete process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED;
else process.env.NEXT_PUBLIC_DREDGING_BACKEND_ENABLED = previous;
}
});
@@ -1,130 +0,0 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const ts = require("typescript");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const model = loadTypeScript(
"src/features/gisManagement/model/upload-retry-guard.ts",
);
const partialBody = {
success: false,
status: 500,
code: "GIS_UPLOAD_HISTORY_SAVE_FAILED",
uploadId: "GUP-1",
msApplied: true,
retryAllowed: false,
};
test("부분 성공 body는 transport/Axios/wrapper 경계에서 구분되고 일반500은 제외", () => {
for (const error of [
{ body: partialBody },
{ response: { data: partialBody } },
{ body: { data: partialBody } },
]) {
assert.deepEqual(model.readGisPartialSuccess(error), { uploadId: "GUP-1" });
}
for (const error of [
new Error("500"),
{ body: { ...partialBody, msApplied: false } },
{ body: { ...partialBody, retryAllowed: true } },
{ body: { code: partialBody.code } },
]) {
assert.equal(model.readGisPartialSuccess(error), null);
}
});
test("기관 전환·파일명과 무관하게 차단 유지, 같은/빈 downloadId는 해제하지 않음", () => {
const guard = model.createGisUploadRetryGuard();
const block = guard.recordFailure("4173000", "D-1", { body: partialBody });
assert.equal(guard.getBlock("other"), null);
assert.equal(guard.getBlock("4173000"), block);
assert.equal(guard.acceptFreshDownload("4173000", ""), null);
assert.equal(guard.acceptFreshDownload("4173000", "D-1"), null);
assert.equal(guard.acceptFreshDownload("other", "D-2"), null);
assert.equal(guard.getBlock("4173000"), block);
assert.match(model.getGisPartialSuccessNotice(block), /GUP-1/);
assert.match(model.getGisPartialSuccessNotice(block), /관리자/);
});
test("같은 기관의 새 현재본 downloadId만 새 작업을 허용하며 이전 운영확인ID를 반환", () => {
const guard = model.createGisUploadRetryGuard();
guard.recordFailure("4173000", "D-1", { body: partialBody });
const previous = guard.acceptFreshDownload("4173000", "D-2");
assert.deepEqual(previous, { uploadId: "GUP-1", downloadId: "D-1" });
assert.equal(guard.getBlock("4173000"), null);
});
test("일반 오류는 기존 차단을 해제하거나 새 차단을 만들지 않음", () => {
const guard = model.createGisUploadRetryGuard();
assert.equal(guard.recordFailure("scope", "D-1", new Error("500")), null);
guard.recordFailure("scope", "D-1", { body: partialBody });
assert.equal(guard.recordFailure("scope", "D-2", new Error("network")), null);
assert.equal(guard.getBlock("scope").uploadId, "GUP-1");
});
/** 실제 JSX 컴포넌트의 업로드 handler만 추출해 외부 HTTP를 모의하고 재호출 차단을 검증한다. */
test("실제 GIS 업로드 handler는 부분 성공 후 같은 세션 두 번째 POST를 보내지 않음", async () => {
const filename = path.resolve(
__dirname,
"../app/(map-layout)/map/components/gisManagement/GisManagementPopupPage.tsx",
);
const source = fs.readFileSync(filename, "utf8");
const ast = ts.createSourceFile(
filename,
source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
let initializer;
function visit(node) {
if (
ts.isVariableDeclaration(node) &&
node.name.getText(ast) === "handleUpload"
)
initializer = node.initializer.getText(ast);
ts.forEachChild(node, visit);
}
visit(ast);
assert.ok(initializer);
const output = ts.transpileModule(`globalThis.run = ${initializer};`, {
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.CommonJS,
},
}).outputText;
const alerts = [];
let posts = 0;
const guard = model.createGisUploadRetryGuard();
const context = {
console: { error() {} },
FormData,
uploadRetryGuard: guard,
getGisPartialSuccessNotice: model.getGisPartialSuccessNotice,
selectedScopeKey: "scope",
latestDownloadId: "D-1",
selectedMngInstCd: "4173000",
isManagementSelected: true,
selectedFile: new Blob(["test"]),
facility: { layerId: "tb_ms_swage_ppl" },
SCREEN_ID: "SN_MN010302",
FACILITY_TYPE_CODE: "PPL",
LAYER_ID: "tb_ms_swage_ppl",
alert: (message) => alerts.push(message),
setIsLoading() {},
setNotice() {},
postMutiPart: async () => {
posts++;
throw { body: partialBody };
},
};
vm.runInNewContext(output, context);
await context.run();
context.selectedFile = new Blob(["renamed-file"]);
await context.run();
assert.equal(posts, 1);
assert.equal(alerts.length, 2);
assert.match(alerts[1], /GUP-1/);
});
-47
View File
@@ -1,47 +0,0 @@
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const { createRequire } = require("node:module");
const ts = require("typescript");
/** 설치된 TypeScript로 실제 소스를 메모리에서만 변환한다. 앱·.next 파일은 생성하지 않는다. */
exports.loadTypeScript = function loadTypeScript(relativePath, mocks = {}) {
const filename = path.resolve(__dirname, "..", relativePath);
const nativeRequire = createRequire(filename);
const source = fs.readFileSync(filename, "utf8");
const output = ts.transpileModule(source, {
compilerOptions: {
target: ts.ScriptTarget.ES2022,
module: ts.ModuleKind.CommonJS,
jsx: ts.JsxEmit.ReactJSX,
esModuleInterop: true,
},
fileName: filename,
}).outputText;
const module = { exports: {} };
const localRequire = (specifier) => {
if (Object.hasOwn(mocks, specifier)) return mocks[specifier];
if (
specifier.startsWith(".") ||
specifier.startsWith("@/features/dredging")
) {
const dependency = path.resolve(
specifier.startsWith("@/")
? path.resolve(__dirname, "../src")
: path.dirname(filename),
`${specifier.startsWith("@/") ? specifier.slice(2) : specifier}.ts`,
);
if (fs.existsSync(dependency)) {
return loadTypeScript(
path.relative(path.resolve(__dirname, ".."), dependency),
mocks,
);
}
}
return nativeRequire(specifier);
};
vm.runInThisContext(`(function (require, module, exports) { ${output}\n})`, {
filename,
})(localRequire, module, module.exports);
return module.exports;
};
-166
View File
@@ -1,166 +0,0 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const layer = loadTypeScript("src/features/mapLayers/lib/layer-info.ts");
const { subscribeFlowDirectionMoveEnd } = loadTypeScript(
"src/features/mapLayers/lib/flow-direction-listener.ts",
);
test("레이어 camel/snake 별칭과 기존 우선순위 유지", () => {
assert.equal(
layer.getLayerTypeName({
lyrTyNm: " A ",
lyrTypeNm: "B",
lyr_type_nm: "C",
}),
"A",
);
assert.equal(
layer.getLayerTypeName({ lyr_type_nm: " tb_il_swage_prcs_fclt " }),
"tb_il_swage_prcs_fclt",
);
assert.equal(
layer.getLayerFilterSql({ filter_sql: "FCLT_CPCT >= 500" }),
"fclt_cpct>=500",
);
assert.equal(layer.getLayerTypeName(null), "");
});
test("처리시설 500 경계·동시체크·해제 모드 유지", () => {
const large = {
lyrTyNm: "tb_il_swage_prcs_fclt",
filtrSql: "FCLT_CPCT >= 500",
};
const small = { lyr_type_nm: "tb_il_swage_prcs_fclt", lyrNm: "500톤 미만" };
assert.equal(layer.getSwagePrcsFcltLayerMode([]), "none");
assert.equal(layer.getSwagePrcsFcltLayerMode([{ data: large }]), "gte500");
assert.equal(layer.getSwagePrcsFcltLayerMode([small]), "lt500");
assert.equal(layer.getSwagePrcsFcltLayerMode([large, small]), "all");
assert.equal(
layer.getSwagePrcsFcltCapacityKind({ ...large, lyrTyNm: "other" }),
null,
);
});
test("중첩 유하방향·관측소 정규화는 원본을 변경하지 않고 계약 플래그 유지", () => {
const original = [
{
lyrNm: "root",
children: [
{ lyrNm: "유하방향", minZoom: 0, clickPsblYn: "Y" },
{ lyr_type_nm: "tb_ms_rain_obsvtr_pstn_info", clickPsblYn: "Y" },
{ lyrTyNm: "other", clickPsblYn: "Y" },
],
},
];
const before = JSON.stringify(original);
const normalized = layer.normalizeRainObservationLayerInteraction(
layer.normalizeFlowDirectionLayerInfos(original),
);
const [flow, rain, other] = normalized[0].children;
assert.equal(flow.lyrTyNm, "tb_sn_swage_ppl_flow_direction");
assert.equal(flow.srvcNm, "api");
assert.equal(flow.cmncNm, "bbox");
assert.equal(flow.minViewSize, 0);
assert.equal(flow.clickPsblYn, "N");
assert.equal(rain.clickPsblYn, "N");
assert.equal(other.clickPsblYn, "Y");
assert.equal(JSON.stringify(original), before);
});
test("map/map2·줌·zIndex·BBOX 계산의 0과 빈값 구분", () => {
assert.equal(
layer.getFlowDirectionModeByPathname("/map2/"),
"PIPE_START_END",
);
assert.equal(layer.getFlowDirectionModeByPathname("/map"), "PIPE_DEPTH");
assert.equal(layer.getFiniteNumber(" "), null);
assert.equal(layer.getFiniteNumber("NaN"), null);
assert.equal(layer.getFiniteNumber(0), 0);
assert.equal(layer.getFlowDirectionMinZoom({}), 13);
assert.equal(layer.getFlowDirectionMinZoom({ minViewSize: 0 }), 0);
assert.equal(layer.getFlowDirectionZIndex({ drawOrdr: 0 }), 0);
assert.equal(layer.getFlowDirectionZIndex({}), 9998);
assert.deepEqual(
layer.expandFlowDirectionExtent([0, 10, 100, 110]),
[-35, -25, 135, 145],
);
assert.equal(
layer.formatFlowDirectionBbox([1.23456, -4, 5, 6]),
"1.235,-4.000,5.000,6.000",
);
});
test("관로 캐시 키는 같은 시설이라도 geometry가 다르면 구분", () => {
const feature = (coords) => ({
getId: () => null,
get: (name) => (name === "fclt_mng_no" ? " P-1 " : undefined),
getGeometry: () => ({ getCoordinates: () => coords }),
});
assert.equal(
layer.getFlowDirectionFeatureCacheKey(
feature([
[0, 0],
[1, 1],
]),
),
"fclt:P-1:[[0,0],[1,1]]",
);
assert.notEqual(
layer.getFlowDirectionFeatureCacheKey(
feature([
[0, 0],
[1, 1],
]),
),
layer.getFlowDirectionFeatureCacheKey(
feature([
[0, 0],
[2, 2],
]),
),
);
});
test("moveend는 최신 체크·레이어를 읽고 구독 해제·재구독 뒤 중복 호출하지 않는다", () => {
const listeners = new Map();
const map = {
on(event, handler) {
assert.equal(event, "moveend");
const key = {};
listeners.set(key, handler);
return key;
},
};
const unsubscribe = (key) => listeners.delete(key);
let state = { checked: false, layerInfo: { id: "A" } };
const calls = [];
const move = () => [...listeners.values()].forEach((handler) => handler());
const cleanup = subscribeFlowDirectionMoveEnd(
map,
() => state,
(info) => calls.push(info.id),
unsubscribe,
);
move();
state = { checked: true, layerInfo: { id: "B" } };
move();
state = { checked: true, layerInfo: null };
move();
assert.deepEqual(calls, ["B"]);
cleanup();
state = { checked: true, layerInfo: { id: "C" } };
move();
assert.deepEqual(calls, ["B"]);
const cleanupNext = subscribeFlowDirectionMoveEnd(
map,
() => state,
(info) => calls.push(info.id),
unsubscribe,
);
move();
assert.deepEqual(calls, ["B", "C"]);
assert.equal(listeners.size, 1);
cleanupNext();
assert.equal(listeners.size, 0);
});
@@ -1,76 +0,0 @@
const assert = require("node:assert/strict");
const path = require("node:path");
const { test } = require("node:test");
const {
validateSource,
checkWorkspace,
} = require("../scripts/check-module-boundaries.cjs");
test("현재 신규 feature·transport·legacy bridge가 구조 정책을 통과", () => {
const result = checkWorkspace(path.resolve(__dirname, ".."));
assert.ok(result.checkedFiles >= 20);
assert.deepEqual(result.errors, []);
});
test("model의 app·API 의존은 type-only여도 실패", () => {
assert.ok(
validateSource(
"src/features/dredging/model/new.ts",
'import type { X } from "@/app/route";',
).length,
);
assert.ok(
validateSource(
"src/features/dredging/model/new.ts",
'import type { X } from "../api/client";',
).length,
);
});
test("명시한 legacy helper만 기존 adapter에서 허용", () => {
assert.deepEqual(
validateSource(
"src/features/dredging/api/dredging-api.ts",
'import { getData } from "@/components/lib/api";',
),
[],
);
assert.ok(
validateSource(
"src/features/dredging/lib/new.ts",
'import { getData } from "@/components/lib/api";',
).length,
);
assert.ok(
validateSource(
"src/features/dredging/api/dredging-api.ts",
'import { X } from "@/components/ui/new";',
).length,
);
});
test("legacy 진입점에 구현을 다시 추가하면 실패", () => {
const file = "app/(with-base-layout)/oper/dredging/lib/date.ts";
assert.deepEqual(
validateSource(file, 'export * from "@/features/dredging/lib/date";'),
[],
);
assert.ok(
validateSource(file, "export const formatDate = () => null;").length,
);
});
test("transport 상위 계층 의존과 동적 app import도 실패", () => {
assert.ok(
validateSource(
"src/shared/api/http-transport.ts",
'import { token } from "@/entities/auth";',
).length,
);
assert.ok(
validateSource(
"src/features/mapLayers/lib/new.ts",
'const route = import("@/app/map");',
).length,
);
});
@@ -1,624 +0,0 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
class Options {
constructor(options) {
this.options = options;
}
}
class Feature {
constructor(values = {}) {
this.values = values;
}
set(key, value) {
this.values[key] = value;
}
get(key) {
return this.values[key];
}
setStyle(style) {
this.style = style;
}
}
class Geometry {
constructor(type, coordinates) {
this.type = type;
this.coordinates = coordinates;
}
getType() {
return this.type;
}
getCoordinates() {
return this.coordinates;
}
setCoordinates(value) {
this.coordinates = value;
}
}
const olMocks = {
"ol/Feature.js": Feature,
"ol/geom/Point.js": class extends Geometry {
constructor(coordinates) {
super("Point", coordinates);
}
},
"ol/geom/Polygon.js": class extends Geometry {
constructor(coordinates) {
super("Polygon", coordinates);
}
},
"ol/style/Style.js": Options,
"ol/style/Fill.js": Options,
"ol/style/Stroke.js": Options,
"ol/style/Circle.js": Options,
"ol/style/Text.js": Options,
"ol/format/GeoJSON.js": class {
writeGeometryObject(geometry) {
return {
type: geometry.getType(),
coordinates: geometry.getCoordinates(),
};
}
},
};
const edit = loadTypeScript("src/features/gisEdit/lib/edit-model.ts");
const snapshot = loadTypeScript(
"src/features/gisEdit/lib/feature-snapshot.ts",
olMocks,
);
const mutations = loadTypeScript(
"src/features/gisEdit/lib/geometry-mutations.ts",
olMocks,
);
const handles = loadTypeScript(
"src/features/gisEdit/lib/vertex-handles.ts",
olMocks,
);
test("GIS 편집 레이어 제한·속성 whitelist·일자 정규화를 보존", () => {
const layers = edit.collectEditableLayers([
{
children: [
{ lyrTyNm: "tb_ms_swage_mhl" },
{ lyr_type_nm: "tb_ms_swage_ppl", line_colr: "#f00" },
],
},
]);
assert.equal(layers.length, 1);
assert.equal(layers[0].layerId, "tb_ms_swage_ppl");
assert.deepEqual(layers[0].allowedTools, [
"ADD",
"MOVE",
"VERTEX_EDIT",
"DELETE",
]);
assert.equal(layers[0].previewColors.lineColor, "rgba(255, 0, 0, 0.95)");
assert.deepEqual(
edit.buildPipeSaveAttributes({
fcltMngNo: "P1",
instlYmd: "2026-09-11",
instMngNo: " ",
arbitrary: "drop",
}),
{ fcltMngNo: "P1", instlYmd: "20260911", instMngNo: null },
);
});
test("feature 식별자와 LineString 저장 snapshot은 기존 계약 유지", () => {
const ref = snapshot.getFeatureRef({
values_: { fclt_mng_no: " P1 ", mng_inst_cd: " 123 " },
getId: () => "ws:tb_ms_swage_ppl.11",
});
assert.deepEqual(ref, { fcltMngNo: "P1", mngInstCd: "123", fid: "11" });
const geometry = {
type: "LineString",
coordinates: [
[0, 0],
[1, 1],
],
};
assert.deepEqual(
snapshot.normalizePipeGeometry("tb_ms_swage_ppl", geometry),
{
type: "MultiLineString",
coordinates: [
[
[0, 0],
[1, 1],
],
],
},
);
assert.equal(snapshot.normalizePipeGeometry("other", geometry), geometry);
assert.equal(
snapshot.buildBbox({
getSize: () => [10, 10],
getView: () => ({ calculateExtent: () => [1.23456, 2, 3, 4] }),
}),
"1.235,2.000,3.000,4.000",
);
});
test("꼭지점 이동·중간점 삽입과 폴리곤 폐합 유지", () => {
const line = new Geometry("LineString", [
[0, 0],
[10, 0],
]);
mutations.applyVertexHandleMove(
line,
{ kind: "midpoint", geometryType: "LineString", coordIndex: 0 },
[5, 2],
);
assert.deepEqual(line.getCoordinates(), [
[0, 0],
[5, 2],
[10, 0],
]);
const polygon = new Geometry("Polygon", [
[
[0, 0],
[5, 0],
[5, 5],
[0, 0],
],
]);
mutations.applyVertexHandleMove(
polygon,
{ kind: "vertex", geometryType: "Polygon", ringIndex: 0, coordIndex: 0 },
[1, 1],
);
assert.deepEqual(polygon.getCoordinates()[0], [
[1, 1],
[5, 0],
[5, 5],
[1, 1],
]);
});
test("최소 꼭지점·midpoint 삭제 차단 및 다중선 실패 시 원형 보존", () => {
const line = new Geometry("LineString", [
[0, 0],
[1, 1],
]);
assert.equal(
mutations.deleteSelectedVertexHandles(line, [
{ kind: "vertex", geometryType: "LineString", coordIndex: 0 },
]).ok,
false,
);
assert.equal(
mutations.deleteSelectedVertexHandles(line, [
{ kind: "midpoint", geometryType: "LineString", coordIndex: 0 },
]).ok,
false,
);
const multi = new Geometry("MultiLineString", [
[
[0, 0],
[1, 1],
[2, 2],
],
[
[3, 3],
[4, 4],
],
]);
const before = JSON.stringify(multi.getCoordinates());
const result = mutations.deleteSelectedVertexHandles(multi, [
{
kind: "vertex",
geometryType: "MultiLineString",
partIndex: 0,
coordIndex: 1,
},
{
kind: "vertex",
geometryType: "MultiLineString",
partIndex: 1,
coordIndex: 0,
},
]);
assert.equal(result.ok, false);
assert.equal(JSON.stringify(multi.getCoordinates()), before);
});
test("겹친 다중선 꼭지점 핸들은 메타를 합쳐 선택관계를 유지", () => {
const geometry = new Geometry("MultiLineString", [
[
[0, 0],
[10, 0],
],
[
[0, 0],
[20, 0],
],
]);
const features = handles.buildVertexHandleFeatures(geometry, {
getPixelFromCoordinate: ([x, y]) => [x * 10, y * 10],
});
assert.equal(features.length, 3);
assert.ok(
features.some((feature) => feature.get("handleMetaList").length === 2),
);
});
test("유하방향 대표 선분·중앙 위치·각도 계산 보존", () => {
const flow = loadTypeScript(
"src/features/mapLayers/lib/flow-direction-styles.ts",
olMocks,
);
const placement = flow.getArrowPlacementAtHalfLength([
[0, 0],
[4, 0],
[4, 8],
]);
assert.deepEqual(placement.coordinate, [4, 2]);
assert.equal(placement.angle, Math.PI / 2);
assert.equal(flow.withAlpha("#f00", 0.5), "rgba(255, 0, 0, 0.5)");
});
function downloadContext(t) {
const original = globalThis.window;
const delays = [];
globalThis.window = {
setTimeout: (callback, delay) => {
delays.push(delay);
callback();
return 1;
},
};
t.after(() => {
if (original === undefined) delete globalThis.window;
else globalThis.window = original;
});
t.mock.method(console, "warn", () => {});
return delays;
}
test("GIS 파일 연결 실패만 1회 재시도하고 HTTP 오류는 바로 전달", async (t) => {
const delays = downloadContext(t);
const { createGisDownloadClient } = loadTypeScript(
"src/features/gisManagement/api/gis-download.ts",
);
let count = 0;
const response = {
blob: new Blob(),
headers: new Headers(),
fileName: "map.gpkg",
};
const client = createGisDownloadClient({
getData: async () => null,
postRawJson: async () => null,
getBlobDataWithHeaders: async () => {
if (++count === 1) throw new TypeError("Failed to fetch");
return response;
},
});
assert.equal(await client.downloadGeoPackage("file"), response);
assert.equal(count, 2);
assert.deepEqual(delays, [500]);
let failedCalls = 0;
const failed = createGisDownloadClient({
getData: async () => null,
postRawJson: async () => null,
getBlobDataWithHeaders: async () => {
failedCalls++;
throw new Error("HTTP 500");
},
});
await assert.rejects(failed.downloadGeoPackage("file"), /HTTP 500/);
assert.equal(failedCalls, 1);
});
test("GIS 다운로드 작업 접수·상태 polling의 URL·순서·READY/FAILED 처리 유지", async (t) => {
const delays = downloadContext(t);
const { createGisDownloadClient } = loadTypeScript(
"src/features/gisManagement/api/gis-download.ts",
);
const calls = [],
statuses = [];
const jobs = [
{ status: "QUEUED", retryAfterMillis: 1 },
{ status: "READY", downloadId: "D1" },
];
const client = createGisDownloadClient({
getData: async (url) => {
calls.push(url);
return jobs.shift();
},
postRawJson: async () => ({ data: { jobId: "J/1" } }),
getBlobDataWithHeaders: async () => null,
});
assert.equal((await client.startDownloadJob("create")).jobId, "J/1");
assert.equal(
(await client.waitForDownloadJob("J/1", (job) => statuses.push(job.status)))
.downloadId,
"D1",
);
assert.deepEqual(calls, [
"v1/map/gisDownloadJobs/J%2F1",
"v1/map/gisDownloadJobs/J%2F1",
]);
assert.deepEqual(statuses, ["QUEUED", "READY"]);
assert.deepEqual(delays, [1000]);
const failed = createGisDownloadClient({
getData: async () => ({ status: "FAILED", message: "실패" }),
postRawJson: async () => ({}),
getBlobDataWithHeaders: async () => null,
});
await assert.rejects(failed.startDownloadJob("create"), /접수 정보/);
await assert.rejects(failed.waitForDownloadJob("id"), /실패/);
});
const exportOptions = () => ({
selectedTrtdstNm: "01. 덕풍",
defectGroupTitle: "관로",
isUnitMode: false,
defectTypeLabel: "전체",
unitRows: [],
rows: [],
recordTypeLabels: { DEFECT: "결함" },
pipeItemLabels: { CL: "균열" },
severityLabels: { L: "대", M: "중", S: "소" },
visibleDefectColumns: [{ code: "CL", label: "균열" }],
totals: {
extensionM: 10,
drivingDistanceM: 8,
structuralGrade: 2,
operationalGrade: 1,
},
defectSummary: {
anomalySubtotals: { L: 1, M: 0, S: 0 },
anomalyTotal: 1,
defectSubtotals: { CL: { L: 1, M: 0, S: 0 } },
defectTotals: { CL: 1 },
},
summaryLabelColumnCount: 7,
fixedColumnCount: 11,
setNotice: () => {},
});
test("조사 상세 Excel의 동적 결함열·소계·합계·병합영역 유지", async () => {
let output;
const { downloadExmnSurveyDetails } = loadTypeScript(
"src/features/exmn/lib/download-exmn-details.ts",
{
"@/shared/lib/download-excel": {
downloadExcelWithKoreanHeaders: async (value) => {
output = value;
},
},
},
);
const options = exportOptions();
options.rows = [
{
reportNo: "R1",
pipeNo: "P1",
upstreamManhole: "M1",
downstreamManhole: "M2",
drainageType: "D",
pipeType: "T",
specification: "600",
extensionM: 10,
drivingDistanceM: 8,
structuralGrade: 2,
operationalGrade: 1,
defectCounts: { CL: { L: 1, M: 0, S: 0 } },
},
];
await downloadExmnSurveyDetails(options);
assert.equal(output.data.length, 3);
assert.equal(output.data[0].CL_L, 1);
assert.equal(output.data[1].reportNo, "소계");
assert.equal(output.data[2].reportNo, "합계");
assert.equal(output.headerRange, "A1:Q1");
assert.deepEqual(output.mergeRanges, ["A3:G3", "A4:G4", "L4:N4", "O4:Q4"]);
});
test("본단위 Excel 표기와 빈 결과 안내는 유지하고 파일을 만들지 않음", async () => {
let output, notice;
const { downloadExmnSurveyDetails } = loadTypeScript(
"src/features/exmn/lib/download-exmn-details.ts",
{
"@/shared/lib/download-excel": {
downloadExcelWithKoreanHeaders: async (value) => {
output = value;
},
},
},
);
const options = {
...exportOptions(),
isUnitMode: true,
setNotice: (value) => {
notice = value;
},
};
await downloadExmnSurveyDetails(options);
assert.equal(output, undefined);
assert.match(notice.text, /본단위구간 데이터가 없습니다/);
options.unitRows = [
{
pipeNo: "P1",
recordType: "DEFECT",
code: "CL",
defectName: "(기존)균열",
severity: "L",
continuous: false,
structuralScore: 0,
},
];
await downloadExmnSurveyDetails(options);
assert.equal(output.headerRange, "A1:N1");
assert.equal(output.data[0].defectName, "(결함)균열");
assert.equal(output.data[0].continuous, "N");
assert.equal(output.data[0].structuralScore, 0);
});
test("준설 표시 컴포넌트는 현재 그룹·날짜·파일 조작을 상위 handler로 전달", () => {
const marker = () => null;
const ui = Object.fromEntries(
[
"Button",
"InputDatePicker",
"Table",
"TableBody",
"TableCell",
"TableHead",
"TableHeader",
"TableRow",
].map((name) => [
name,
function UiMarker() {
return null;
},
]),
);
const { DredgingRegistrationBody } = loadTypeScript(
"src/features/dredging/ui/DredgingRegistrationBody.tsx",
{
"@common/components": ui,
"@common/business_components/ui/file-upload": { FileUpload: marker },
},
);
const group = {
id: "G1",
registrationMode: "PHOTO",
photos: [],
manualLocation: null,
matchedPipe: null,
isMatching: false,
};
const calls = [];
const noop = () => {};
const tree = DredgingRegistrationBody({
presentation: "popup",
groups: [group],
activeGroup: group,
representativeDredgingDate: "",
setRepresentativeDredgingDate: (value) => calls.push(value),
parseLocalDate: (value) => new Date(value),
formatLocalDate: () => "2026-09-11",
getResolvedLocation: () => null,
addGroup: noop,
removeGroup: noop,
selectGroup: noop,
changeRegistrationMode: noop,
handleFilesChange: (id, files) => calls.push([id, files]),
removePhoto: noop,
registrationModeLabels: { PHOTO: "사진", MANUAL: "수동" },
photoStatusLabels: {},
locationSourceLabels: {},
isEditingActiveLocation: false,
locationDraft: null,
applyLocationDraft: noop,
cancelLocationEditing: noop,
startLocationEditing: noop,
activeResolvedLocation: null,
hasUnappliedLocationDraft: false,
statusMessage: "",
changeLocationDraft: noop,
LocationMap: marker,
});
const elements = [];
function visit(node) {
if (!node || typeof node !== "object") return;
if (Array.isArray(node)) return node.forEach(visit);
elements.push(node);
visit(node.props?.children);
}
visit(tree);
const upload = elements.find(
(node) => node.type === marker && node.props.maxFiles === 5,
);
const date = elements.find((node) => node.type === ui.InputDatePicker);
const files = [{ name: "photo.jpg" }];
upload.props.setFiles(files);
date.props.onChange(new Date());
assert.deepEqual(calls, [["G1", files], "2026-09-11"]);
});
test("불필요한 HTML 실행 파일은 없고 의도적 공개 barrel은 유지", () => {
const root = path.resolve(__dirname, "..");
const collect = (dir) =>
fs.existsSync(dir)
? fs
.readdirSync(dir, { withFileTypes: true })
.flatMap((entry) =>
entry.isDirectory()
? collect(path.join(dir, entry.name))
: [path.join(dir, entry.name)],
)
: [];
assert.deepEqual(collect(path.join(root, "app/html")), []);
assert.ok(
fs
.readFileSync(
path.join(root, "src/features/windowPopup/index.ts"),
"utf8",
)
.includes("openWindowPopup"),
);
assert.ok(
fs
.readFileSync(
path.join(root, "src/entities/menu-authority/@x/posting.ts"),
"utf8",
)
.includes("AuthorityButton"),
);
});
test("통수위험 영역 초기화는 GIS 편집 preview를 제거하지 않음", () => {
const pageSource = fs.readFileSync(
path.resolve(__dirname, "../app/(map-layout)/map/page.tsx"),
"utf8",
);
const areaLayerStart = pageSource.indexOf(
"const ensureWaterRiskAreaLayer = useCallback",
);
const pipeLayerStart = pageSource.indexOf(
"const ensureWaterRiskPipeLayer = useCallback",
areaLayerStart,
);
assert.ok(areaLayerStart >= 0 && pipeLayerStart > areaLayerStart);
const areaLayerBlock = pageSource.slice(areaLayerStart, pipeLayerStart);
assert.match(areaLayerBlock, /WATER_RISK_AREA_LAYER_ID/);
assert.doesNotMatch(areaLayerBlock, /GIS_EDIT_PREVIEW_LAYER_ID/);
assert.match(
areaLayerBlock,
/map\.addLayer\(waterRiskAreaLayerRef\.current\)/,
);
});
test("신규 관로 preview는 재표시되고 상세 패널 밖으로 최초 1회 focus", () => {
const pageSource = fs.readFileSync(
path.resolve(__dirname, "../app/(map-layout)/map/page.tsx"),
"utf8",
);
const previewLayerStart = pageSource.indexOf(
"const ensureGisEditPreviewLayer = useCallback",
);
const previewLayerEnd = pageSource.indexOf(
"const clearGisEditPreviewLayer = useCallback",
previewLayerStart,
);
const previewLayerBlock = pageSource.slice(
previewLayerStart,
previewLayerEnd,
);
assert.match(previewLayerBlock, /setVisible\?\.\(true\)/);
assert.match(
pageSource,
/gisEditAutoFocusedAddChangeIdRef\.current === focusedChange\.changeId/,
);
assert.match(
pageSource,
/focusGisEditChangeOnMap\(focusedChange\.changeId\)/,
);
});
-87
View File
@@ -1,87 +0,0 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { test } = require("node:test");
const { loadTypeScript } = require("./load-typescript.cjs");
const root = path.resolve(__dirname, "..");
const popupTheme = loadTypeScript(
"src/features/windowPopup/model/popupTheme.ts",
);
test("SN의 5개 테마와 기존 dark 별칭을 WindowPopup 계약으로 제한", () => {
for (const theme of ["white", "blue", "green", "red", "black"]) {
assert.equal(popupTheme.resolveWindowPopupTheme(theme), theme);
}
assert.equal(popupTheme.resolveWindowPopupTheme("dark"), "black");
assert.equal(popupTheme.resolveWindowPopupTheme("unknown"), undefined);
});
test("분석도구·SN 폼 컨트롤·페이징의 테마 계약 유지", () => {
const analysisMenu = fs.readFileSync(
path.join(
root,
"app/(map-layout)/map/components/analysisMenu/AnalysisPopupMenu.tsx",
),
"utf8",
);
const standardTableCss = fs.readFileSync(
path.resolve(
root,
"../../packages/common/business_components/src/ui/standard_data_table/standard-data-table.css",
),
"utf8",
);
const popupCss = fs.readFileSync(
path.join(root, "src/app/styles/components.css"),
"utf8",
);
const procSearchBar = fs.readFileSync(
path.resolve(
root,
"../../packages/common/business_components/src/ui/map/searchBar/procSearchBar.tsx",
),
"utf8",
);
const buttonCss = fs.readFileSync(
path.resolve(
root,
"../../packages/common/components/src/ui/button/button.css",
),
"utf8",
);
assert.doesNotMatch(analysisMenu, /bg-white/);
assert.doesNotMatch(analysisMenu, /ring-\[#0279FA\]/);
assert.match(analysisMenu, /bg-background text-label/);
assert.match(analysisMenu, /ring-primary/);
assert.match(standardTableCss, /background: var\(--background\) !important/);
assert.match(standardTableCss, /color: var\(--foreground\) !important/);
assert.doesNotMatch(
popupCss,
/\.standard-popup-layout button\[role="button"\]\.white/,
);
assert.doesNotMatch(procSearchBar, /!bg-white/);
assert.match(procSearchBar, /!bg-form/);
assert.match(procSearchBar, /!text-foreground/);
assert.match(
popupCss,
/\[data-theme="black"\][\s\S]*button\[role="combobox"\][\s\S]*background-color: var\(--form\) !important/,
);
assert.match(
popupCss,
/button\[role="combobox"\]:disabled[\s\S]*background-color: var\(--field\) !important/,
);
assert.match(
popupCss,
/input\[type="text"\][\s\S]*textarea[\s\S]*background-color: var\(--form\) !important/,
);
assert.match(
popupCss,
/input\[readonly\][\s\S]*textarea\[readonly\][\s\S]*background-color: var\(--field\) !important/,
);
assert.match(
buttonCss,
/button\[role="button"\]\.white[\s\S]*bg-\[rgba\(255,255,255,1\)\]/,
);
});