Repository navigation
feat: GUI enhancements, UI test suite, and defect fixes - #2
Conversation
…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. |
There was a problem hiding this comment.
🔴 MAJOR — onManageEnvironments() 中存在大量死代码和绕过 EnvironmentStore API 的手动 JSON 解析
问题:
loadVarsForEnvlambda (L2004-2013): 被定义但从未被调用。它的 for 循环体为空(; // not used),实际上是一个死 lambda。- L2018-2023:
envVars变量和空代码块{}— 声明后从未使用。 - L2025-2032: 创建
workStore并加载文件,但后续代码(L2072-2073)disconnect 后重新 connect,使workStore完全白费。 - L2050-2070: 直接打开
environments.json手动解析 JSON,绕过了EnvironmentStore的公开 API。如果EnvironmentStore的存储格式发生变化,这段代码会静默失效。
建议:
- 删除 L2004-2013 的
loadVarsForEnvlambda(死代码) - 删除 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() | |||
There was a problem hiding this comment.
🟡 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; |
There was a problem hiding this comment.
🟡 MINOR — onAbortAllTask() 不重置 m_currentTaskId
stopAllRequest() 停止所有请求后,m_currentTaskId 仍保留旧的 taskId。如果用户先点 "Abort All" 再点 "Abort",会用已失效的 taskId 调用 stopRequest(),虽然这不会崩溃(stopRequest 对无效 id 是 no-op),但语义上不正确。
建议:在 onAbortAllTask() 末尾添加 m_currentTaskId = 0;
| 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); |
There was a problem hiding this comment.
🟡 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(); |
There was a problem hiding this comment.
🟡 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; | ||
| } |
There was a problem hiding this comment.
🟡 MINOR — CollectionModel::save() 使用 QJsonDocument::Compact 格式
紧凑格式保存的 collection JSON 文件对用户不友好(无法手动编辑)。对于 collection 这种用户可能需要手动编辑的配置文件,推荐使用 QJsonDocument::Indented。
建议:将 QJsonDocument::Compact 改为 QJsonDocument::Indented。注意 PostmanConverter::toPostmanV21 已正确使用 Indented。
- 新增 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
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)
feat(http)feat(gui)M0M1M2M3M4M5fixDefect fixes
onAbortTask()now storestaskIdfromNetworkReplyand callsstopRequest()applyAuthHeader()resolves{{var}}placeholders in auth credentialsonSendRequest()warns viaQMessageBoxwhen auth type is selected but credentials are emptyTest results