From fae1e928c8da2d13f75b6bf66b0068760ff62f1e Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Tue, 24 Mar 2026 08:37:39 +0100 Subject: [PATCH 1/3] fix(trade): guard nil sortedResultTbl in dropdown tooltipFunc The nil guard lost during the vaisest/trader-improvements merge caused a crash when hovering the result dropdown after clearing search results. Co-Authored-By: Claude Opus 4.6 --- src/Classes/TradeQuery.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Classes/TradeQuery.lua b/src/Classes/TradeQuery.lua index 4965c4eec1..7f33b15705 100644 --- a/src/Classes/TradeQuery.lua +++ b/src/Classes/TradeQuery.lua @@ -1009,8 +1009,15 @@ function TradeQueryClass:PriceItemRowDisplay(row_idx, top_pane_alignment_ref, ro end end controls["resultDropdown"..row_idx].tooltipFunc = function(tooltip, dropdown_mode, dropdown_index, dropdown_display_string) - local pb_index = self.sortedResultTbl[row_idx][dropdown_index].index - local result = self.resultTbl[row_idx][pb_index] + local sortedRow = self.sortedResultTbl[row_idx] + if not sortedRow or not sortedRow[dropdown_index] then + return + end + local pb_index = sortedRow[dropdown_index].index + local result = self.resultTbl[row_idx] and self.resultTbl[row_idx][pb_index] + if not result then + return + end local item = new("Item", result.item_string) tooltip:Clear() self.itemsTab:AddItemTooltip(tooltip, item, slotTbl) From 48859b1d6b0aff795c7adf6eb89d4a9f8323c4f5 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Thu, 9 Apr 2026 18:06:37 +0200 Subject: [PATCH 2/3] test(trade): cover stale result dropdown tooltip guards --- spec/System/TestTradeQuery_spec.lua | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 spec/System/TestTradeQuery_spec.lua diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua new file mode 100644 index 0000000000..42857f3e68 --- /dev/null +++ b/spec/System/TestTradeQuery_spec.lua @@ -0,0 +1,57 @@ +describe("TradeQuery", function() + describe("result dropdown tooltipFunc", function() + -- Builds a TradeQuery with the strict minimum needed for + -- PriceItemRowDisplay to construct row 1 without exploding. Only the + -- two itemsTab subtables read by the slot lookup at the top of + -- PriceItemRowDisplay need to be created here; everything else either + -- lives behind a callback we never trigger, or is already initialized + -- by the TradeQuery constructor. + local function newTradeQuery(state) + local tq = new("TradeQuery", { itemsTab = {} }) + tq.itemsTab.activeItemSet = {} + tq.itemsTab.slots = {} + tq.slotTables[1] = { slotName = "Ring 1" } + if state.resultTbl then tq.resultTbl = state.resultTbl end + if state.sortedResultTbl then tq.sortedResultTbl = state.sortedResultTbl end + return tq + end + + -- Builds row 1 of the trader UI and returns the dropdown that owns the + -- tooltipFunc we want to exercise. + local function buildRow1Dropdown(tq) + tq:PriceItemRowDisplay(1, nil, 0, 20) + return tq.controls.resultDropdown1 + end + + it("returns early when sortedResultTbl[row_idx] is missing", function() + -- No sorted results at all -> first guard must short-circuit. + local tq = newTradeQuery({}) + local dropdown = buildRow1Dropdown(tq) + local tooltip = new("Tooltip") + + assert.has_no.errors(function() + dropdown.tooltipFunc(tooltip, "DROP", 1, nil) + end) + assert.are.equal(0, #tooltip.lines) + end) + + it("returns early when the backing result entry has been cleared", function() + -- The dropdown must be built against a valid result so that + -- PriceItemRowDisplay's construction loop succeeds; we wipe + -- resultTbl[1] only afterwards, to simulate a stale tooltip + -- callback firing after the results were invalidated. + local tq = newTradeQuery({ + resultTbl = { [1] = { [1] = { item_string = "Rarity: RARE\nBehemoth Hold\nGold Ring" } } }, + sortedResultTbl = { [1] = { { index = 1 } } }, + }) + local dropdown = buildRow1Dropdown(tq) + tq.resultTbl[1] = {} + local tooltip = new("Tooltip") + + assert.has_no.errors(function() + dropdown.tooltipFunc(tooltip, "DROP", 1, nil) + end) + assert.are.equal(0, #tooltip.lines) + end) + end) +end) From 159a4e3cd2919e876b8aaafa7713e742c32dbfd9 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Fri, 10 Apr 2026 09:03:37 +0200 Subject: [PATCH 3/3] style(spec): use tab indentation in TradeQuery test --- spec/System/TestTradeQuery_spec.lua | 100 ++++++++++++++-------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/spec/System/TestTradeQuery_spec.lua b/spec/System/TestTradeQuery_spec.lua index 42857f3e68..857d3cb20e 100644 --- a/spec/System/TestTradeQuery_spec.lua +++ b/spec/System/TestTradeQuery_spec.lua @@ -1,57 +1,57 @@ describe("TradeQuery", function() - describe("result dropdown tooltipFunc", function() - -- Builds a TradeQuery with the strict minimum needed for - -- PriceItemRowDisplay to construct row 1 without exploding. Only the - -- two itemsTab subtables read by the slot lookup at the top of - -- PriceItemRowDisplay need to be created here; everything else either - -- lives behind a callback we never trigger, or is already initialized - -- by the TradeQuery constructor. - local function newTradeQuery(state) - local tq = new("TradeQuery", { itemsTab = {} }) - tq.itemsTab.activeItemSet = {} - tq.itemsTab.slots = {} - tq.slotTables[1] = { slotName = "Ring 1" } - if state.resultTbl then tq.resultTbl = state.resultTbl end - if state.sortedResultTbl then tq.sortedResultTbl = state.sortedResultTbl end - return tq - end + describe("result dropdown tooltipFunc", function() + -- Builds a TradeQuery with the strict minimum needed for + -- PriceItemRowDisplay to construct row 1 without exploding. Only the + -- two itemsTab subtables read by the slot lookup at the top of + -- PriceItemRowDisplay need to be created here; everything else either + -- lives behind a callback we never trigger, or is already initialized + -- by the TradeQuery constructor. + local function newTradeQuery(state) + local tq = new("TradeQuery", { itemsTab = {} }) + tq.itemsTab.activeItemSet = {} + tq.itemsTab.slots = {} + tq.slotTables[1] = { slotName = "Ring 1" } + if state.resultTbl then tq.resultTbl = state.resultTbl end + if state.sortedResultTbl then tq.sortedResultTbl = state.sortedResultTbl end + return tq + end - -- Builds row 1 of the trader UI and returns the dropdown that owns the - -- tooltipFunc we want to exercise. - local function buildRow1Dropdown(tq) - tq:PriceItemRowDisplay(1, nil, 0, 20) - return tq.controls.resultDropdown1 - end + -- Builds row 1 of the trader UI and returns the dropdown that owns the + -- tooltipFunc we want to exercise. + local function buildRow1Dropdown(tq) + tq:PriceItemRowDisplay(1, nil, 0, 20) + return tq.controls.resultDropdown1 + end - it("returns early when sortedResultTbl[row_idx] is missing", function() - -- No sorted results at all -> first guard must short-circuit. - local tq = newTradeQuery({}) - local dropdown = buildRow1Dropdown(tq) - local tooltip = new("Tooltip") + it("returns early when sortedResultTbl[row_idx] is missing", function() + -- No sorted results at all -> first guard must short-circuit. + local tq = newTradeQuery({}) + local dropdown = buildRow1Dropdown(tq) + local tooltip = new("Tooltip") - assert.has_no.errors(function() - dropdown.tooltipFunc(tooltip, "DROP", 1, nil) - end) - assert.are.equal(0, #tooltip.lines) - end) + assert.has_no.errors(function() + dropdown.tooltipFunc(tooltip, "DROP", 1, nil) + end) + assert.are.equal(0, #tooltip.lines) + end) - it("returns early when the backing result entry has been cleared", function() - -- The dropdown must be built against a valid result so that - -- PriceItemRowDisplay's construction loop succeeds; we wipe - -- resultTbl[1] only afterwards, to simulate a stale tooltip - -- callback firing after the results were invalidated. - local tq = newTradeQuery({ - resultTbl = { [1] = { [1] = { item_string = "Rarity: RARE\nBehemoth Hold\nGold Ring" } } }, - sortedResultTbl = { [1] = { { index = 1 } } }, - }) - local dropdown = buildRow1Dropdown(tq) - tq.resultTbl[1] = {} - local tooltip = new("Tooltip") + it("returns early when the backing result entry has been cleared", function() + -- The dropdown must be built against a valid result so that + -- PriceItemRowDisplay's construction loop succeeds; we wipe + -- resultTbl[1] only afterwards, to simulate a stale tooltip + -- callback firing after the results were invalidated. + local tq = newTradeQuery({ + resultTbl = { [1] = { [1] = { item_string = "Rarity: RARE\nBehemoth Hold\nGold Ring" } } }, + sortedResultTbl = { [1] = { { index = 1 } } }, + }) + local dropdown = buildRow1Dropdown(tq) + tq.resultTbl[1] = {} + local tooltip = new("Tooltip") - assert.has_no.errors(function() - dropdown.tooltipFunc(tooltip, "DROP", 1, nil) - end) - assert.are.equal(0, #tooltip.lines) - end) - end) + assert.has_no.errors(function() + dropdown.tooltipFunc(tooltip, "DROP", 1, nil) + end) + assert.are.equal(0, #tooltip.lines) + end) + end) end)