@ihyeon/flow-qa 0.8.2 → 0.8.3
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/ui/public/app.js +26 -12
- package/dist/src/ui/public/i18n.js +4 -1
- package/dist/src/ui/public/index.html +1 -1
- package/dist/src/ui/public/style.css +3 -1
- package/docs/mobile.md +1 -1
- package/docs/releases.md +11 -0
- package/docs/ui.md +6 -4
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -16,7 +16,7 @@ CLI와 `127.0.0.1` 전용 웹 UI를 제공합니다. UI는 영어로 시작하
|
|
|
16
16
|
Node.js 24.x(24.22.1 이상) 또는 26.12.1 이상이 필요합니다. 브라우저는 첫 웹 실행 전에 설치합니다.
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
npm install -g @ihyeon/flow-qa@0.8.
|
|
19
|
+
npm install -g @ihyeon/flow-qa@0.8.3
|
|
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.x (24.22.1 or later) or 26.12.1 or later is required. Install Chromium before the first web run:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
npm install -g @ihyeon/flow-qa@0.8.
|
|
19
|
+
npm install -g @ihyeon/flow-qa@0.8.3
|
|
20
20
|
flow-qa browser install
|
|
21
21
|
flow-qa ui --demo
|
|
22
22
|
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const i18n = window.FlowQAI18n;
|
|
5
5
|
const t = i18n.t;
|
|
6
6
|
const names = { scenario: 'nav.scenario', library: 'nav.library', history: 'nav.history', settings: 'nav.settings' };
|
|
7
|
-
const app = { state: null, connected: false, displayTargets: [], draft: null, selectedCaseIds: new Set(), script: '', savedFile: '', savedHash: '', origin: 'none', sourceFlow: '', boundSource: null, stale: false, dirty: false, busy: false, planning: false, planRequest: 0, stopping: false, report: null, reportMarkdown: '', selectedRun: '', reportFilter: { search: '', status: 'all' }, rerunOrigin: null, rerunTargetMismatch: false, navigationVersion: 0, comparison: null, comparisonRequest: 0, comparisonBusy: false, comparisonStatus: 'comparison.selectBoth', comparisonPins: { baseline: null, current: null }, comparisonSelectionGeneration: 0, comparisonValidation: null, history: { runs: [], nextCursor: null, pagesLoaded: 0, loaded: false, loading: false, request: 0, debounce: null, kind: null, error: null, updatesAvailable: false, pendingRecentUpdate: false }, view: 'scenario' };
|
|
7
|
+
const app = { state: null, connected: false, displayTargets: [], draft: null, selectedCaseIds: new Set(), script: '', savedFile: '', savedHash: '', origin: 'none', sourceFlow: '', boundSource: null, stale: false, dirty: false, busy: false, planning: false, planRequest: 0, stopping: false, report: null, reportMarkdown: '', selectedRun: '', reportFilter: { search: '', status: 'all', sort: 'recorded' }, rerunOrigin: null, rerunTargetMismatch: false, navigationVersion: 0, comparison: null, comparisonRequest: 0, comparisonBusy: false, comparisonStatus: 'comparison.selectBoth', comparisonPins: { baseline: null, current: null }, comparisonSelectionGeneration: 0, comparisonValidation: null, history: { runs: [], nextCursor: null, pagesLoaded: 0, loaded: false, loading: false, request: 0, debounce: null, kind: null, error: null, updatesAvailable: false, pendingRecentUpdate: false }, view: 'scenario' };
|
|
8
8
|
|
|
9
9
|
function el(tag, className, value) {
|
|
10
10
|
const node = document.createElement(tag);
|
|
@@ -636,7 +636,7 @@
|
|
|
636
636
|
catch (error) { showError(error); }
|
|
637
637
|
finally { app.stopping = false; readiness(); }
|
|
638
638
|
}
|
|
639
|
-
async function loadReport(runId) { await work(async () => { const result = await api(`/api/report?runId=${encodeURIComponent(runId)}`); if (app.selectedRun !== runId) app.reportFilter = { search: '', status: 'all' }; app.report = result.report; app.reportMarkdown = result.markdown; app.selectedRun = runId; renderReport(); readiness(); $('report-panel').hidden = false; $('report-panel').scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }
|
|
639
|
+
async function loadReport(runId) { await work(async () => { const result = await api(`/api/report?runId=${encodeURIComponent(runId)}`); if (app.selectedRun !== runId) app.reportFilter = { search: '', status: 'all', sort: 'recorded' }; app.report = result.report; app.reportMarkdown = result.markdown; app.selectedRun = runId; renderReport(); readiness(); $('report-panel').hidden = false; $('report-panel').scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }
|
|
640
640
|
async function prepareFailedCases() { await work(async () => {
|
|
641
641
|
const runId = app.selectedRun;
|
|
642
642
|
const report = app.report;
|
|
@@ -672,6 +672,11 @@
|
|
|
672
672
|
} catch (error) { if (runId === app.selectedRun && locale === i18n.locale()) showError(error); }
|
|
673
673
|
}
|
|
674
674
|
const reportStatuses = ['all', 'failed-error', 'passed', 'failed', 'error', 'blocked', 'flaky', 'skipped'];
|
|
675
|
+
function reportCaseTotalAttemptDurationMs(attempts) {
|
|
676
|
+
if (!Array.isArray(attempts) || attempts.length === 0 || !attempts.every((attempt) => typeof attempt?.durationMs === 'number' && Number.isFinite(attempt.durationMs) && attempt.durationMs >= 0)) return null;
|
|
677
|
+
const total = attempts.reduce((sum, attempt) => sum + attempt.durationMs, 0);
|
|
678
|
+
return Number.isFinite(total) ? total : null;
|
|
679
|
+
}
|
|
675
680
|
function reportCaseMatchesSearch(item, query) {
|
|
676
681
|
if (!query) return true;
|
|
677
682
|
const values = [item.id, item.title, item.reason, ...(item.expected || [])];
|
|
@@ -697,9 +702,11 @@
|
|
|
697
702
|
const label = option.value === 'all' ? t('report.filterAll') : option.value === 'failed-error' ? t('report.filterFailedError') : i18n.status(option.value);
|
|
698
703
|
option.textContent = `${label} (${counts[option.value]})`;
|
|
699
704
|
}
|
|
705
|
+
for (const option of $('report-case-sort').options) option.textContent = t(option.value === 'duration-desc' ? 'report.sortDurationDescending' : 'report.sortRecorded');
|
|
700
706
|
if ($('report-case-search').value !== app.reportFilter.search) $('report-case-search').value = app.reportFilter.search;
|
|
701
707
|
$('report-case-status').value = app.reportFilter.status;
|
|
702
|
-
$('report-case-
|
|
708
|
+
$('report-case-sort').value = app.reportFilter.sort;
|
|
709
|
+
$('report-case-reset').disabled = !app.reportFilter.search && app.reportFilter.status === 'all' && app.reportFilter.sort === 'recorded';
|
|
703
710
|
return cases;
|
|
704
711
|
}
|
|
705
712
|
function renderReportCases() {
|
|
@@ -707,19 +714,25 @@
|
|
|
707
714
|
const cases = renderReportFilters();
|
|
708
715
|
const query = app.reportFilter.search.trim().toLowerCase();
|
|
709
716
|
const status = app.reportFilter.status;
|
|
710
|
-
const shown = cases.filter((item) => (status === 'all' || (status === 'failed-error' ? item.status === 'failed' || item.status === 'error' : item.status === status)) && reportCaseMatchesSearch(item, query));
|
|
711
|
-
|
|
717
|
+
const shown = cases.map((item, index) => ({ item, index })).filter(({ item }) => (status === 'all' || (status === 'failed-error' ? item.status === 'failed' || item.status === 'error' : item.status === status)) && reportCaseMatchesSearch(item, query));
|
|
718
|
+
if (app.reportFilter.sort === 'duration-desc') shown.sort((left, right) => {
|
|
719
|
+
const leftDuration = reportCaseTotalAttemptDurationMs(left.item.attempts);
|
|
720
|
+
const rightDuration = reportCaseTotalAttemptDurationMs(right.item.attempts);
|
|
721
|
+
if (leftDuration === null) return rightDuration === null ? left.index - right.index : 1;
|
|
722
|
+
if (rightDuration === null) return -1;
|
|
723
|
+
return rightDuration - leftDuration || left.index - right.index;
|
|
724
|
+
});
|
|
725
|
+
const displayed = shown.map(({ item }) => item);
|
|
726
|
+
$('report-case-count').textContent = t('report.caseCount', { shown: displayed.length, total: cases.length });
|
|
712
727
|
const host = $('report-case-list'); replace(host);
|
|
713
728
|
if (!cases.length) { host.append(el('p', 'empty', t('report.empty'))); return; }
|
|
714
|
-
if (!
|
|
715
|
-
for (const item of
|
|
729
|
+
if (!displayed.length) { host.append(el('p', 'empty', t('report.noMatches'))); return; }
|
|
730
|
+
for (const item of displayed) {
|
|
716
731
|
const attemptCount = (item.attempts || []).length;
|
|
717
732
|
const attemptCountLabel = t(attemptCount === 1 ? 'report.attemptCountSingular' : 'report.attemptCountPlural', { count: attemptCount });
|
|
718
733
|
const section = el('section', 'case-report'); section.append(el('h3', '', `${item.id} · ${item.title} · ${i18n.status(item.status)} · ${attemptCountLabel}`));
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (Number.isFinite(totalAttemptTime)) section.append(el('p', 'hint case-attempt-time', t('report.totalAttemptTime', { duration: totalAttemptTime })));
|
|
722
|
-
}
|
|
734
|
+
const totalAttemptTime = reportCaseTotalAttemptDurationMs(item.attempts);
|
|
735
|
+
if (totalAttemptTime !== null) section.append(el('p', 'hint case-attempt-time', t('report.totalAttemptTime', { duration: totalAttemptTime })));
|
|
723
736
|
if (item.reason) section.append(el('p', '', t('app.reason', { reason: item.reason })));
|
|
724
737
|
section.append(el('p', '', t('app.expected'))); const expected = el('ul', 'compact-list'); for (const check of item.expected || []) expected.append(el('li', '', check)); section.append(expected);
|
|
725
738
|
for (const [attemptIndex, attempt] of (item.attempts || []).entries()) {
|
|
@@ -841,7 +854,8 @@
|
|
|
841
854
|
$('report-rerun').addEventListener('click', prepareFailedCases);
|
|
842
855
|
$('report-case-search').addEventListener('input', (event) => { app.reportFilter.search = event.currentTarget.value; renderReportCases(); });
|
|
843
856
|
$('report-case-status').addEventListener('change', (event) => { app.reportFilter.status = event.currentTarget.value; renderReportCases(); });
|
|
844
|
-
$('report-case-
|
|
857
|
+
$('report-case-sort').addEventListener('change', (event) => { app.reportFilter.sort = event.currentTarget.value; renderReportCases(); });
|
|
858
|
+
$('report-case-reset').addEventListener('click', () => { app.reportFilter = { search: '', status: 'all', sort: 'recorded' }; renderReportCases(); });
|
|
845
859
|
$('target-form').addEventListener('submit', saveTarget);
|
|
846
860
|
$('target-kind').addEventListener('change', targetFields); targetFields();
|
|
847
861
|
$('language-select').addEventListener('change', (event) => i18n.setLocale(event.currentTarget.value));
|
|
@@ -146,6 +146,9 @@
|
|
|
146
146
|
'report.searchLabel': ['Search cases', '케이스 검색'],
|
|
147
147
|
'report.searchPlaceholder': ['Search ID, title, expected result, or errors', 'ID, 제목, 기대 결과 또는 오류 검색'],
|
|
148
148
|
'report.statusLabel': ['Case status', '케이스 상태'],
|
|
149
|
+
'report.sortLabel': ['Case order', '케이스 정렬'],
|
|
150
|
+
'report.sortRecorded': ['Recorded order', '기록 순서'],
|
|
151
|
+
'report.sortDurationDescending': ['Longest attempt time first', '시도 시간이 긴 순'],
|
|
149
152
|
'report.filterAll': ['All statuses', '전체 상태'],
|
|
150
153
|
'report.filterFailedError': ['Failed/error only', '실패·오류만'],
|
|
151
154
|
'report.reset': ['Reset filters', '필터 초기화'],
|
|
@@ -155,7 +158,7 @@
|
|
|
155
158
|
'report.totalAttemptTime': ['Total attempt time: {duration}ms', '시도 시간 합계: {duration}ms'],
|
|
156
159
|
'report.empty': ['This report has no cases.', '이 보고서에는 케이스가 없습니다.'],
|
|
157
160
|
'report.noMatches': ['No cases match these filters.', '필터에 맞는 케이스가 없습니다.'],
|
|
158
|
-
'report.filterHint': ['Filters only change the displayed cases. Downloads include all cases, and failed/error preparation uses all recorded failed/error cases regardless of visibility.', '
|
|
161
|
+
'report.filterHint': ['Filters and sorting only change the displayed cases. Downloads include all cases, and failed/error preparation uses all recorded failed/error cases regardless of visibility.', '필터와 정렬은 표시되는 케이스에만 적용됩니다. 다운로드에는 모든 케이스가 포함되고, 실패·오류 준비에는 표시 여부와 관계없이 기록된 모든 실패·오류 케이스가 사용됩니다.'],
|
|
159
162
|
'history.prepareFailed': ['Prepare failed/error cases', '실패·오류 케이스 준비'],
|
|
160
163
|
'history.prepareHint': ['Only failed and error cases are selected. Passed, flaky, blocked, and skipped cases are excluded. Preparation never starts a run.', '실패·오류 케이스만 선택합니다. 통과·간헐 실패·차단·건너뜀 케이스는 제외됩니다. 준비만으로 실행은 시작되지 않습니다.'],
|
|
161
164
|
'history.downloadMarkdown': ['Download Markdown', 'Markdown 다운로드'],
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
<div id="comparison-results" hidden><div id="comparison-warnings"></div><div id="comparison-summary"></div><div class="comparison-filter-row"><label for="comparison-filter" data-i18n="comparison.filter">Show cases</label><select id="comparison-filter" aria-controls="comparison-content"><option value="" data-i18n="comparison.all">All categories</option><option value="newly-failed" data-i18n="comparison.newlyFailed">Newly failed</option><option value="now-passed" data-i18n="comparison.nowPassed">Now passed</option><option value="changed" data-i18n="comparison.changed">Changed status</option><option value="unchanged" data-i18n="comparison.unchanged">Same status</option><option value="added" data-i18n="comparison.added">Added to current</option><option value="missing" data-i18n="comparison.missing">Missing from current</option></select></div><div class="actions comparison-actions"><button type="button" id="comparison-md" class="button" data-i18n="comparison.downloadMarkdown" disabled>Download comparison Markdown</button><button type="button" id="comparison-json" class="button quiet" data-i18n="comparison.downloadJson" disabled>Download comparison JSON</button><button type="button" id="compare-open-baseline" class="button quiet" data-i18n="comparison.openBaseline" disabled>View baseline report</button><button type="button" id="compare-open-current" class="button quiet" data-i18n="comparison.openCurrent" disabled>View current report</button></div><div id="comparison-content"></div></div>
|
|
105
105
|
</div>
|
|
106
106
|
</section>
|
|
107
|
-
<section id="report-panel" class="report-panel" hidden><div class="section-head"><span class="section-number">03</span><div><h2 data-i18n="history.report">Detailed report</h2><p id="report-heading"></p></div></div><div class="section-body"><div class="actions"><button type="button" id="report-md" class="button" data-i18n="history.downloadMarkdown">Download Markdown</button><button type="button" id="report-json" class="button quiet" data-i18n="history.downloadJson">Download JSON</button><button type="button" id="report-rerun" class="button primary" disabled data-i18n="history.prepareFailed">Prepare failed/error cases</button></div><p class="hint" data-i18n="history.prepareHint">Passed, flaky, blocked, and skipped cases are excluded. Preparation does not start a run.</p><p class="hint" data-i18n="history.evidenceHint">The report summarizes results. Open or download screenshots and recordings from each attempt. Evidence files are not included in report downloads.</p><div class="report-case-filters"><div><label for="report-case-search" data-i18n="report.searchLabel">Search cases</label><input id="report-case-search" type="search" aria-controls="report-case-list" placeholder="Search ID, title, expected result, or errors" data-i18n-placeholder="report.searchPlaceholder"></div><div><label for="report-case-status" data-i18n="report.statusLabel">Case status</label><select id="report-case-status" aria-controls="report-case-list"><option value="all">All</option><option value="failed-error">Failed/error only</option><option value="passed">passed</option><option value="failed">failed</option><option value="error">error</option><option value="blocked">blocked</option><option value="flaky">flaky</option><option value="skipped">skipped</option></select></div><button type="button" id="report-case-reset" class="button quiet" data-i18n="report.reset">Reset filters</button></div><p id="report-case-count" class="hint" role="status" aria-live="polite"></p><p class="hint" data-i18n="report.filterHint">Filters only change the displayed cases. Downloads include all cases, and failed/error preparation uses all recorded failed/error cases regardless of visibility.</p><div id="report-content"></div></div></section>
|
|
107
|
+
<section id="report-panel" class="report-panel" hidden><div class="section-head"><span class="section-number">03</span><div><h2 data-i18n="history.report">Detailed report</h2><p id="report-heading"></p></div></div><div class="section-body"><div class="actions"><button type="button" id="report-md" class="button" data-i18n="history.downloadMarkdown">Download Markdown</button><button type="button" id="report-json" class="button quiet" data-i18n="history.downloadJson">Download JSON</button><button type="button" id="report-rerun" class="button primary" disabled data-i18n="history.prepareFailed">Prepare failed/error cases</button></div><p class="hint" data-i18n="history.prepareHint">Passed, flaky, blocked, and skipped cases are excluded. Preparation does not start a run.</p><p class="hint" data-i18n="history.evidenceHint">The report summarizes results. Open or download screenshots and recordings from each attempt. Evidence files are not included in report downloads.</p><div class="report-case-filters"><div><label for="report-case-search" data-i18n="report.searchLabel">Search cases</label><input id="report-case-search" type="search" aria-controls="report-case-list" placeholder="Search ID, title, expected result, or errors" data-i18n-placeholder="report.searchPlaceholder"></div><div><label for="report-case-status" data-i18n="report.statusLabel">Case status</label><select id="report-case-status" aria-controls="report-case-list"><option value="all">All</option><option value="failed-error">Failed/error only</option><option value="passed">passed</option><option value="failed">failed</option><option value="error">error</option><option value="blocked">blocked</option><option value="flaky">flaky</option><option value="skipped">skipped</option></select></div><div><label for="report-case-sort" data-i18n="report.sortLabel">Case order</label><select id="report-case-sort" aria-controls="report-case-list"><option value="recorded" data-i18n="report.sortRecorded">Recorded order</option><option value="duration-desc" data-i18n="report.sortDurationDescending">Longest attempt time first</option></select></div><button type="button" id="report-case-reset" class="button quiet" data-i18n="report.reset">Reset filters</button></div><p id="report-case-count" class="hint" role="status" aria-live="polite"></p><p class="hint" data-i18n="report.filterHint">Filters and sorting only change the displayed cases. Downloads include all cases, and failed/error preparation uses all recorded failed/error cases regardless of visibility.</p><div id="report-content"></div></div></section>
|
|
108
108
|
</section>
|
|
109
109
|
<section id="view-settings" class="view" hidden>
|
|
110
110
|
<div class="page-heading"><p class="eyebrow" data-i18n="settings.eyebrow">04 / CONNECTIONS</p><h1 data-i18n="nav.settings">Connections</h1><p data-i18n="settings.intro">Configure run targets and models separately. Key values are read only from server environment variables.</p></div>
|
|
@@ -105,7 +105,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
|
|
105
105
|
.runtime-panel > p { margin: 0; overflow-wrap: anywhere; }
|
|
106
106
|
#case-readiness { margin: 0 0 12px; }
|
|
107
107
|
#report-content { margin-top: 20px; }
|
|
108
|
-
.report-case-filters { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) auto; align-items: end; gap: 12px; margin-top: 24px; }
|
|
108
|
+
.report-case-filters { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1.4fr) auto; align-items: end; gap: 12px; margin-top: 24px; }
|
|
109
109
|
.report-case-filters > div { min-width: 0; }
|
|
110
110
|
.report-case-filters label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: 750; }
|
|
111
111
|
.report-case-filters input, .report-case-filters select { display: block; width: 100%; min-width: 0; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius-control); background: var(--paper); color: var(--ink); }
|
|
@@ -200,6 +200,8 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
|
|
200
200
|
.model-block h3 { margin: 0 0 7px; font-size: 16px; }
|
|
201
201
|
.model-block > p { margin: 0 0 16px; color: var(--muted); font-size: 12px; line-height: 1.65; }
|
|
202
202
|
@media (max-width: 1100px) {
|
|
203
|
+
.report-case-filters { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
|
204
|
+
.report-case-filters .button { justify-self: start; }
|
|
203
205
|
.workflow { grid-template-columns: minmax(0, 1fr); }
|
|
204
206
|
.execution-section { grid-column: auto; }
|
|
205
207
|
.summary { min-height: 170px; }
|
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.8.
|
|
12
|
+
npm install -g @ihyeon/flow-qa@0.8.3
|
|
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,17 @@
|
|
|
2
2
|
|
|
3
3
|
The alpha entries below are historical release records. They document earlier package versions and checks.
|
|
4
4
|
|
|
5
|
+
## 0.8.3: find slow cases by recorded attempt time
|
|
6
|
+
|
|
7
|
+
Detailed reports now offer **Case order → Longest attempt time first**. The view sorts by the sum of valid recorded attempt durations, keeps equal totals in their original order, and places unknown or invalid totals after all valid totals, including zero. **Recorded order** restores the original order; **Reset filters** also resets sorting. English and Korean labels are available.
|
|
8
|
+
|
|
9
|
+
Sorting combines with case search and status filters, survives navigation, language changes, refreshes, and reopening the same report, and resets only when another report opens successfully. Complete downloads, evidence indices, and failed/error preparation keep their original data and scope.
|
|
10
|
+
|
|
11
|
+
Locally verified on macOS with Node.js 24.22.1 on 2026-11-07 (Asia/Tokyo): 118 tests, type checking, UI syntax/design-system audit, the report-explorer browser smoke, and package checks passed. Browser fixtures covered descending totals, stable ties, valid zero, unknown/invalid timing, recorded-order restoration, filters, full exports, original evidence links, rerun scope, persistence/reset behavior, English/Korean, and a 390px layout. A fresh tarball installation ran seven passing browser cases and verified sorting and reset in the installed UI. No model calls, external logins, or native-device runs were used.
|
|
12
|
+
|
|
13
|
+
- Package: [`@ihyeon/flow-qa@0.8.3`](https://www.npmjs.com/package/@ihyeon/flow-qa/v/0.8.3)
|
|
14
|
+
- Release: [GitHub 0.8.3](https://github.com/hyeonse-swing/flow-qa/releases/tag/v0.8.3)
|
|
15
|
+
|
|
5
16
|
## 0.8.2: show total recorded attempt time per case
|
|
6
17
|
|
|
7
18
|
Detailed report cases now show **Total attempt time**, summing saved attempt durations in milliseconds. The total includes all recorded attempts and supports English/Korean labels. No attempts, incomplete/invalid timing, or a non-finite sum omits the total; a valid measured zero remains visible. This is recorded attempt time, not overall elapsed run time. Search/filter behavior, report exports, and execution remain unchanged.
|
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.x (24.22.1 or later) or 26.12.1 or later is required. Node 25 is unsupported. Upgrade the Node runtime and restart any running UI process; updating Flow QA alone does not patch Node. Install Flow QA and start the web demo:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
npm install -g @ihyeon/flow-qa@0.8.
|
|
10
|
+
npm install -g @ihyeon/flow-qa@0.8.3
|
|
11
11
|
flow-qa browser install
|
|
12
12
|
flow-qa ui --demo
|
|
13
13
|
```
|
|
@@ -82,13 +82,15 @@ When recent run records change, a single loaded page refreshes automatically. If
|
|
|
82
82
|
|
|
83
83
|
From `0.8.1`, each case heading also shows the number of attempts recorded in that report. Zero attempts is shown explicitly. This count does not infer a retry policy or change the recorded case status. From `0.8.2`, **Total attempt time** sums the recorded attempt durations in milliseconds. It is not the overall elapsed run time. The total is shown only when at least one attempt exists and every duration is a finite, nonnegative number; incomplete/invalid timing is omitted, while a recorded total of zero is shown.
|
|
84
84
|
|
|
85
|
+
From `0.8.3`, **Case order → Longest attempt time first** sorts visible cases by their total recorded attempt time. Equal totals keep their recorded order. Missing, incomplete, invalid, or overflowing totals appear after valid totals, including zero. **Recorded order** restores the original report order.
|
|
86
|
+
|
|
85
87
|
From `0.8.0`, open **Run history → Detailed report** and use **Search cases** with **Case status** to narrow the displayed results. Search ignores case and surrounding whitespace, and matches case IDs, titles, reasons, expected results, attempt errors, secondary errors, step labels, and step errors. User-written Korean text is supported. Artifact paths and translated status labels are not search fields.
|
|
86
88
|
|
|
87
|
-
**Failed/error only** shows those two recorded statuses; the other status choices distinguish passed, failed, error, blocked, flaky, and skipped. Status-option counts describe the entire open report, while the displayed count shows matches out of that report's recorded cases. A partial run's omitted cases are not included in those counts; its selected/full-suite scope remains visible separately. Report-level warnings remain visible even when no case matches. **Reset filters** clears
|
|
89
|
+
**Failed/error only** shows those two recorded statuses; the other status choices distinguish passed, failed, error, blocked, flaky, and skipped. Status-option counts describe the entire open report, while the displayed count shows matches out of that report's recorded cases. A partial run's omitted cases are not included in those counts; its selected/full-suite scope remains visible separately. Report-level warnings remain visible even when no case matches. **Reset filters** clears search and status and restores recorded order.
|
|
88
90
|
|
|
89
|
-
Filters stay while navigating, changing language, refreshing history/settings, or reopening the same report. Successfully opening another report resets them; a failed report load preserves the current report and
|
|
91
|
+
Filters and sorting stay while navigating, changing language, refreshing history/settings, or reopening the same report. Successfully opening another report resets them; a failed report load preserves the current report, filters, and sorting. Filters and sorting reset on page reload. Evidence links still refer to each case's original attempt and artifact.
|
|
90
92
|
|
|
91
|
-
These controls change only displayed cases. Markdown and JSON downloads contain every recorded case, and **Prepare failed/error cases** still prepares every recorded failed/error case from the report, even if hidden by the current filters. Filtering
|
|
93
|
+
These controls change only displayed cases. Markdown and JSON downloads contain every recorded case, and **Prepare failed/error cases** still prepares every recorded failed/error case from the report, even if hidden by the current filters. Filtering and sorting do not alter a saved report, the scenario draft, review confirmation, or execution selection.
|
|
92
94
|
|
|
93
95
|
### Prepare failed cases
|
|
94
96
|
|