@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 +1 -1
- package/README.md +1 -1
- package/dist/src/i18n.js +2 -0
- package/dist/src/ui/public/app.js +43 -3
- package/dist/src/ui/public/i18n.js +9 -0
- package/dist/src/ui/public/index.html +2 -0
- package/dist/src/ui/public/style.css +2 -0
- package/dist/src/ui/server.js +19 -0
- package/docs/mobile.md +1 -1
- package/docs/releases.md +18 -0
- package/docs/ui.md +11 -1
- package/package.json +1 -1
package/README.ko.md
CHANGED
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.
|
|
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; }
|
package/dist/src/ui/server.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|