pragma ComponentBehavior: Bound import QtQuick import QtTest import "../../qml/components/liquidity" as Liquidity TestCase { id: testCase name: "NewPositionForm" readonly property string tokenLow: "22222222222222222222222222222222" readonly property string tokenHigh: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" readonly property string tokenThird: "33333333333333333333333333333333" readonly property string submittedTransactionId: "1thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE" Component { id: formComponent Liquidity.NewPositionForm { visible: false width: 760 newPositionContext: testCase.readyContext() } } Component { id: clipboardSinkComponent TextEdit {} } SignalSpy { id: quoteRequestedSpy signalName: "quoteRequested" } function readyContext() { return { "status": "ready", "tokens": [ { "definitionId": tokenLow, "name": "Low", "totalSupplyRaw": "1000000", "balanceRaw": "1000", "selectable": true }, { "definitionId": tokenHigh, "name": "High", "totalSupplyRaw": "1000000000000", "balanceRaw": "5000000000", "selectable": true } ] } } function rawAmountsContext() { return { "status": "ready", "tokens": [ { "definitionId": tokenLow, "name": "Sir Mints-a-Lot", "totalSupplyRaw": "1000000000000", "balanceRaw": "1000000000", "selectable": true }, { "definitionId": tokenHigh, "name": "Aurora", "totalSupplyRaw": "1000000000000", "balanceRaw": "1000000000", "selectable": true } ] } } function flowState(quote) { return { "quote": quote || ({}), "contextLoading": false, "quoteLoading": false, "quoteStale": false, "submitting": false } } function createEmptyForm(context) { var form = createTemporaryObject(formComponent, testCase, { "flowState": flowState(({})), "newPositionContext": context || readyContext() }) verify(form) wait(0) return form } function createForm(context) { var form = createEmptyForm(context) form.selectToken("A", tokenLow) form.selectToken("B", tokenHigh) compare(form.selectedTokenAId, tokenLow) compare(form.selectedTokenBId, tokenHigh) return form } function test_walletTokensDoNotPrefillPosition() { var form = createEmptyForm() compare(form.selectedTokenAId, "") compare(form.selectedTokenBId, "") compare(form.amountA, "") compare(form.amountB, "") } function test_displayOrderMapsToCanonicalRequestAfterSwap() { var form = createForm() var built = form.buildQuoteRequest() verify(built.ok) compare(built.request.tokenAId, tokenHigh) compare(built.request.tokenBId, tokenLow) form.swapTokens() compare(form.selectedTokenAId, tokenHigh) compare(form.selectedTokenBId, tokenLow) built = form.buildQuoteRequest() verify(built.ok) compare(built.request.tokenAId, tokenHigh) compare(built.request.tokenBId, tokenLow) } function test_tokenAmountsUseRawUnits() { var form = createForm() compare(form.decimalsA, 0) compare(form.decimalsB, 0) compare(form.balanceText(form.tokenA, form.decimalsA), "1000") compare(form.balanceText(form.tokenB, form.decimalsB), "5000000000") } function test_missingPoolKeepsMinimumRatioWhileEditingDeposit() { var quote = { "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "2", "minimumAmountBRaw": "3", "actualAmountARaw": "2", "actualAmountBRaw": "3" } var form = createForm() form.priceAmountA = "3" form.priceAmountB = "2" form.flowState = flowState(quote) wait(0) compare(form.amountA, "3") compare(form.amountB, "2") form.editMissingAmount("A", "6") compare(form.amountA, "6") compare(form.amountB, "2") form.finishMissingAmount("A", "6") compare(form.amountB, "4") var built = form.buildQuoteRequest() verify(built.ok) compare(built.request.amountARaw, "4") compare(built.request.amountBRaw, "6") } function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() { var form = createForm(rawAmountsContext()) form.priceAmountA = "15" form.priceAmountB = "10" form.flowState = flowState({ "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "26", "minimumAmountBRaw": "39", "actualAmountARaw": "26", "actualAmountBRaw": "39" }) wait(0) form.finishMissingAmount("A", "150") compare(form.amountA, "150") compare(form.amountB, "100") var built = form.buildQuoteRequest() verify(built.ok) compare(built.request.amountARaw, "100") compare(built.request.amountBRaw, "150") compare(built.request.initialPriceRealRaw, "27670116110564327424") verify(!built.request.hasOwnProperty("depositScaleBps")) form.finishMissingAmount("B", "200") compare(form.amountA, "300") compare(form.amountB, "200") built = form.buildQuoteRequest() verify(built.ok) compare(built.request.amountARaw, "200") compare(built.request.amountBRaw, "300") } function test_missingPoolRoundsPairedRawAmounts() { var form = createForm(rawAmountsContext()) form.priceAmountA = "15" form.priceAmountB = "10" form.flowState = flowState({ "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "1", "minimumAmountBRaw": "1", "actualAmountARaw": "1", "actualAmountBRaw": "1" }) wait(0) var cases = [ { "input": "1", "paired": "1", "rawA": "1", "rawB": "1" }, { "input": "2", "paired": "1", "rawA": "1", "rawB": "2" }, { "input": "3", "paired": "2", "rawA": "2", "rawB": "3" }, { "input": "4", "paired": "3", "rawA": "3", "rawB": "4" } ] for (var i = 0; i < cases.length; ++i) { form.finishMissingAmount("A", cases[i].input) compare(form.amountB, cases[i].paired) var built = form.buildQuoteRequest() verify(built.ok) compare(built.request.amountARaw, cases[i].rawA) compare(built.request.amountBRaw, cases[i].rawB) } form.finishMissingAmount("A", "1.1234567") compare(form.amountA, "1.1234567") verify(!form.buildQuoteRequest().ok) compare(form.fieldError("amountA"), form.issueText("invalid_amount_precision")) } function test_missingPoolUsesTradeStyleInputsWithInlinePrice() { var form = createForm() form.flowState = flowState({ "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "2000000", "minimumAmountBRaw": "3", "actualAmountARaw": "2000000", "actualAmountBRaw": "3" }) wait(0) var amountAInput = findChild(form, "tokenAAmountInput") var amountBInput = findChild(form, "tokenBAmountInput") verify(amountAInput) verify(amountBInput) compare(amountAInput.selectedTokenId, tokenLow) compare(amountBInput.selectedTokenId, tokenHigh) verify(amountAInput.height <= 114) verify(amountBInput.height <= 114) verify(findChild(form, "priceAmountAField")) verify(findChild(form, "priceAmountBField")) form.width = 328 wait(0) compare(amountAInput.adjustmentWidth, 100) compare(amountBInput.adjustmentWidth, 100) } function test_errorMessageStaysAbovePairAndMarksCausingControl() { var form = createForm() form.flowState = flowState({ "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "2000000", "minimumAmountBRaw": "3", "actualAmountARaw": "2000000", "actualAmountBRaw": "3" }) wait(0) var amountAInput = findChild(form, "tokenAAmountInput") var amountBInput = findChild(form, "tokenBAmountInput") form.localErrors = [form.localIssue("amount_exceeds_balance", ["amountB"])] wait(0) compare(amountAInput.errorText, form.issueText("amount_exceeds_balance")) compare(amountAInput.invalid, false) compare(amountBInput.invalid, true) form.resolveToken("B", "invalid") wait(0) compare(amountAInput.errorText, form.issueText("invalid_token_id")) compare(amountAInput.tokenInvalid, false) compare(amountBInput.tokenInvalid, true) } function test_staleMissingPoolQuoteDoesNotReplaceDraft() { var quote = { "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "2000000", "minimumAmountBRaw": "3", "actualAmountARaw": "2000000", "actualAmountBRaw": "3" } var form = createForm() form.flowState = flowState(quote) wait(0) compare(form.amountA, "3") form.editMissingAmount("A", "2") form.flowState = { "quote": { "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "missing_pool", "minimumAmountARaw": "2000000", "minimumAmountBRaw": "3", "actualAmountARaw": "2000000", "actualAmountBRaw": "3" }, "contextLoading": false, "quoteLoading": false, "quoteStale": true, "submitting": false } wait(0) compare(form.amountA, "2") } function test_poolActivationClearsCreationDraftWithoutPublishingProbeAmounts() { var form = createForm() form.confirmedPoolStatus = "missing_pool" form.amountA = "3" form.amountB = "2" form.minimumAmountARaw = "3" form.minimumAmountBRaw = "2000000" verify(form.acceptPoolActivation({ "schema": "new-position.v1", "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "active_pool", "reserveARaw": "3000000", "reserveBRaw": "2" })) verify(form.activePool) compare(form.activePoolQuote.poolStatus, "active_pool") compare(form.amountA, "") compare(form.amountB, "") compare(form.minimumAmountARaw, "") compare(form.minimumAmountBRaw, "") quoteRequestedSpy.target = form quoteRequestedSpy.clear() form.requestQuote(true) compare(quoteRequestedSpy.count, 0) compare(form.localErrors.length, 0) } function test_staleQuoteErrorsDoNotMarkCurrentDraft() { var quote = { "schema": "new-position.v1", "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "active_pool", "reserveARaw": "2", "reserveBRaw": "10", "maxAmountARaw": "4", "maxAmountBRaw": "20", "errors": [{ "code": "amount_exceeds_balance", "blockingFields": ["maxAmountARaw"] }] } var form = createForm() form.flowState = flowState(quote) wait(0) compare(form.fieldError("amountB"), form.issueText("amount_exceeds_balance")) var staleState = flowState(quote) staleState.quoteStale = true form.flowState = staleState wait(0) compare(form.fieldError("amountB"), "") compare(form.formErrorText(), "") compare(form.accountPreview().length, 0) } function test_activePoolEditUsesDisplayReserveRatio() { var quote = { "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "active_pool", "reserveARaw": "2", "reserveBRaw": "10", "maxAmountARaw": "4", "maxAmountBRaw": "20" } var form = createForm() form.flowState = flowState(quote) wait(0) compare(form.amountA, "") compare(form.amountB, "") quoteRequestedSpy.target = form quoteRequestedSpy.clear() form.editActiveAmount("A", "5") compare(form.amountA, "5") compare(form.amountB, "") compare(quoteRequestedSpy.count, 0) form.finishActiveAmount("A", "5") compare(form.amountB, "1") compare(quoteRequestedSpy.count, 1) var built = form.buildQuoteRequest() verify(built.ok) compare(built.request.maxAmountARaw, "1") compare(built.request.maxAmountBRaw, "5") form.finishActiveAmount("B", "1.1234567") compare(form.amountB, "1.1234567") verify(!form.buildQuoteRequest().ok) } function test_activePoolEditRecoversAfterInvalidQuote() { var form = createForm() form.flowState = flowState({ "status": "ok", "tokenAId": tokenHigh, "tokenBId": tokenLow, "poolStatus": "active_pool", "poolFeeBps": 30, "reserveARaw": "2", "reserveBRaw": "10", "maxAmountARaw": "4", "maxAmountBRaw": "20" }) wait(0) form.amountA = "0" form.amountB = "0" form.flowState = flowState({ "status": "error", "code": "value_must_be_positive", "tokenAId": tokenHigh, "tokenBId": tokenLow }) wait(0) compare(form.poolFeeBps, 30) form.finishActiveAmount("B", "1") compare(form.amountA, "5") var built = form.buildQuoteRequest() verify(built.ok) compare(built.request.maxAmountARaw, "1") compare(built.request.maxAmountBRaw, "5") } function test_quoteStateChangeDoesNotRequestAnotherQuote() { var form = createForm() quoteRequestedSpy.target = form quoteRequestedSpy.clear() form.flowState = { "quote": ({}), "contextLoading": false, "quoteLoading": true, "quoteStale": true, "submitting": false } wait(0) compare(quoteRequestedSpy.count, 0) } function test_existingPoolFeeCorrectionKeepsQuoteRequestValid() { var form = createForm() quoteRequestedSpy.target = form quoteRequestedSpy.clear() form.flowState = flowState({ "status": "error", "code": "fee_tier_mismatch", "tokenAId": tokenHigh, "tokenBId": tokenLow, "errors": [{ "code": "fee_tier_mismatch", "details": { "poolFeeBps": "5" } }] }) wait(0) compare(form.selectedFeeBps, 5) compare(form.amountA, "") compare(form.amountB, "") compare(quoteRequestedSpy.count, 1) verify(quoteRequestedSpy.signalArguments[0][1].ok) compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountARaw, "5000000000") compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountBRaw, "1000") } function test_contextFailureFinishesTokenResolution() { var form = createForm() form.resolvingTokenId = tokenThird form.resolvingTokenSide = "A" form.newPositionContext = { "status": "error", "code": "config_unavailable", "tokens": [] } form.finishTokenResolution(true) wait(0) compare(form.resolvingTokenId, "") compare(form.resolvingTokenSide, "") compare(form.tokenResolutionError, form.issueText("config_unavailable")) } function test_staleContextDoesNotFinishNewerTokenResolution() { var form = createForm() form.resolvingTokenId = tokenThird form.resolvingTokenSide = "B" form.newPositionContext = { "status": "ready", "tokens": [{ "definitionId": tokenLow, "name": "Earlier token", "selectable": true }] } wait(0) compare(form.resolvingTokenId, tokenThird) compare(form.resolvingTokenSide, "B") compare(form.tokenResolutionError, "") } function test_replacingSelectedTokensClearsPairDraft() { var form = createForm() form.amountA = "12" form.amountB = "34" form.minimumAmountARaw = "12" form.minimumAmountBRaw = "34" form.confirmedPoolStatus = "active_pool" form.newPositionContext = { "status": "ready", "tokens": [ { "definitionId": tokenHigh, "name": "High", "totalSupplyRaw": "1000000000000", "balanceRaw": "5000000000", "selectable": true }, { "definitionId": tokenThird, "name": "Third", "totalSupplyRaw": "1000000", "balanceRaw": "100", "selectable": true } ] } wait(0) compare(form.selectedTokenAId, "") compare(form.selectedTokenBId, tokenHigh) compare(form.amountA, "") compare(form.amountB, "") compare(form.minimumAmountARaw, "") compare(form.minimumAmountBRaw, "") compare(form.confirmedPoolStatus, "") } function test_networkFailurePreservesPairDraft() { var form = createForm() form.amountA = "12" form.amountB = "34" form.confirmedPoolStatus = "active_pool" form.newPositionContext = { "status": "network_mismatch", "tokens": [] } wait(0) compare(form.selectedTokenAId, tokenLow) compare(form.selectedTokenBId, tokenHigh) compare(form.amountA, "12") compare(form.amountB, "34") compare(form.confirmedPoolStatus, "active_pool") verify(form.contextBlocksForm()) } function test_submittedBase58TransactionIdIsCopied() { var state = flowState(({})) state.transactionId = submittedTransactionId var form = createTemporaryObject(formComponent, testCase, { "flowState": state }) var sink = createTemporaryObject(clipboardSinkComponent, testCase) verify(form) verify(sink) wait(0) compare(form.transactionId, submittedTransactionId) var copyButton = findChild(form, "copySubmittedTransactionButton") verify(copyButton) copyButton.clicked() verify(copyButton.copied) sink.paste() tryCompare(sink, "text", submittedTransactionId) } }