Refine self-improvement review guidance

Narrow IsHandled, label-scope, UI-handler, checkpoint, and bulk-operation guidance to evidence-backed false-positive boundaries.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
wenjiefan 2026-08-18 11:17:14 +02:00
parent 841b4e7cab
commit 5f1cff2fb6
21 changed files with 99 additions and 155 deletions

View file

@ -1,43 +1,28 @@
codeunit 50401 "Test UI Handlers Bad"
codeunit 50401 "Test UI Handler Proof Bad"
{
Subtype = Test;
// Several wiring mistakes, each of which fails at runtime rather than as a
// clean assertion the reviewer can read:
// * A UI call with no listed handler -> "unhandled UI" abort (the Message
// below has no handler).
// * The mirror mistake, listing a handler the path never hits, instead
// fails with "handler function was not executed".
// * A handler that hardcodes its answer and asserts inline, with no
// enqueue/dequeue -> nothing proves the RIGHT dialog fired the RIGHT
// number of times, and a failed inline assert can be swallowed by the
// calling UI operation.
[Test]
[HandlerFunctions('ConfirmHandler')]
procedure PostDocumentConfirmsAndMessages()
[HandlerFunctions('CustomerCardHandler')]
procedure CustomerCardActionSucceeds()
var
Customer: Record Customer;
begin
// No Initialize(): a value leaked by an earlier test corrupts this one.
RunPostingThatConfirmsAndMessages();
// No AssertEmpty(): a missing or extra dialog goes unnoticed.
Customer.Get('10000');
ActionSucceeded := true;
Page.RunModal(Page::"Customer Card", Customer);
// This only proves a value assigned before the action stayed true.
Assert.IsTrue(ActionSucceeded, 'The customer card action failed.');
end;
local procedure RunPostingThatConfirmsAndMessages()
[ModalPageHandler]
procedure CustomerCardHandler(var CustomerCard: TestPage "Customer Card")
begin
// Raises a Confirm AND a Message, but only ConfirmHandler is listed:
// the Message has nothing to intercept it -> unhandled-UI runtime abort.
if Confirm('Post this document?', false) then
Message('Posting completed.');
end;
[ConfirmHandler]
procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean)
begin
// Hardcoded expectation and hardcoded reply. If the wrong dialog fires,
// this inline assert may never surface as the test's verdict.
Assert.AreEqual('Post this document?', Question, 'Wrong confirm.');
Reply := true;
end;
var
Assert: Codeunit "Library Assert";
ActionSucceeded: Boolean;
}

View file

@ -1,57 +1,28 @@
codeunit 50400 "Test UI Handlers Good"
codeunit 50400 "Test UI Handler Capture Good"
{
Subtype = Test;
[Test]
[HandlerFunctions('ConfirmHandler,PostMessageHandler')]
procedure PostDocumentConfirmsAndMessages()
[HandlerFunctions('CustomerCardHandler')]
procedure CustomerCardShowsSelectedCustomer()
var
Customer: Record Customer;
begin
Initialize();
Customer.Get('10000');
CapturedCustomerNo := '';
// [GIVEN] the test enqueues, in interaction order, what each handler
// will see and how it should answer: the Confirm's expected
// question plus the reply to return, then the expected Message.
LibraryVariableStorage.Enqueue('Post this document?'); // expected question (substring)
LibraryVariableStorage.Enqueue(true); // reply ConfirmHandler returns
LibraryVariableStorage.Enqueue('Posting completed.'); // expected message (substring)
Page.RunModal(Page::"Customer Card", Customer);
// [WHEN] the code under test raises the Confirm and then the Message
RunPostingThatConfirmsAndMessages();
// [THEN] every enqueued expectation was consumed exactly once
LibraryVariableStorage.AssertEmpty();
Assert.AreEqual(Customer."No.", CapturedCustomerNo, 'The customer card opened for the wrong customer.');
end;
local procedure Initialize()
[ModalPageHandler]
procedure CustomerCardHandler(var CustomerCard: TestPage "Customer Card")
begin
// Clear leftover values so a value leaked by an earlier test cannot
// cascade into this one.
LibraryVariableStorage.Clear();
end;
local procedure RunPostingThatConfirmsAndMessages()
begin
// Stands in for the production routine that confirms, then messages.
if Confirm('Post this document?', false) then
Message('Posting completed.');
end;
[ConfirmHandler]
procedure ConfirmHandler(Question: Text[1024]; var Reply: Boolean)
begin
// Verify the RIGHT dialog fired (substring match), then return the
// reply the test enqueued for it.
Assert.ExpectedConfirm(LibraryVariableStorage.DequeueText(), Question);
Reply := LibraryVariableStorage.DequeueBoolean();
end;
[MessageHandler]
procedure PostMessageHandler(Message: Text[1024])
begin
Assert.ExpectedMessage(LibraryVariableStorage.DequeueText(), Message);
CapturedCustomerNo := CustomerCard."No.".Value();
end;
var
Assert: Codeunit "Library Assert";
LibraryVariableStorage: Codeunit "Library - Variable Storage";
CapturedCustomerNo: Code[20];
}

View file

@ -1,28 +1,28 @@
---
bc-version: [all]
domain: testing
keywords: [handler, handlerfunctions, confirm, message, strmenu, variable-storage, enqueue, unhandled-ui]
keywords: [handler, handlerfunctions, confirm, message, strmenu, variable-storage, enqueue, capture, runmodal, unhandled-ui]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Wire and verify UI handlers with enqueue-driven expectations
# Wire UI handlers and verify meaningful outcomes
## Description
A test runs headless: there is no interactive user to answer a dialog. Every UI call the executed path raises — `Confirm`, `Message`, error dialogs, `Page.Run`/`RunModal`, `Report.Run`/`RunModal`, request pages, `StrMenu`, `Notification.Send` — must be intercepted by a handler carrying the matching attribute (`[ConfirmHandler]`, `[MessageHandler]`, `[StrMenuHandler]`, `[ModalPageHandler]`, …) and named in the method's `[HandlerFunctions(...)]`. The list is a two-sided contract: raise a UI call with no listed handler and the platform aborts with an *unhandled UI* error; list a handler the path never hits and it fails with *"handler function was not executed"*. Both are runtime failures — the test never reaches its verdict, so a reviewer sees an infrastructure error instead of a result on the behavior under test.
A test runs headless, so every UI call on the executed path must be intercepted by a matching handler named in `[HandlerFunctions(...)]`. The list is a two-sided contract: an unhandled UI call aborts the test, while Microsoft documents that [every listed handler must execute at least once](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/attributes/devenv-handlerfunctions-attribute#remarks) or the test fails.
Getting the handler *present* is only half the job; the handler must also verify the *right* dialog fired the *right* number of times. Do that by driving handlers from the test, not by hardcoding answers inside them.
Beyond that wiring guarantee, the test must verify the behavior it cares about. The appropriate pattern depends on the contract: a handler can capture concrete page state or a result and the test can assert that semantic postcondition after `RunModal`; assertions inside a handler are also supported. Queue/enqueue/dequeue and `LibraryVariableStorage.AssertEmpty` are useful when interaction order, count, text, replies, or a scripted sequence is itself part of the contract, but they are not mandatory for every handler.
## Best Practice
Make the test own the expectations and the handlers consume them. Before acting, the test `Enqueue`s — in interaction order — the expected text (a stable substring) and any reply each handler must return. The handler `Dequeue`s the expected text, verifies it with the purpose-built asserts (`Assert.ExpectedMessage`, `Assert.ExpectedConfirm`, `Assert.ExpectedStrMenu` — which match on a fragment, not the full localized caption), then `Dequeue`s and returns its reply. Finish the test body with `LibraryVariableStorage.AssertEmpty` to prove every enqueued interaction fired exactly once, and start each test with an `Initialize` that calls `LibraryVariableStorage.Clear` so a value leaked by an earlier test cannot cascade. List in `[HandlerFunctions]` precisely the handlers the scenario triggers — no superset "just in case", no subset that happens to work today.
List precisely the handlers the scenario triggers and make each handler contribute meaningful evidence. For a single modal page, reset a capture variable before the action, capture a concrete value from the page in the handler, and assert the expected value after `RunModal`. For ordered or repeated interactions, let the test enqueue expectations, let handlers dequeue and verify them, clear storage during initialization, and finish with `AssertEmpty`.
See sample: `ui-handlers-in-tests.good.al`.
## Anti Pattern
Omitting a handler for a UI call the path raises (unhandled-UI abort), padding the list with a handler the path never reaches ("handler function was not executed"), or writing handlers that hardcode their answer and assert inline with no enqueue/dequeue. The last is the subtle one: nothing proves the correct dialog fired the expected number of times, and an inline assertion that fails inside a handler can be swallowed by the calling UI operation, leaving the suite green while the behavior is broken. Skipping `Initialize`/`AssertEmpty` hides both a leaked queue and a missing or extra dialog.
Omitting a handler for a UI call, listing a handler the path never reaches, or claiming action success from a Boolean set before the action runs. A handler that only closes a page can also leave the test without a semantic assertion. Do not flag the absence of queue storage by itself; require it only when the test needs to prove interaction order, count, text, replies, or a scripted sequence.
See sample: `ui-handlers-in-tests.bad.al`.