Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
paths:
- "src/ALCops.TestAutomationCop/**/InvokeActionOnPartTestPage*"
- "src/ALCops.TestAutomationCop.Test/Rules/InvokeActionOnPartTestPage/**"
---

# TA0002: InvokeActionOnPartTestPage

## Purpose

Reports `Invoke()`, `Enabled()` and `Visible()` calls on an action of a `TestPage` variable whose target page has `PageType = ListPart` or `CardPart`. A part page opened directly through its own `TestPage` renders no actions at runtime, so the test fails with "The action with ID = xxx is not found on the page." The working pattern is to invoke the action through the part control of the hosting page (`HostPage.SubPagePart.MyAction.Invoke()`).

Registers `RegisterOperationAction` on `InvocationExpression`; main type `InvokeActionOnPartTestPage`.

**References:** https://github.com/ALCops/Analyzers/discussions/455

## Design decisions

| Decision | Rationale |
|---|---|
| Register on `InvocationExpression` rather than a hypothetical `TestActionAccess` operation kind | `IOperation` has no `Parent`, so the action access alone cannot reach the containing `Invoke`/`Enabled`/`Visible` call; the invocation has both the target method identity and the `ITestActionAccess` instance. Covers both `Invoke()` and `Invoke` (without parentheses) because `IInvocationExpression` represents both spellings. |
| All three `TestAction` methods (`Invoke`, `Enabled`, `Visible`) | They are the only members of the built-in `TestAction` class; all three fail identically at runtime on a directly opened part page. |
| Type-based reach (every body, no `Subtype = Test` gate) | `TestPage` variables can appear in helper codeunits that are not `Subtype = Test`; gating on subtype would miss those. |
| Only `ListPart` and `CardPart` page types | `HeadlinePart` is not confirmed to fail the same way; silence is safer. A page without an explicit `PageType` property defaults to `Card`, which is not a part type and is therefore silent. |
| Identify the call by `invocation.Instance is ITestActionAccess`, not by method name or built-in class | `Enabled` and `Visible` also exist on `TestField` and `Invoke` on the built-in `OK()` path, so names alone cannot identify a page action; the access operation is the binder's statement that the receiver is a test-page action, and every such receiver has type `TestAction`, whose only members are `Invoke`, `Enabled` and `Visible`, so an extra `MethodKind` or class-name check adds nothing. A future fourth `TestAction` method is covered automatically. |
| No `ctx.IsObsolete()` gate; obsolete test code and obsolete part pages are both reported | The test runner still executes an `[Obsolete]` test method or an `ObsoleteState = Pending` test codeunit, and the action lookup still fails, so the usual "obsolete code is noise" rule would create a false negative on a test that breaks the run. |
| Severity `Warning`, category `Usage` | The call always fails at runtime, but Warning (not Error) matches the convention for rules that do not prevent compilation. |
| No CodeFix | The fix requires knowing the hosting page and its part control name; no mechanical rewrite exists. |
| No `PageTypeKind` sentinel hardening | A missing `PageTypeKind` member would only cause silence (the equality check fails); the sentinel pattern is unnecessary here. |

## Deliberate non-reports

- Actions reached through a part control (`MainPage.SubPagePart.MyAction.Invoke()`): the `ITestActionAccess.Instance` is typed `TestPart` (`NavTypeKind.TestPart`), not `TestPage`, so the `NavTypeKind` check excludes it.
- Field access, `OpenView`/`OpenEdit`/`OpenNew` and all other `TestPage` built-ins on a part: these are not `TestAction` members and pass through the `ContainingSymbol` class-name check.
- Built-in actions `OK`/`Cancel`/`Yes`/`No`/`View`/`Edit`: `SubPage.OK().Invoke()` has an invocation (the `OK()` call) as the `Instance` of the outer `Invoke`, not an `ITestActionAccess`.
- `HeadlinePart` and every other `PageType` not confirmed to fail.
- `TestRequestPage`: its receiver type is `RequestPageTypeSymbol`, which is `IPageBaseTypeSymbol` but not `IPageTypeSymbol`; the `OriginalDefinition is not IPageTypeSymbol` cast bails out.

## Test notes

- `ListPartActionFromPageExtension` is skipped below runtime 13.0: AL 12 rejects a pageextension whose target page is declared in the same module (AL0334).

## SDK facts

- `TestPage "X"` type is the internal `TestPageTypeSymbol`: `NavTypeKind.TestPage`, `OriginalDefinition` is the `PageTypeSymbol` (public `IPageTypeSymbol`, `PageType` on `IPageBaseTypeSymbol`). No public `ITestPageTypeSymbol`.
- `X.SomeAction` binds to `BoundTestActionAccess : ITestActionAccess` (`Instance`, `ActionSymbol : IActionSymbol`).
- `MainPage.Part` binds to `BoundTestPartAccess : ITestPartAccess`; its `Type` is `TestPartSymbol` (`NavTypeKind.TestPart`). For `MainPage.Part.Action.Invoke()` the `ITestActionAccess.Instance` is typed `TestPart`; for `SubPage.Action.Invoke()` it is typed `TestPage`.
- `.Invoke()`/`.Enabled()`/`.Visible()` are the only three members of the built-in class `TestAction` (`Symbols/TestActionClassTypeSymbol.cs:7-12`); the call is an `IInvocationExpression` with `TargetMethod.MethodKind == BuiltInMethod`, `ContainingSymbol` the `IClassTypeSymbol` named `TestAction`.
- `OK`/`Cancel`/`Yes`/`No`/`View`/`Edit` are `TestPage` built-ins returning `TestActionType`: `SubPage.OK().Invoke()` has an invocation as `Instance`, never an `ITestActionAccess`.
- Pageextension actions are folded into the test page members; classify by receiver type, not by `IActionSymbol.ContainingSymbol`.
- `TestRequestPage` is `RequestPageTypeSymbol`, which is `IPageBaseTypeSymbol` but not `IPageTypeSymbol`, so the cast bails out silently.
- Bare `SubPage.SomeAction;` does not compile, so every `TestAction` access is one of the three calls.
6 changes: 6 additions & 0 deletions src/ALCops.Common/Reflection/EnumProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ public static class NavTypeKind
new(() => ParseEnum<NavCodeAnalysis.NavTypeKind>(nameof(NavCodeAnalysis.NavTypeKind.TableExtension)));
private static readonly Lazy<NavCodeAnalysis.NavTypeKind> _tableFilter =
new(() => ParseEnum<NavCodeAnalysis.NavTypeKind>(nameof(NavCodeAnalysis.NavTypeKind.TableFilter)));
private static readonly Lazy<NavCodeAnalysis.NavTypeKind> _testPage =
new(() => ParseEnum<NavCodeAnalysis.NavTypeKind>(nameof(NavCodeAnalysis.NavTypeKind.TestPage)));
private static readonly Lazy<NavCodeAnalysis.NavTypeKind> _text =
new(() => ParseEnum<NavCodeAnalysis.NavTypeKind>(nameof(NavCodeAnalysis.NavTypeKind.Text)));
private static readonly Lazy<NavCodeAnalysis.NavTypeKind> _variant =
Expand Down Expand Up @@ -480,6 +482,7 @@ public static class NavTypeKind
public static NavCodeAnalysis.NavTypeKind String => _string.Value;
public static NavCodeAnalysis.NavTypeKind TableExtension => _tableExtension.Value;
public static NavCodeAnalysis.NavTypeKind TableFilter => _tableFilter.Value;
public static NavCodeAnalysis.NavTypeKind TestPage => _testPage.Value;
public static NavCodeAnalysis.NavTypeKind Text => _text.Value;
public static NavCodeAnalysis.NavTypeKind Variant => _variant.Value;
public static NavCodeAnalysis.NavTypeKind XmlPort => _xmlPort.Value;
Expand Down Expand Up @@ -574,6 +577,8 @@ public static class PageTypeKind
new(() => ParseEnum<NavCodeAnalysis.PageTypeKind>(nameof(NavCodeAnalysis.PageTypeKind.API)));
private static readonly Lazy<NavCodeAnalysis.PageTypeKind> _card =
new(() => ParseEnum<NavCodeAnalysis.PageTypeKind>(nameof(NavCodeAnalysis.PageTypeKind.Card)));
private static readonly Lazy<NavCodeAnalysis.PageTypeKind> _cardPart =
new(() => ParseEnum<NavCodeAnalysis.PageTypeKind>(nameof(NavCodeAnalysis.PageTypeKind.CardPart)));
private static readonly Lazy<NavCodeAnalysis.PageTypeKind> _document =
new(() => ParseEnum<NavCodeAnalysis.PageTypeKind>(nameof(NavCodeAnalysis.PageTypeKind.Document)));
private static readonly Lazy<NavCodeAnalysis.PageTypeKind> _headlinePart =
Expand All @@ -589,6 +594,7 @@ public static class PageTypeKind

public static NavCodeAnalysis.PageTypeKind API => _api.Value;
public static NavCodeAnalysis.PageTypeKind Card => _card.Value;
public static NavCodeAnalysis.PageTypeKind CardPart => _cardPart.Value;
public static NavCodeAnalysis.PageTypeKind Document => _document.Value;
public static NavCodeAnalysis.PageTypeKind HeadlinePart => _headlinePart.Value;
public static NavCodeAnalysis.PageTypeKind List => _list.Value;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyCardPart
{
PageType = CardPart;
SourceTable = MyTable;

actions
{
area(processing)
{
action(MyAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

[Test]
procedure MyTest()
var
SubPage: TestPage MyCardPart;
begin
SubPage.OpenView();
[|SubPage.MyAction.Invoke()|];
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyListPart
{
PageType = ListPart;
SourceTable = MyTable;
}

pageextension 50100 MyListPartExt extends MyListPart
{
actions
{
addlast(processing)
{
action(ExtAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

[Test]
procedure MyTest()
var
SubPage: TestPage MyListPart;
begin
SubPage.OpenView();
[|SubPage.ExtAction.Invoke()|];
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyListPart
{
PageType = ListPart;
SourceTable = MyTable;

actions
{
area(processing)
{
action(MyAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

[Test]
procedure MyTest()
var
SubPage: TestPage MyListPart;
IsEnabled: Boolean;
IsVisible: Boolean;
begin
SubPage.OpenView();
IsEnabled := [|SubPage.MyAction.Enabled()|];
IsVisible := [|SubPage.MyAction.Visible()|];
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyListPart
{
PageType = ListPart;
SourceTable = MyTable;

actions
{
area(processing)
{
action(MyAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

var
SubPage: TestPage MyListPart;

[Test]
procedure MyTest()
begin
SubPage.OpenView();
[|SubPage.MyAction.Invoke()|];
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyListPart
{
PageType = ListPart;
SourceTable = MyTable;

actions
{
area(processing)
{
action(MyAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

[Test]
procedure MyTest()
var
SubPage: TestPage MyListPart;
begin
SubPage.OpenView();
[|SubPage.MyAction.Invoke|];
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyListPart
{
PageType = ListPart;
SourceTable = MyTable;

actions
{
area(processing)
{
action(MyAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

[Test]
procedure MyTest()
var
SubPage: TestPage MyListPart;
begin
SubPage.OpenView();
[|SubPage.MyAction.Invoke()|];
end;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
table 50100 MyTable
{
fields
{
field(1; MyField; Integer) { }
}
}

page 50100 MyListPart
{
PageType = ListPart;
SourceTable = MyTable;

actions
{
area(processing)
{
action(MyAction)
{
}
}
}
}

codeunit 50100 MyTestCodeunit
{
Subtype = Test;

[Test]
procedure MyTest()
var
SubPage: TestPage MyListPart;
begin
SubPage.OpenView();
InvokeOnPart(SubPage);
end;

local procedure InvokeOnPart(var PartPage: TestPage MyListPart)
begin
[|PartPage.MyAction.Invoke()|];
end;
}
Loading