Files
lez-programs/apps/amm/tests/qml/tst_NewPositionForm.qml
T
Ricardo Guilherme Schmidt 8c3e6fccfe feat(wallet): humanize shared wallet experience
Consolidate wallet account decoding and portfolio handling around the shared Rust IDL decoder. Simplify AMM wallet integration, remove obsolete caches and network plumbing, and cover account selection and live flows.
2026-08-21 17:38:45 -03:00

562 lines
18 KiB
QML

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
tokens: testCase.readyTokens()
walletReady: true
}
}
Component {
id: clipboardSinkComponent
TextEdit {}
}
SignalSpy {
id: quoteRequestedSpy
signalName: "quoteRequested"
}
function readyTokens() {
return [
{
"definitionId": tokenLow,
"name": "Low",
"totalSupply": "1000000",
"balance": "1000"
},
{
"definitionId": tokenHigh,
"name": "High",
"totalSupply": "1000000000000",
"balance": "5000000000"
}
]
}
function rawAmountTokens() {
return [
{
"definitionId": tokenLow,
"name": "Sir Mints-a-Lot",
"totalSupply": "1000000000000",
"balance": "1000000000"
},
{
"definitionId": tokenHigh,
"name": "Aurora",
"totalSupply": "1000000000000",
"balance": "1000000000"
}
]
}
function flowState(quote, poolExists) {
var state = {
"quote": quote || ({}),
"quoteLoading": false,
"quoteStale": false,
"submitting": false
}
if (poolExists !== undefined)
state.poolExists = poolExists
else if (quote && quote.poolStatus === "active_pool")
state.poolExists = true
else if (quote && quote.poolStatus === "missing_pool")
state.poolExists = false
return state
}
function createEmptyForm(tokens) {
var form = createTemporaryObject(formComponent, testCase, {
"flowState": flowState(({})),
"tokens": tokens || readyTokens()
})
verify(form)
wait(0)
return form
}
function createForm(tokens) {
var form = createEmptyForm(tokens)
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",
"minimumAmountA": "2",
"minimumAmountB": "3",
"actualAmountA": "2",
"actualAmountB": "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.amountA, "4")
compare(built.request.amountB, "6")
}
function test_missingPoolAcceptsLargeDirectAmountsFromEitherSide() {
var form = createForm(rawAmountTokens())
form.priceAmountA = "15"
form.priceAmountB = "10"
form.flowState = flowState({
"status": "ok",
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountA": "26",
"minimumAmountB": "39",
"actualAmountA": "26",
"actualAmountB": "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.amountA, "100")
compare(built.request.amountB, "150")
compare(built.request.price, "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.amountA, "200")
compare(built.request.amountB, "300")
}
function test_missingPoolRoundsPairedRawAmounts() {
var form = createForm(rawAmountTokens())
form.priceAmountA = "15"
form.priceAmountB = "10"
form.flowState = flowState({
"status": "ok",
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "missing_pool",
"minimumAmountA": "1",
"minimumAmountB": "1",
"actualAmountA": "1",
"actualAmountB": "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.amountA, cases[i].rawA)
compare(built.request.amountB, 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",
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "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)
// missing_pool now renders the holding-selector footer, so the input card grows by the
// footer's own height plus its extra bottom spacing (the surface adds footerHeight + 30
// when a footer is active vs + 24 without one, i.e. footerHeight + 6 over the pre-footer
// bound). Keep the compactness guard, but make it footer-aware.
verify(amountAInput.footerActive) // the holding-selector footer is present in missing_pool
verify(amountBInput.footerActive)
verify(amountAInput.height <= 114 + amountAInput.footerHeight + 6)
verify(amountBInput.height <= 114 + amountBInput.footerHeight + 6)
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",
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "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",
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "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",
"minimumAmountA": "2000000",
"minimumAmountB": "3",
"actualAmountA": "2000000",
"actualAmountB": "3"
},
"contextLoading": false,
"quoteLoading": false,
"quoteStale": true,
"submitting": false
}
wait(0)
compare(form.amountA, "2")
}
function test_staleQuoteErrorsDoNotMarkCurrentDraft() {
var quote = {
"status": "ok",
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"reserveA": "2",
"reserveB": "10",
"maxAmountA": "4",
"maxAmountB": "20",
"errors": [{
"code": "amount_exceeds_balance",
"blockingFields": ["maxAmountA"]
}]
}
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(), "")
}
function test_activePoolEditUsesDisplayReserveRatio() {
var quote = {
"status": "ok",
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"reserveA": "2",
"reserveB": "10",
"maxAmountA": "4",
"maxAmountB": "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.maxAmountA, "1")
compare(built.request.maxAmountB, "5")
form.finishActiveAmount("B", "1.1234567")
compare(form.amountB, "1.1234567")
verify(!form.buildQuoteRequest().ok)
}
function test_activePoolEditRecoversAfterInvalidQuote() {
var form = createForm()
form.flowState = flowState(({}), true)
wait(0)
form.flowState = flowState({
"status": "ok",
"tokenAId": tokenHigh,
"tokenBId": tokenLow,
"poolStatus": "active_pool",
"poolFeeBps": 30,
"reserveA": "2",
"reserveB": "10",
"maxAmountA": "4",
"maxAmountB": "20"
})
wait(0)
form.amountA = "0"
form.amountB = "0"
form.flowState = flowState({
"status": "error",
"code": "value_must_be_positive",
"tokenAId": tokenHigh,
"tokenBId": tokenLow
}, true)
wait(0)
compare(form.poolFeeBps, 30)
form.finishActiveAmount("B", "1")
compare(form.amountA, "5")
var built = form.buildQuoteRequest()
verify(built.ok)
compare(built.request.maxAmountA, "1")
compare(built.request.maxAmountB, "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" }
}]
}, true)
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.maxAmountA, "5000000000")
compare(quoteRequestedSpy.signalArguments[0][1].request.maxAmountB, "1000")
}
function test_tokenResolutionFailureClearsPendingState() {
var form = createForm()
form.resolvingTokenId = tokenThird
form.resolvingTokenSide = "A"
form.failTokenResolution("config_unavailable")
wait(0)
compare(form.resolvingTokenId, "")
compare(form.resolvingTokenSide, "")
compare(form.tokenResolutionError, form.issueText("config_unavailable"))
}
function test_replacingSelectedTokensClearsPairDraft() {
var form = createForm()
form.amountA = "12"
form.amountB = "34"
form.minimumAmountA = "12"
form.minimumAmountB = "34"
form.tokens = [
{
"definitionId": tokenHigh,
"name": "High",
"totalSupply": "1000000000000",
"balance": "5000000000"
},
{
"definitionId": tokenThird,
"name": "Third",
"totalSupply": "1000000",
"balance": "100"
}
]
wait(0)
compare(form.selectedTokenAId, "")
compare(form.selectedTokenBId, tokenHigh)
compare(form.amountA, "")
compare(form.amountB, "")
compare(form.minimumAmountA, "")
compare(form.minimumAmountB, "")
}
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)
}
}