@ihyeon/flow-qa 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -16,7 +16,7 @@ CLI와 `127.0.0.1` 전용 웹 UI를 제공합니다. UI는 영어로 시작하
16
16
  Node.js 24.8 이상이 필요합니다. 브라우저는 첫 웹 실행 전에 설치합니다.
17
17
 
18
18
  ```sh
19
- npm install -g @ihyeon/flow-qa@0.2.0
19
+ npm install -g @ihyeon/flow-qa@0.2.2
20
20
  flow-qa browser install
21
21
  flow-qa ui --demo
22
22
  ```
package/README.md CHANGED
@@ -16,7 +16,7 @@ Flow QA is available as a CLI and a web UI bound to `127.0.0.1`. Provide a targe
16
16
  Node.js 24.8 or later is required. Install Chromium before the first web run:
17
17
 
18
18
  ```sh
19
- npm install -g @ihyeon/flow-qa@0.2.0
19
+ npm install -g @ihyeon/flow-qa@0.2.2
20
20
  flow-qa browser install
21
21
  flow-qa ui --demo
22
22
  ```
package/dist/src/i18n.js CHANGED
@@ -54,6 +54,8 @@ const koreanMessages = {
54
54
  'Could not read the Flow QA configuration.': 'Flow QA 설정을 읽을 수 없습니다.',
55
55
  'Invalid configuration file path.': '설정 파일 경로가 올바르지 않습니다.',
56
56
  'The source flow changed. Save the cases again.': '원본 플로우가 변경됐습니다. 케이스를 다시 저장하세요.',
57
+ 'Select the source Markdown together with the case JSON.': '케이스 JSON과 원본 Markdown을 함께 선택하세요.',
58
+ 'The selected Markdown does not match the case source.': '선택한 Markdown이 케이스의 원본과 일치하지 않습니다.',
57
59
  'Invalid run ID.': '실행 ID가 올바르지 않습니다.',
58
60
  'The UI server is shutting down.': 'UI 서버가 종료 중입니다.',
59
61
  'Another operation is in progress.': '다른 작업이 진행 중입니다.',
@@ -75,6 +75,9 @@
75
75
  $('plan-btn').disabled = !app.connected || !app.state?.models?.planner?.configured || app.state.models.planner.kind !== 'openai-compatible' || !app.state.models.planner.keyReady || !!active || app.busy;
76
76
  $('compile-btn').disabled = app.stale || app.busy;
77
77
  $('new-draft-btn').disabled = app.busy;
78
+ $('import-btn').disabled = app.busy;
79
+ $('import-json').disabled = app.busy;
80
+ $('import-markdown').disabled = app.busy;
78
81
  $('flow-input').disabled = app.busy;
79
82
  $('suite-editor').disabled = app.busy;
80
83
  $('prompt-btn').disabled = app.busy;
@@ -95,7 +98,7 @@
95
98
  const pill = $('draft-status');
96
99
  pill.textContent = t(app.stale ? 'app.regenerate' : app.dirty ? 'app.revalidate' : ready ? 'app.validated' : 'scenario.noDraft');
97
100
  pill.className = 'status-pill' + (ready ? ' ready' : app.stale || app.dirty ? ' dirty' : '');
98
- $('draft-source').textContent = t(app.origin === 'generated' ? 'app.sourceGenerated' : app.origin === 'library' ? (app.sourceFlow ? 'app.sourceLibraryFlow' : 'app.sourceLibrary') : app.origin === 'manual' ? (app.sourceFlow ? 'app.sourceManualFlow' : 'app.sourceManual') : 'scenario.sourceNone');
101
+ $('draft-source').textContent = t(app.origin === 'generated' ? 'app.sourceGenerated' : app.origin === 'library' ? (app.sourceFlow ? 'app.sourceLibraryFlow' : 'app.sourceLibrary') : app.origin === 'imported' ? (app.sourceFlow ? 'app.sourceImportedFlow' : 'app.sourceImported') : app.origin === 'manual' ? (app.sourceFlow ? 'app.sourceManualFlow' : 'app.sourceManual') : 'scenario.sourceNone');
99
102
  $('script-preview').textContent = ready ? app.script : t('scenario.scriptPlaceholder');
100
103
  }
101
104
  function locatorText(locator) {
@@ -249,6 +252,41 @@
249
252
  finally { app.planning = false; await refresh(false).catch(() => {}); }
250
253
  }); }
251
254
  async function prompt() { await work(async () => { const flow = $('flow-input').value.trim(); if (!flow) throw localizedError('app.enterFlow'); const result = await api('/api/prompt', { flow, platform: $('scenario-platform').value }); download('flow-qa-prompt.md', result.prompt, 'text/markdown'); noticeKey('app.promptDownloaded'); }); }
255
+ function editableFlow(flow) { return flow.replace(/\r\n?/g, '\n'); }
256
+ function hasUnsavedScenario() {
257
+ const flow = $('flow-input').value;
258
+ const suite = $('suite-editor').value;
259
+ if (!app.savedFile) return !!(flow.trim() || suite.trim());
260
+ return flow !== editableFlow(app.sourceFlow)
261
+ || suite !== JSON.stringify(app.draft, null, 2)
262
+ || $('scenario-platform').value !== suitePlatform();
263
+ }
264
+ function warnBeforeUnload(event) {
265
+ if (!hasUnsavedScenario()) return;
266
+ event.preventDefault();
267
+ event.returnValue = '';
268
+ }
269
+ async function importFiles() {
270
+ const files = [...$('import-json').files, ...$('import-markdown').files];
271
+ let imported = false;
272
+ await work(async () => {
273
+ const jsonFiles = files.filter((file) => file.name.toLowerCase().endsWith('.json'));
274
+ const markdownFiles = files.filter((file) => file.name.toLowerCase().endsWith('.md'));
275
+ if (files.length > 2 || jsonFiles.length !== 1 || markdownFiles.length > 1 || jsonFiles.length + markdownFiles.length !== files.length) throw localizedError('app.importSelection');
276
+ if (jsonFiles[0].size > 256 * 1024 || (markdownFiles[0] && markdownFiles[0].size > 128 * 1024)) throw localizedError('app.importTooLarge');
277
+ let suite;
278
+ try { suite = JSON.parse(await jsonFiles[0].text()); } catch { throw localizedError('app.invalidJson'); }
279
+ const flow = markdownFiles[0] ? await markdownFiles[0].text() : undefined;
280
+ const body = { suite, ...(flow === undefined ? {} : { flow }) };
281
+ if (new TextEncoder().encode(JSON.stringify(body)).byteLength > 256 * 1024) throw localizedError('app.importTooLarge');
282
+ const result = await api('/api/import', body);
283
+ $('flow-input').value = flow || '';
284
+ setDraft(result.suite, result.script, 'imported', flow || '');
285
+ noticeKey('app.importComplete');
286
+ imported = true;
287
+ });
288
+ if (imported) { $('import-json').value = ''; $('import-markdown').value = ''; }
289
+ }
252
290
  async function compile() { await work(async () => {
253
291
  if (app.stale) throw localizedError('app.compileStale');
254
292
  let suite; try { suite = JSON.parse($('suite-editor').value); } catch { throw localizedError('app.invalidJson'); }
@@ -261,7 +299,7 @@
261
299
  setDraft(result.suite, result.script, app.origin === 'none' ? 'manual' : app.origin, sourceFlow, app.dirty ? '' : app.savedFile, app.dirty ? '' : app.savedHash);
262
300
  noticeKey('app.compileComplete');
263
301
  }); }
264
- async function save() { await work(async () => { if (!app.draft || app.dirty || app.stale) throw localizedError('app.needValidated'); const body = { suite: app.draft }; if (!app.boundSource && app.sourceFlow && app.sourceFlow === $('flow-input').value.trim()) body.flow = app.sourceFlow; const result = await api('/api/save', body); const saved = await api(`/api/suite?file=${encodeURIComponent(result.file)}`); $('flow-input').value = saved.flow || ''; setDraft(saved.suite, saved.script, 'library', saved.flow || '', result.file, saved.suiteHash || ''); noticeKey('app.savedSuite', { file: result.file }); await refresh(false); }); }
302
+ async function save() { await work(async () => { if (!app.draft || app.dirty || app.stale) throw localizedError('app.needValidated'); const body = { suite: app.draft }; if (!app.boundSource && app.sourceFlow && (app.origin === 'imported' ? editableFlow(app.sourceFlow) === $('flow-input').value : app.sourceFlow === $('flow-input').value.trim())) body.flow = app.sourceFlow; const result = await api('/api/save', body); const saved = await api(`/api/suite?file=${encodeURIComponent(result.file)}`); $('flow-input').value = saved.flow || ''; setDraft(saved.suite, saved.script, 'library', saved.flow || '', result.file, saved.suiteHash || ''); noticeKey('app.savedSuite', { file: result.file }); await refresh(false); }); }
265
303
  async function run() { await work(async () => { if (!app.draft || !app.script || !app.savedFile || !app.savedHash || app.dirty || app.stale || !$('review-checkbox').checked || !compatibleTarget()) throw localizedError('app.runNotReady'); const target = selectedTarget(); const result = await api('/api/run', { suite: app.savedFile, suiteHash: app.savedHash, targetName: target.name, targetFingerprint: target.targetFingerprint }); noticeKey('app.runStarted', { id: result.operationId }); await refresh(false); }); }
266
304
  async function stop() {
267
305
  if (app.stopping || !app.state?.active) return;
@@ -343,6 +381,7 @@
343
381
  else if (!$('notice').hidden) notice(''); // A server error has no stable code to translate; the next request uses the selected language.
344
382
  }
345
383
  function wire() {
384
+ window.addEventListener('beforeunload', warnBeforeUnload);
346
385
  document.querySelectorAll('[data-view]').forEach((button) => button.addEventListener('click', () => view(button.dataset.view)));
347
386
  $('nav-toggle').addEventListener('click', () => { const open = $('sidebar').classList.toggle('nav-open'); $('nav-toggle').setAttribute('aria-expanded', String(open)); });
348
387
  $('library-shortcut').addEventListener('click', () => view('library'));
@@ -355,11 +394,12 @@
355
394
  noticeKey('app.newDraftNotice');
356
395
  $('suite-editor').focus();
357
396
  });
358
- $('flow-input').addEventListener('input', () => { if ((app.sourceFlow || app.boundSource) && $('flow-input').value.trim() !== app.sourceFlow.trim()) { app.stale = true; app.savedHash = ''; $('review-checkbox').checked = false; readiness(); } });
397
+ $('flow-input').addEventListener('input', () => { if (app.origin === 'imported' ? $('flow-input').value !== editableFlow(app.sourceFlow) : (app.sourceFlow || app.boundSource) && $('flow-input').value.trim() !== editableFlow(app.sourceFlow).trim()) { app.stale = true; app.savedHash = ''; $('review-checkbox').checked = false; readiness(); } });
359
398
  $('scenario-platform').addEventListener('change', () => { app.planRequest += 1; if (app.draft) { app.stale = true; app.savedHash = ''; } $('review-checkbox').checked = false; readiness(); });
360
399
  $('suite-editor').addEventListener('input', () => { app.dirty = true; app.script = ''; app.savedFile = ''; app.savedHash = ''; if (app.origin === 'none') app.origin = 'manual'; $('review-checkbox').checked = false; summarize(null); readiness(); });
361
400
  $('review-checkbox').addEventListener('change', readiness); $('target-select').addEventListener('change', () => { $('review-checkbox').checked = false; targetDescription(); });
362
401
  $('plan-btn').addEventListener('click', generate); $('prompt-btn').addEventListener('click', prompt); $('compile-btn').addEventListener('click', compile); $('save-btn').addEventListener('click', save); $('run-btn').addEventListener('click', run); $('stop-btn').addEventListener('click', stop);
402
+ $('import-btn').addEventListener('click', importFiles);
363
403
  $('global-stop-btn').addEventListener('click', stop);
364
404
  $('reconnect-btn').addEventListener('click', () => refresh(true).catch(showError));
365
405
  $('script-download').addEventListener('click', () => { if (app.draft && !app.dirty && !app.stale) download(`${app.draft.id || 'flow-qa'}.test.mjs`, app.script, 'text/javascript'); });
@@ -36,6 +36,10 @@
36
36
  'scenario.prompt': ['Download generation prompt', '생성 프롬프트 다운로드'],
37
37
  'scenario.reviewTitle': ['Review cases', '케이스 검토'],
38
38
  'scenario.reviewHelp': ['Open each case to inspect its actions and expected results.', '각 케이스를 펼쳐 행동과 기대 결과를 살펴보세요.'],
39
+ 'scenario.importFiles': ['Import case files', '케이스 파일 가져오기'],
40
+ 'scenario.importJson': ['TC JSON', 'TC JSON'],
41
+ 'scenario.importMarkdown': ['Source Markdown (optional)', '원본 Markdown (선택)'],
42
+ 'scenario.importHint': ['Select one TC JSON and, if needed, its source Markdown separately. Import creates a copy to save and review.', 'TC JSON 1개와 필요한 경우 원본 Markdown을 각각 선택하세요. 검토하고 저장할 복사본을 가져옵니다.'],
39
43
  'scenario.noDraft': ['No draft', '초안 없음'],
40
44
  'scenario.summaryEmpty': ['No cases to review yet. Load a local example from the library or generate cases from the scenario.', '아직 검토할 케이스가 없습니다. 보관함의 로컬 예제를 불러오거나 왼쪽 시나리오에서 생성하세요.'],
41
45
  'scenario.sourceNone': ['Source: none', '원본: 없음'],
@@ -148,6 +152,8 @@
148
152
  'app.sourceGenerated': ['Source: AI generated from this scenario · JSON can be reviewed', '원본: 현재 시나리오에서 AI 생성 · JSON 검토 가능'],
149
153
  'app.sourceLibraryFlow': ['Source: saved cases and linked scenario', '원본: 저장된 케이스와 연결된 시나리오'],
150
154
  'app.sourceLibrary': ['Source: saved cases · no linked scenario', '원본: 저장된 케이스 · 연결된 시나리오 없음'],
155
+ 'app.sourceImportedFlow': ['Source: imported copy with source Markdown · save and review required', '원본: Markdown과 함께 가져온 복사본 · 저장 및 검토 필요'],
156
+ 'app.sourceImported': ['Source: imported case copy · save and review required', '원본: 가져온 케이스 복사본 · 저장 및 검토 필요'],
151
157
  'app.sourceManualFlow': ['Source: current scenario · manually written JSON', '원본: 현재 시나리오 · 직접 작성한 JSON'],
152
158
  'app.sourceManual': ['Source: manually written JSON · no linked scenario', '원본: 직접 작성한 JSON · 연결된 시나리오 없음'],
153
159
  'app.locatorRole': ['role', '역할'],
@@ -228,6 +234,9 @@
228
234
  'app.promptDownloaded': ['Prompt downloaded. Paste the generated JSON and validate it.', '프롬프트를 다운로드했습니다. 생성된 JSON을 붙여 넣고 검증하세요.'],
229
235
  'app.compileStale': ['Scenario changed. Generate cases again or reload them from the library.', '시나리오가 변경됐습니다. 다시 생성하거나 보관함의 케이스를 불러오세요.'],
230
236
  'app.invalidJson': ['Check the JSON syntax. Use double quotes around property names and strings, and check commas and brackets.', 'JSON 구문을 확인하세요. 속성 이름과 문자열은 큰따옴표로 감싸고 쉼표·괄호를 확인해 주세요.'],
237
+ 'app.importSelection': ['Select exactly one TC JSON and at most one source Markdown file.', 'TC JSON 파일 1개와 원본 Markdown 파일 최대 1개를 선택하세요.'],
238
+ 'app.importTooLarge': ['The selected files are too large to import. JSON must be at most 256 KiB, Markdown at most 128 KiB, and the request at most 256 KiB.', '선택한 파일이 너무 큽니다. JSON은 최대 256 KiB, Markdown은 최대 128 KiB, 요청 전체는 최대 256 KiB여야 합니다.'],
239
+ 'app.importComplete': ['Case copy imported. Review the cases and source, then save before running.', '케이스 복사본을 가져왔습니다. 케이스와 원본을 검토한 뒤 실행 전에 저장하세요.'],
231
240
  'app.compileComplete': ['JSON validated and script generated. The linked source scenario was kept.', 'JSON 검증과 스크립트 생성이 완료됐습니다. 연결된 원본 시나리오는 유지됩니다.'],
232
241
  'app.savedSuite': ['Cases saved to a separate file: {file}. Review them again before running.', '케이스를 별도 파일로 저장했습니다: {file}. 실행 전 내용을 다시 검토하세요.'],
233
242
  'app.runNotReady': ['Save or reload cases, select a target, and review the contents before running.', '케이스 저장 또는 다시 불러오기, 대상 선택, 직접 검토가 필요합니다.'],
@@ -61,6 +61,8 @@
61
61
  <div class="section-head"><span class="section-number">02</span><div><h2 id="draft-title" data-i18n="scenario.reviewTitle">Review cases</h2><p data-i18n="scenario.reviewHelp">Open each case to inspect its actions and expected results.</p></div><span id="draft-status" class="status-pill" data-i18n="scenario.noDraft">No draft</span></div>
62
62
  <div class="section-body">
63
63
  <div class="draft-topline"><span id="draft-source" data-i18n="scenario.sourceNone">Source: none</span><button type="button" class="text-button" id="library-shortcut"><span data-i18n="scenario.fromLibrary">Load from library</span><span aria-hidden="true">↗</span></button></div>
64
+ <div class="draft-actions import-actions"><label for="import-json"><span data-i18n="scenario.importJson">TC JSON</span><input id="import-json" type="file" accept=".json"></label><label for="import-markdown"><span data-i18n="scenario.importMarkdown">Source Markdown (optional)</span><input id="import-markdown" type="file" accept=".md"></label><button id="import-btn" type="button" class="button quiet" data-i18n="scenario.importFiles">Import case files</button></div>
65
+ <p class="hint" data-i18n="scenario.importHint">Select one TC JSON and, if needed, its source Markdown separately. Import creates a copy to save and review.</p>
64
66
  <div id="draft-summary" class="summary" aria-live="polite"><p class="summary-empty" data-i18n="scenario.summaryEmpty">No cases to review yet. Load a local example from the library or generate cases from the scenario.</p></div>
65
67
  <div class="draft-actions"><button id="save-btn" type="button" class="button" data-i18n="scenario.save">Save cases separately</button><span data-i18n="scenario.saveHint">Only saved cases can be run.</span></div>
66
68
  <details id="technical-details" class="technical-details">
@@ -108,6 +108,8 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
108
108
  .blocked-title { margin-top: 21px !important; color: var(--accent-text); }
109
109
  .draft-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px; margin: calc(var(--space-factor) * 16px) 0; }
110
110
  .draft-actions span { color: var(--muted); font-size: 12px; }
111
+ .import-actions label { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px; min-width: 0; }
112
+ .import-actions input { min-width: 0; max-width: 100%; }
111
113
  .technical-details { border-top: 1px solid var(--line); }
112
114
  .technical-details > summary { display: flex; justify-content: space-between; gap: 10px; padding: 15px 0; color: var(--ink); font-size: 13px; font-weight: 750; cursor: pointer; list-style: none; }
113
115
  .technical-details > summary::-webkit-details-marker { display: none; }
@@ -600,6 +600,25 @@ export async function startUIServer(options) {
600
600
  sendJSON(response, 200, result);
601
601
  return;
602
602
  }
603
+ if (url.pathname === '/api/import') {
604
+ const result = await mutation(async () => {
605
+ const { suite: inputSuite, flow } = parse(z.object({ suite: suiteSchema, flow: z.string().min(1).optional() }).strict(), body);
606
+ if (flow !== undefined && Buffer.byteLength(flow) > MAX_FLOW)
607
+ throw new HTTPError(413, 'The flow is too large.');
608
+ sourceName(inputSuite);
609
+ if (inputSuite.source) {
610
+ if (flow === undefined)
611
+ invalid('Select the source Markdown together with the case JSON.');
612
+ if (sha256(flow) !== inputSuite.source.sha256)
613
+ throw new HTTPError(409, 'The selected Markdown does not match the case source.');
614
+ }
615
+ // Import prepares a review copy; saving later assigns its workspace source path.
616
+ const { source: _source, ...suite } = inputSuite;
617
+ return { suite, script: compileSuite(suite) };
618
+ });
619
+ sendJSON(response, 200, result);
620
+ return;
621
+ }
603
622
  if (url.pathname === '/api/compile') {
604
623
  const result = await mutation(async () => {
605
624
  const { suite } = parse(z.object({ suite: suiteSchema }).strict(), body);
package/docs/mobile.md CHANGED
@@ -9,7 +9,7 @@ Choose **Mobile** on the scenario screen, describe the flow, and generate or dow
9
9
  With the installed CLI:
10
10
 
11
11
  ```sh
12
- npm install -g @ihyeon/flow-qa@0.2.0
12
+ npm install -g @ihyeon/flow-qa@0.2.2
13
13
  flow-qa init ./my-qa
14
14
  # Write your mobile flow in my-qa/flows/my-flow.md first.
15
15
  flow-qa plan flows/my-flow.md --platform mobile --prompt-only --workspace ./my-qa
package/docs/releases.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  The alpha entries below are historical release records. They document earlier package versions and checks.
4
4
 
5
+ ## 0.2.2: warn before leaving an unsaved draft
6
+
7
+ Requests the browser's confirmation before a refresh or tab close would discard an unsaved scenario or TC draft. Imported and manually edited cases are covered, including scenario edits to a saved case without a linked source. Unchanged saved cases stay quiet, and successful saving clears the warning. The browser controls the prompt; this does not add automatic saving or change review and execution gates.
8
+
9
+ Locally verified on macOS with Node.js 24.14.1 on 2026-10-20: 92 tests, type checking, UI syntax and design-system audit, a real Chromium reload prompt that preserves input when dismissed, clean/saved/imported/edited draft checks, existing browser and UI smoke tests, and a fresh tarball install with three passing browser cases. No model calls were used.
10
+
11
+ - Package: [`@ihyeon/flow-qa@0.2.2`](https://www.npmjs.com/package/@ihyeon/flow-qa/v/0.2.2)
12
+ - Release: [GitHub 0.2.2](https://github.com/hyeonse-swing/flow-qa/releases/tag/v0.2.2)
13
+
14
+ ## 0.2.1: import case files from the UI
15
+
16
+ Adds a file picker for a TC JSON file and its optional source Markdown. Import checks the suite and source hash before replacing the current draft, preserves unresolved review questions, and creates a separate copy when saved. Source text retains its line endings and trailing whitespace. This does not import error-to-test provenance metadata; original bundles remain separate. See [case file import](ui.md#import-existing-case-files).
17
+
18
+ Locally verified on macOS with Node.js 24.14.1 on 2026-10-15: 92 tests, type checking, UI syntax and design-system audit, real Chromium import/save/retry checks in English and Korean, existing demo pass/failure/blocked classifications, and a fresh tarball install with three passing browser cases. No model calls or native-device runs were used for this patch.
19
+
20
+ - Package: [`@ihyeon/flow-qa@0.2.1`](https://www.npmjs.com/package/@ihyeon/flow-qa/v/0.2.1)
21
+ - Release: [GitHub 0.2.1](https://github.com/hyeonse-swing/flow-qa/releases/tag/v0.2.1)
22
+
5
23
  ## 0.2.0: configurable mobile targets
6
24
 
7
25
  Adds native iOS/Android target settings, mobile case authoring before device setup, platform-aware compilation and execution, and app/device identity in review and reports. Users register an installed app and a prepared device when ready; no product-specific app is bundled. Existing web settings and suites remain readable. Node.js 24.8 or later is required by the mobile runtime. Actual native-device success requires a user-selected app and device. See [mobile setup and verification limits](mobile.md).
package/docs/ui.md CHANGED
@@ -7,7 +7,7 @@ Describe a user flow, review cases, and configure a target when ready to run. We
7
7
  Node.js 24.8 or later is required. Install Flow QA and start the web demo:
8
8
 
9
9
  ```sh
10
- npm install -g @ihyeon/flow-qa@0.2.0
10
+ npm install -g @ihyeon/flow-qa@0.2.2
11
11
  flow-qa browser install
12
12
  flow-qa ui --demo
13
13
  ```
@@ -42,6 +42,16 @@ For each attempt, the report can open or download an approved screenshot or text
42
42
 
43
43
  To enter fresh JSON after changing a scenario, open the JSON editor in case review and choose **New JSON draft**. Existing saved files remain; validate and save the new JSON before reviewing it for a run.
44
44
 
45
+ From version `0.2.2`, the UI requests the browser's leave-page confirmation when refreshing or closing a tab with an unsaved scenario or case draft. Unchanged saved cases do not trigger it; saving clears the warning. Choose to stay on the page to keep editing. The browser controls the prompt wording and availability. This is not automatic saving, and it does not guard actions within the UI that replace the draft.
46
+
47
+ ### Import existing case files
48
+
49
+ From version `0.2.1`, select a `flow-qa/suite-1` JSON file and, when it declares a source, its Markdown file in case review, then choose **Import case files**. The two file controls let you select files from different folders. For an error-to-test export, select `cases/error-to-test.json` and `flows/error-to-test.md`. A JSON file without a source can be imported alone or with a Markdown file.
50
+
51
+ Import validates the suite and checks the selected Markdown against its declared SHA-256 before replacing the draft. Cancelling or selecting invalid files preserves the current draft. The Markdown text, including line endings and trailing whitespace, is retained for saving. Markdown is limited to 128 KiB; the combined JSON request is limited to 256 KiB.
52
+
53
+ This creates an unsaved review copy. Choose **Save cases separately**, review the cases, and confirm review before running. Unresolved questions remain blocked. Saving assigns a new workspace source path and hash; it does not overwrite the input files or import `provenance.json`. If you need error-to-test `verify` / `link-run` provenance, keep the original bundle and use its original suite instead of this saved copy.
54
+
45
55
  ## Display and language
46
56
 
47
57
  The UI uses role tokens and preference utilities from `@hyeonse/design-system`, pinned to GitHub commit `38978c6a86f422fc0e63e919f600bc2dd3451e5c`. Flow QA implements its own workflow layout, buttons, and fields with those tokens.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ihyeon/flow-qa",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "license": "MIT",
5
5
  "author": "Hyeonse Im",
6
6
  "repository": {