Skip to content

feat: GUI enhancements, UI test suite, and defect fixes - #2

Merged
voidvec merged 12 commits into
masterfrom
feature/ui-test-and-fixes
Aug 6, 2026
Merged

voidvec merged 12 commits into
masterfrom
feature/ui-test-and-fixes

Conversation

@voidvec

@voidvec voidvec commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

This feature branch consolidates all GUI enhancements and the comprehensive UI test suite, plus three defect fixes discovered during testing.

Commits included (9 commits on top of 95c0151)

Commit Description
feat(http) Postman-like auth, body types, query params and response extras
feat(gui) Request-body syntax highlighting, FormData builder symmetry
M0 Shared logic layer + unit tests (OAuth2 / env vars / Collections / Postman v2.1)
M1 Environment variable GUI end-to-end
M2 OAuth2 runtime pipeline + GUI settings
M3 Response body toolbar (search / copy / save / pretty toggle)
M4 Collection tree panel + Postman I/O + R2 auth serialization
M5 Tests, docs, and missing authOAuth2 builder
fix Three defects resolved from UI test audit (see below)

Defect fixes

  1. Abort empty stub: onAbortTask() now stores taskId from NetworkReply and calls stopRequest()
  2. Auth env var expansion: applyAuthHeader() resolves {{var}} placeholders in auth credentials
  3. Silent auth fallback: onSendRequest() warns via QMessageBox when auth type is selected but credentials are empty

Test results

100% tests passed, 0 tests failed out of 7
Total Test time = 29.16 sec

voidvec added 9 commits August 6, 2026 12:29
…nse extras

- Add AuthConfig with Basic/Bearer/ApiKey(in Header or Query) support
- Extend RequestContext with BodyType(raw/json/xml/form-urlencoded/binary/form-data),
  binaryBody and queryParams; fluent Builder methods for each
- Enhance ResponseResult with cookies, json() and contentType()
- Apply auth/body/query params in NetworkRequest pipeline
- GUI: auth settings dialog (incl. ApiKey), binary body page, response Cookies tab,
  route UI through RequestContextBuilder
- Tests: AuthTests/BodyTypeTests suites + GUI buildRequestContext/auth/cookies cases
- Fix greedy hex-escape bugs in test fixtures ('\x00DEF' -> C7744, '\x00data' -> 0x00DA)
…y, cleanup and test.bat

- Add NetworkRequestTool::applyBodySyntaxHighlighting to colorize raw JSON/XML
  request bodies (reuses JsonSyntaxHighlighter/XmlSyntaxHighlighter); clears the
  4 TODO placeholders in onBodyTypeChanged/onRawTypeChanged
- Add RequestContextBuilder::bodyFormData(files, kvPairs) so BodyType::FormData
  has a symmetric builder; add testBodyFormDataBuilder (BuilderTests)
- Section F cleanup: merge duplicate cmb_body_type slot onBodyTypeComboChanged
  into onBodyTypeChanged (also fixes cmb_raw_type left enabled after binary), and
  remove the duplicate table_headers cellChanged lambda in the constructor
- Add scripts/test.bat (Windows counterpart of test_local.sh): Qt/VS auto-detect,
  configure + build + ctest, with /clean /nobuild /debug /help and ctest exit code
  propagated
…/ Postman v2.1)

New files:
- include/authconfig.h: OAuth2Config with 4 grant types + bearer token header
- include/environment.h, source/environment.cpp: env var substitution
  ({{var}} in url/headers/body/queryParams/auth fields)
- include/oauth2tokencache.h, source/oauth2tokencache.cpp: Q_GLOBAL_STATIC
  singleton token cache with expiry
- include/collectionmodel.h, source/collectionmodel.cpp: in-memory Collection
  tree (folders + requests) with JSON serde, findById, add/remove
- include/postmanconverter.h, source/postmanconverter.cpp: Postman v2.1
  bidirectional converter (auth round-trip, nested folders, request mapping)
- test/test_environment.*, test/test_collection.*, test/test_postman.*,
  test/main_collection.cpp: BuilderTests env cases + CollectionTests

Modified:
- include/requestcontext.h: BodyType::FormData + bodyFormData() builder
- source/CMakeLists.txt, test/CMakeLists.txt: added new sources
- scripts/test.bat: Windows test runner (walk-up repo root, goto VS detect,
  no EnableDelayedExpansion, echo brackets avoided)

Fixes across 4 compile rounds: struct order, member/method name collision,
Q_GLOBAL_STATIC access, QJsonArray::reserve, Qt5.15 QString::replace API,
missing QJsonArray/QJsonObject includes, Postman import empty-id collision.

All 7 test suites pass.
Pipeline (B.2):
- source/networkrequest.cpp: wire applyEnvironment() into setRequestContext()
  after m_upContext = move and before m_url = QUrl, so {{var}} placeholders
  are resolved before the URL/headers/body/query/auth are finalized.

EnvironmentStore (B.3):
- include/environmentstore.h, source/environmentstore.cpp: multi-environment
  manager with JSON persistence ({active, environments[{name, variables}]}).
  NETWORK_EXPORT, no UI dependency.
- source/CMakeLists.txt: add environmentstore.cpp + .h

GUI (B.4):
- networkrequesttool.h: EnvironmentStore m_envStore, QComboBox/PushButton
  members, onEnvironmentChanged/onManageEnvironments slots, helpers.
- networkrequesttool.cpp:
  - initializeUI(): inject cmb_environment + btn_manage_env into request toolbar
  - buildRequestContext(): append .environment(m_envStore.activeVariables())
  - loadEnvironments(): load from environments.json, create default 'Dev' on
    first run (host=localhost, port=8080)
  - onManageEnvironments(): full dialog with QListWidget env picker +
    QTableWidget variable editor + Add/Remove + JSON persistence

UiTests:
- test_qtrequester.h/.cpp: test_envDropdownSwitch verifies combo exists,
  No Environment yields empty map, Dev selection populates activeVariables,
  buildRequestContext carries the env map.

All 7 test suites green.
R1 — applyAuthConfig OAuth2 case:
- source/networkrequest.cpp: fold AuthType::OAuth2 into Basic/Bearer group
  so the fetched access_token is written to the Authorization header.

Refactor start() -> performSend():
- source/networkcommonrequest.h: add #include <functional>, private methods
  performSend() and fetchOAuth2Token(onReady).
- source/networkcommonrequest.cpp: extract send block (L98-243) into
  performSend(). start() calls fetchOAuth2Token for OAuth2, else directly
  calls performSend().

fetchOAuth2Token async token retrieval:
- Cache key via OAuth2TokenCache::makeKey(). Synchronous fast path on cache
  hit (find() + isValid()). Miss -> POST to tokenUrl via thread-affine NAM
  (NetworkRequestManager::acquireThreadNam()). Parse JSON {access_token,
  refresh_token, expires_in}, store to OAuth2TokenCache::instance().store().
  Errors reported via setError() + emit response(ToFailedResult()).

401 auto-refresh:
- m_bOAuthRefreshed flag moved from NetworkCommonRequest to base class
  NetworkRequest (source/networkrequest.h). Reset in setRequestContext().
- handleFailure() in base class gains OAuth2 401 branch: after tryRetry(),
  before redirect. On 401 + OAuth2 + !refreshed + refreshToken available:
  set refreshed=true, swap grant to RefreshToken, cleanup + restart().
  Second 401 skips refresh (guard), emits failure.

GUI OAuth2 settings (A.4):
- RequestSettings gains 8 OAuth2 fields (grantType/clientId/clientSecret/
  scopes/tokenUrl/username/password/refreshToken).
- onSettingsClicked(): add 'OAuth2' to authTypeCombo, add OAuth2 controls
  (grant combo + 7 QLineEdits), show/hide by grant type (Password shows
  user/pass, Refresh Token shows refresh field).
- buildAuthConfig(): new OAuth2 branch builds OAuth2Config from settings
  and returns AuthConfig::oauth2(cfg).

All 7 test suites green.
GUI-only enhancement — inject a compact toolbar into the response body tab:

Search (QLineEdit + prev/next buttons):
- onResponseSearchChanged: re-search on every keystroke
- doResponseSearch: QTextDocument::find() to collect all matches,
  highlight with QTextEdit::ExtraSelection (yellow for all, orange for
  current hit)
- navigateSearchHit(): wrap-around prev/next with scroll-to-hit

Copy: QApplication::clipboard()->setText(response body)
Save: QFileDialog::getSaveFileName + QFile::write(response body)
Pretty/Raw toggle: re-format JSON with QJsonDocument::Indented (pretty)
  or show raw m_lastResponseBody. Non-JSON responses ignore the toggle.

onResponse() now stores m_lastResponseBody + m_isResponseJson.
Response toolbar injected at index 1 in verticalLayout_response_body
(after m_labelResponseInfo, mirroring existing injection pattern).

All 7 test suites green.
R2 — auth serialize/deserialize:
- saveToDisk(): serialize all auth fields (type/Basic/Bearer/ApiKey/OAuth2)
  alongside method/url/body/headers so authenticated requests survive
  save/load and Postman round-trips.
- loadRequestFromJson(): new reusable method extracts form population from
  loadFromDisk; restores auth fields into m_settings. loadFromDisk() is
  now a thin file-I/O wrapper calling loadRequestFromJson.

D.1 — loadFromDisk refactor:
- Dead loadFromDisk() refactored into loadRequestFromJson(const QJsonObject&),
  reusable by collection-click (D.3) and Postman import (E.2).
- loadFromDisk() = open file + parse JSON + call loadRequestFromJson.

D.2-D.4 — Collection tree panel:
- buildCollectionPanel(): inject QTreeWidget + toolbar (+Coll/+Folder/+Req/
  Import/Export) into left panel layout above history list.
- loadCollection()/saveCollection(): persist to collection.json via
  Collection::load/save.
- populateCollectionTree(): rebuild tree from Collection model (folders
  have folder icon, requests have file icon).
- onNewCollection(): create a named collection.
- onAddCollectionFolder(): add folder under selected node or root.
- onAddCollectionRequest(): snapshot current form (url/method/body/headers/
  auth) as a collection request node.
- onCollectionItemClicked(): load request into form via loadRequestFromJson.
- onImportPostman(): QFileDialog -> PostmanConverter::fromPostmanV21.
- onExportPostman(): PostmanConverter::toPostmanV21 -> QFileDialog write.

All 7 test suites green.
Tests:
- HttpTestServer: add /oauth/token endpoint supporting client_credentials,
  password, and refresh_token grants with proper validation.
- AuthTests: 5 new OAuth2 integration tests (client_credentials, cache hit,
  password grant, refresh_token, tokenUrl-missing rejection).
- UiTests: testSaveAndLoadDiskWithAuth (R2 auth serialisation round-trip
  for Bearer + OAuth2 fields), testResponseSearch (search highlighting +
  prev/next navigation).

Bug fix:
- include/requestcontext.h: add missing RequestContextBuilder::authOAuth2()
  method (A.2 from original plan, missed during M0).

Docs:
- Changelog: v2.3.0 entry covering all M0-M4 features.
- README.zh-CN.md: add OAuth2, environments, response toolbar, collections,
  Postman v2.1 to feature list.
- AGENTS.md: update directory map (16 public headers, 7 test suites,
  39+ source files).

All 7 test suites green.
1. Fix abort empty stub: onAbortTask() now stores taskId from NetworkReply
   and calls stopRequest(); onResponse() clears taskId on completion.

2. Fix auth env var expansion: applyAuthHeader() now resolves {{var}}
   placeholders in Basic/Bearer/ApiKey credentials before writing headers.

3. Fix silent auth fallback: onSendRequest() now warns via QMessageBox when
   a non-None auth type is selected but required credentials are empty.

Tests updated:
- testAbortRunningRequest: real abort flow with mock server (was QSKIP)
- testApplyAuthHeader: added {{token}} env expansion case
- testAuthEmptyCredentialFallback: added warning dialog auto-dismiss case
// --- Simpler approach: embed the working state in lambdas ---
// Pre-populate all env data from the store
// Since we can't iterate envs easily from the public API, rebuild from the
// combination of names + known data.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 MAJOR — onManageEnvironments() 中存在大量死代码和绕过 EnvironmentStore API 的手动 JSON 解析

问题:

  1. loadVarsForEnv lambda (L2004-2013): 被定义但从未被调用。它的 for 循环体为空(; // not used),实际上是一个死 lambda。
  2. L2018-2023: envVars 变量和空代码块 {} — 声明后从未使用。
  3. L2025-2032: 创建 workStore 并加载文件,但后续代码(L2072-2073)disconnect 后重新 connect,使 workStore 完全白费。
  4. L2050-2070: 直接打开 environments.json 手动解析 JSON,绕过了 EnvironmentStore 的公开 API。如果 EnvironmentStore 的存储格式发生变化,这段代码会静默失效。

建议:

  • 删除 L2004-2013 的 loadVarsForEnv lambda(死代码)
  • 删除 L2018-2023 的 envVars 空块
  • 删除 L2025-2032 的 workStore 创建
  • 删除 L2038-2046 的第一个 connect(后续被 disconnect 覆盖)
  • 在 EnvironmentStore 中添加 QMap<QString,QString> variables(const QString &name) 方法,替代手动 JSON 解析

@@ -484,60 +642,47 @@ void NetworkRequestTool::updateContentTypeHeader()

void NetworkRequestTool::onSendRequest()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — buildAuthConfig() 与 applyAuthHeader() 中环境变量解析不一致

applyAuthHeader() 现在对 auth 凭据执行 {{var}} 展开(L1735+),但 buildAuthConfig() 直接把原始字符串传给 AuthConfig。虽然 library 层的 applyEnvironment() 最终也会展开,但这存在两层不一致的风险:

  • UI 预览 header 中看到的值与 library 实际发送的值之间可能不同步(如果用户在发送前修改了环境变量)

建议:在 buildAuthConfig() 中也执行环境变量解析,保持 UI 和 library 输入一致。

if (m_currentTaskId != 0)
{
NetworkRequestManager::globalInstance()->stopRequest(m_currentTaskId);
m_currentTaskId = 0;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — onAbortAllTask() 不重置 m_currentTaskId

stopAllRequest() 停止所有请求后,m_currentTaskId 仍保留旧的 taskId。如果用户先点 "Abort All" 再点 "Abort",会用已失效的 taskId 调用 stopRequest(),虽然这不会崩溃(stopRequest 对无效 id 是 no-op),但语义上不正确。

建议:在 onAbortAllTask() 末尾添加 m_currentTaskId = 0;

Comment thread source/environment.cpp
for (auto it = ctx.headers.begin(); it != ctx.headers.end(); ++it)
it.value() = substituteEnv(QString::fromUtf8(it.value()), vars).toUtf8();

ctx.body = substituteEnv(ctx.body, vars);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — body 字段的 env 替换可能产生歧义

applyEnvironment() 对 ctx.body 执行 {{var}} 替换。如果 body 是 JSON 且某个 value 恰好匹配 {{...}} 模式(例如 {"template": "{{name}}"} 是业务数据而非变量引用),会被意外替换。虽然这与 Postman 的行为一致,但用户可能不知道 body 也会被替换。

建议:在项目文档中明确说明 body/headers/query params 都会被环境变量替换。

: (hv.isObject() ? QJsonArray{ hv.toObject() } : QJsonArray{});
for (const auto &h : ha)
{
const QJsonObject ho = h.toObject();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — PostmanConverter::fromJson() 对缺失必填字段静默处理

itemJson["name"].toString() 当 "name" 字段不存在时返回空字符串,静默创建无名称的 collection item。"request" 对象缺失时 requestJson 也是空对象。

建议:对必填字段(name, request.url, request.method)检查 contains(),缺失时记录 warning 或跳过该 item。

folder.name = name;
parent->children.append(folder);
return folder.id;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MINOR — CollectionModel::save() 使用 QJsonDocument::Compact 格式

紧凑格式保存的 collection JSON 文件对用户不友好(无法手动编辑)。对于 collection 这种用户可能需要手动编辑的配置文件,推荐使用 QJsonDocument::Indented。

建议:将 QJsonDocument::Compact 改为 QJsonDocument::Indented。注意 PostmanConverter::toPostmanV21 已正确使用 Indented。

voidvec added 3 commits August 6, 2026 14:02
- 新增 precision-dark/light QSS 主题样式
- 新增 ThemeManager 实现主题切换功能
- 重构 NetworkDownloaderMainWindow UI 布局
- 重构 NetworkRequestTool UI 布局
- 更新 downloadermainwindow/networkrequesttool 代码适配新UI
- 新增 UI 相关单元测试 (TestDownloaderUi, TestTheme)
…ncy, and modal dialog elimination

🔴 MAJOR (M2):
- Add EnvironmentStore::variables(name) public API for per-env variable access
- Replace ~70 lines of dead code + manual JSON parsing in onManageEnvironments()
  with a single call to m_envStore.variables(name)

🟡 MINOR:
- m1: buildAuthConfig() now resolves {{var}} placeholders in all auth fields
  (Basic/Bearer/ApiKey/OAuth2) for consistency with applyAuthHeader()
- m2: onAbortAllTask() resets m_currentTaskId to 0
- m3: Add comment in environment.cpp about body {{}} substitution behavior
- m4: PostmanConverter::postmanToItem() skips items with empty name or
  missing method/url, logging qWarning instead of silently creating bad items

🐛 Modal dialog elimination:
- showBlockingWarning() replaced all QMessageBox::warning calls with
  non-modal status-bar messages + qWarning() — no more event-loop blocking
- Downloader closeEvent guarded with #ifdef QT_MTNETWORK_UNIT_TEST to skip
  QMessageBox::question in headless/CI runs

Test updates:
- testInvalidUrlSendHandling & testAuthEmptyCredentialFallback: verify
  m_lastWarningText instead of auto-dismissing modal dialogs
- Remove duplicate #include <QTimer>

Test results: 9/9 passed, 0 failures
showBlockingWarning() now uses #ifdef QT_MTNETWORK_UNIT_TEST:
- Test mode: status bar + qWarning (non-blocking)
- Normal GUI: QMessageBox::warning (modal, user cannot miss it)

closeEvent() in downloadermainwindow.cpp was already correct:
- Normal GUI: QMessageBox::question confirm dialog
- Test mode: skip dialog, accept directly
@voidvec
voidvec merged commit c62ea60 into master Aug 6, 2026
3 checks passed
@voidvec
voidvec deleted the feature/ui-test-and-fixes branch August 9, 2026 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant