From ace32c85c15437525183b86d8cd76f2faadfaac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=9A=A9=EC=8B=9D?= Date: Tue, 29 Sep 2026 23:01:29 +0900 Subject: [PATCH 1/6] fix: align desktop smoke tests and validation expectations --- apps/desktop/src/renderer/app/main.tsx | 9 ++--- .../src/renderer/features/search/search.tsx | 4 +-- .../src/renderer/shared/lib/accessibility.ts | 2 +- .../widgets/document-workspace/documents.tsx | 4 +-- .../widgets/document-workspace/preview.tsx | 10 +++--- scripts/integration-support.test.mjs | 4 +-- scripts/m1-02-smoke.mjs | 12 +++---- scripts/m1-03-smoke.mjs | 1 + scripts/m1-04-smoke.mjs | 1 + scripts/m1-05-smoke.mjs | 12 +++---- scripts/m1-06-smoke.mjs | 10 +++--- scripts/m1-08-smoke.mjs | 5 +-- scripts/m2-05-smoke.mjs | 3 +- scripts/m2-06-smoke.mjs | 11 +++--- scripts/m2-07-smoke.mjs | 1 + scripts/m3-01-smoke.mjs | 2 +- scripts/m4-01-baseline.mjs | 1 + scripts/m4-02-smoke.mjs | 35 ++++++++++--------- scripts/m4-03-smoke.mjs | 2 +- scripts/m4-04-smoke.mjs | 3 +- scripts/m4-05-smoke.mjs | 13 +++---- scripts/m4-06-smoke.mjs | 9 ++--- scripts/m4-07-smoke.mjs | 10 +++--- scripts/package-cleanup.mjs | 3 +- scripts/package-cleanup.test.mjs | 17 ++++++++- scripts/preview-features-smoke.mjs | 2 +- scripts/test-cache-cleanup.mjs | 5 +-- scripts/test-cache-cleanup.test.mjs | 13 +++++++ scripts/ux-02-session.mjs | 10 +++--- scripts/ux-escape-smoke.mjs | 6 ++-- 30 files changed, 132 insertions(+), 88 deletions(-) diff --git a/apps/desktop/src/renderer/app/main.tsx b/apps/desktop/src/renderer/app/main.tsx index 02027e3..cee05be 100644 --- a/apps/desktop/src/renderer/app/main.tsx +++ b/apps/desktop/src/renderer/app/main.tsx @@ -59,6 +59,7 @@ async function call(work: () => Promise>): Promise> { } function App() { const documents = useRef(null); + const [inspectorTarget, setInspectorTarget] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const [hideHints, setHideHints] = useState(() => { try { return localStorage.getItem('metis.welcome.hidden.v1') === 'true'; } catch { return false; } }); @@ -337,7 +338,7 @@ function App() { { id: 'workspace.refresh', label: '폴더 새로 고침', reason: folderReady, run: () => list(session!, folder) }, { id: 'workspace.newDocument', label: '새 문서', reason: writable, run: () => beginCreate('document') }, { id: 'workspace.newDirectory', label: '새 폴더', reason: writable, run: () => beginCreate('directory') }, - { id: 'search.text', label: '검색', shortcut: 'Ctrl/Cmd+Shift+F', reason: needsWorkspace, run: () => { setSearchMode('text'); if (layout.narrow) layout.setMobile('left'); else layout.update({ ...layout.value, left: { ...layout.value.left, open: true, active: 'search' } }); } }, + { id: 'search.text', label: '검색', shortcut: 'Ctrl/Cmd+Shift+F', reason: needsWorkspace, run: () => { setSearchMode('text'); if (layout.narrow) layout.setMobile('left'); layout.update({ ...layout.value, left: { ...layout.value.left, open: true, active: 'search' } }); } }, { id: 'search.files', label: '빠른 열기', shortcut: 'Ctrl/Cmd+P', reason: needsWorkspace, run: () => setSearchMode('files') }, { id: 'proposal.open', label: '변경 제안 검토', reason: () => needsWorkspace() ?? (!document ? '먼저 문서를 열어 주세요.' : undefined), run: () => setProposalOpen(true) }, { id: 'semantic.open', label: '의미·텍스트 비교', reason: () => needsWorkspace() ?? (!document ? '먼저 문서를 열어 주세요.' : undefined), run: () => setSemanticOpen(true) }, @@ -409,14 +410,14 @@ function App() { {sidebarCommandButton('workspace.newDirectory', 'folder', '새 폴더')}{sidebarCommandButton('workspace.newDocument', 'document', '새 문서')} {loading ?

목록을 읽고 있습니다.

: !entries.length &&

표시할 폴더나 AsciiDoc 문서가 없습니다.

}}} - {layout.value.left.active === 'search' && session && { if (layout.narrow) layout.setMobile(undefined); else layout.update({ ...layout.value, left: { ...layout.value.left, open: false } }); }} open={hit => { void read({ name: hit.label, relativePath: hit.relativePath, kind: 'document' }, hit.kind === 'file' ? undefined : hit.line, hit.revision); }} />} + {layout.value.left.active === 'search' && session && { if (layout.narrow) layout.setMobile(undefined); layout.update({ ...layout.value, left: { ...layout.value.left, active: 'files', open: layout.narrow ? layout.value.left.open : false } }); window.document.querySelector('.ribbon-button[aria-label="검색"]')?.focus(); }} open={hit => { void read({ name: hit.label, relativePath: hit.relativePath, kind: 'document' }, hit.kind === 'file' ? undefined : hit.line, hit.revision); }} />} {layout.value.left.active === 'favorites' &&

즐겨찾기

{favorites.length ? favorites.map(path =>
) :

즐겨찾기한 문서가 없습니다.

}
} {leftVisible && !layout.narrow && }
{ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; const tabs = [...event.currentTarget.querySelectorAll('[role=tab]')], index = tabs.indexOf(event.target as HTMLButtonElement); if (index < 0) return; event.preventDefault(); tabs[event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : Math.min(tabs.length - 1, Math.max(0, index + (event.key === 'ArrowRight' ? 1 : -1)))].focus(); tabs[event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : Math.min(tabs.length - 1, Math.max(0, index + (event.key === 'ArrowRight' ? 1 : -1)))].click(); }}>{documentTabs.map(tab =>
)}{graphOpen &&
}
{error && !creating &&

작업 실패 — {error}

{document &&
편집 보존·복구 작업

저장·외부 변경 문제라면 현재 편집을 보존하고 원문 상태를 확인하세요.

{commandButton('document.preserve')}{commandButton('document.inspect')}{commandButton('recovery.open')}{commandButton('recovery.folder')}
}
}{warning &&
{warning}
} - + {session && graphOpen && } {centerView === 'documents' && !document &&
M

{session ? '작업 공간을 열었습니다.' : '문서가 있는 곳에서 시작하세요.'}

@@ -427,7 +428,7 @@ function App() {
}
{rightVisible && !layout.narrow && } diff --git a/apps/desktop/src/renderer/features/search/search.tsx b/apps/desktop/src/renderer/features/search/search.tsx index bc7ea4b..d377ac0 100644 --- a/apps/desktop/src/renderer/features/search/search.tsx +++ b/apps/desktop/src/renderer/features/search/search.tsx @@ -41,7 +41,7 @@ export function Search({ session, initialMode, memory, close, open, embedded = f

저장된 .adoc 기준 · 미저장 편집 제외 · 외부 변경은 다시 검색하면 반영됩니다.

- +
{running && }

Escape로 검색어를 지우지 않고 창을 닫습니다. 글자 조합 중에는 창을 유지합니다. 필터는 받은 결과 안에서 적용됩니다. 검색어에서 아래 방향키로 결과 이동, 위·아래/Home/End로 선택, Enter로 열 수 있습니다.

@@ -61,6 +61,6 @@ export function Search({ session, initialMode, memory, close, open, embedded = f {kinds[hit.kind]} · {hit.label}{hit.relativePath}:{hit.line}{hit.context})}} ; return embedded - ?
{ container.current = node; }} aria-labelledby="search-title">{content}
+ ?
{ container.current = node; }} aria-labelledby="search-title">{content}
: { container.current = node; }} aria-labelledby="search-title" onCancel={event => { event.preventDefault(); close(); }}>{content}; } diff --git a/apps/desktop/src/renderer/shared/lib/accessibility.ts b/apps/desktop/src/renderer/shared/lib/accessibility.ts index cbf5f99..82b3961 100644 --- a/apps/desktop/src/renderer/shared/lib/accessibility.ts +++ b/apps/desktop/src/renderer/shared/lib/accessibility.ts @@ -25,7 +25,7 @@ export function useSearchDialogEscape(close: () => void) { return { onCompositionStartCapture: () => { composing.current = true; }, onCompositionEndCapture: () => { composing.current = false; }, - onKeyDownCapture: (event: KeyboardEvent) => { + onKeyDownCapture: (event: KeyboardEvent) => { if (event.key !== 'Escape') return; event.preventDefault(); event.stopPropagation(); diff --git a/apps/desktop/src/renderer/widgets/document-workspace/documents.tsx b/apps/desktop/src/renderer/widgets/document-workspace/documents.tsx index 98be0bf..12aa3e9 100644 --- a/apps/desktop/src/renderer/widgets/document-workspace/documents.tsx +++ b/apps/desktop/src/renderer/widgets/document-workspace/documents.tsx @@ -43,7 +43,7 @@ function Editor({ tab, changed, save }: { tab: Tab; changed(): void; save(): voi useEffect(() => { const readonly = !!tab.baseline.readOnly || tab.baseline.eol === 'mixed'; tab.view?.dispatch({ effects: access.current.reconfigure([EditorState.readOnly.of(readonly), EditorView.editable.of(!readonly)]) }); }, [tab, tab.baseline.readOnly, tab.baseline.eol]); return
; } -export const Documents = forwardRef(function Documents(props, ref) { +export const Documents = forwardRef(function Documents(props, ref) { const tabs = useRef(new Map()); const [active, setActive] = useState(''); const activeRef = useRef(''); @@ -295,7 +295,7 @@ export const Documents = forwardRef setReview(undefined)} resolve={resolve} />}
- {selected && props.session && selected.view?.state.doc.lineAt(selected.view.state.selection.main.head).number ?? 1} reveal={() => setMode('split')} session={props.session} relativePath={active} text={selected.text} mode={mode} onAnalysis={value => { selected.analysis = value; selected.analysisText = analyzedText; props.onAnalysis(active, value); }} navigate={entry => { setMode('split'); if (entry.relativePath === active) jump(selected, entry.line); else props.onSource(entry); }} />} + {selected && props.session && selected.view?.state.doc.lineAt(selected.view.state.selection.main.head).number ?? 1} reveal={() => setMode('split')} session={props.session} relativePath={active} text={selected.text} mode={mode} onAnalysis={value => { selected.analysis = value; selected.analysisText = analyzedText; props.onAnalysis(active, value); }} navigate={entry => { setMode('split'); if (entry.relativePath === active) jump(selected, entry.line); else props.onSource(entry); }} />}
; }); diff --git a/apps/desktop/src/renderer/widgets/document-workspace/preview.tsx b/apps/desktop/src/renderer/widgets/document-workspace/preview.tsx index 68e9bb8..a741e7e 100644 --- a/apps/desktop/src/renderer/widgets/document-workspace/preview.tsx +++ b/apps/desktop/src/renderer/widgets/document-workspace/preview.tsx @@ -1,3 +1,4 @@ +import { createPortal } from 'react-dom'; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import DOMPurify from 'dompurify'; import { OperationStatus } from '../../shared/ui/operation-status'; @@ -21,7 +22,7 @@ export function safePreview(html: string, appearance: WorkspaceAppearanceV1 = de return `${fragment.body.innerHTML}`; } export interface PreviewPosition { text?: string; html?: string; x: number; y: number; outline: number } -export function Preview({ session, relativePath, text, mode, navigate, onAnalysis, position, editorLine, reveal, appearance, stylesheetVersion, inspector = true }: { session: Session; relativePath: string; text: string; mode: 'source' | 'split' | 'preview'; navigate(entry: OutlineEntry): void; onAnalysis(value: Analysis): void; position: PreviewPosition; editorLine(): number; reveal(): void; appearance: WorkspaceAppearanceV1; stylesheetVersion: number; inspector?: boolean }) { +export function Preview({ session, relativePath, text, mode, navigate, onAnalysis, position, editorLine, reveal, appearance, stylesheetVersion, inspector = true, inspectorTarget }: { session: Session; relativePath: string; text: string; mode: 'source' | 'split' | 'preview'; navigate(entry: OutlineEntry): void; onAnalysis(value: Analysis): void; position: PreviewPosition; editorLine(): number; reveal(): void; appearance: WorkspaceAppearanceV1; stylesheetVersion: number; inspector?: boolean; inspectorTarget?: HTMLElement | null }) { const [analysis, setAnalysis] = useState(); const [basis, setBasis] = useState(); const current = !!analysis && basis === text; @@ -116,8 +117,7 @@ export function Preview({ session, relativePath, text, mode, navigate, onAnalysi if (pendingId.current && mode !== 'source' && current && !running) { const id = pendingId.current; pendingId.current = undefined; scrollToId(id); } }; useEffect(() => { const custom = frame.current?.contentDocument?.querySelector('style[data-metis-custom]'); if (custom) custom.textContent = customCss; }, [customCss]); - return
- {inspector && ; + return
+ {inspectorTarget ? createPortal(inspectorContent, inspectorTarget) : inspector && inspectorContent}