diff --git a/doc/AG0042.md b/doc/AG0042.md
new file mode 100644
index 0000000..a2151d2
--- /dev/null
+++ b/doc/AG0042.md
@@ -0,0 +1,123 @@
+# AG0042: QuerySelector should not be used with Playwright
+
+## Problem Description
+
+Using `QuerySelectorAsync()` in Playwright tests can lead to brittle and unreliable tests. This method uses CSS selectors which can be fragile and may break when the UI structure changes. Instead, more reliable locator strategies like data-testid or role-based selectors should be used.
+
+## Rule Details
+
+This rule raises an issue when `QuerySelectorAsync()` is called on Playwright `IPage` or `Page` objects.
+
+### Noncompliant Code Example
+
+```csharp
+public async Task ClickLoginButton(IPage page)
+{
+ // Noncompliant: Using QuerySelectorAsync with CSS selector
+ var loginButton = await page.QuerySelectorAsync(".login-button");
+ await loginButton.ClickAsync();
+}
+```
+
+### Compliant Solution
+
+```csharp
+public async Task ClickLoginButton(IPage page)
+{
+ // Compliant: Using Locator with data-testid
+ await page.Locator("[data-testid='login-button']").ClickAsync();
+
+ // Compliant: Using role-based selector
+ await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
+
+ // Compliant: Using text content
+ await page.GetByText("Login").ClickAsync();
+}
+```
+
+## Why is this an Issue?
+
+1. **Fragile Selectors**: CSS selectors are tightly coupled to the DOM structure and styling classes, making tests brittle when:
+ - CSS classes are renamed or removed
+ - DOM hierarchy changes
+ - Styling frameworks are updated
+
+2. **Maintainability**: CSS selectors can be complex and hard to maintain, especially when dealing with nested elements or specific combinations of classes.
+
+3. **Best Practices**: Playwright provides better alternatives that are:
+ - More resilient to changes
+ - More readable and maintainable
+ - Better aligned with testing best practices
+
+## Better Alternatives
+
+Playwright provides several better methods for selecting elements:
+
+1. **Data Test IDs**:
+ ```csharp
+ await page.Locator("[data-testid='submit-button']").ClickAsync();
+ ```
+
+2. **Role-based Selectors**:
+ ```csharp
+ await page.GetByRole(AriaRole.Button).ClickAsync();
+ await page.GetByRole(AriaRole.Textbox, new() { Name = "Username" }).FillAsync("user");
+ ```
+
+3. **Text Content**:
+ ```csharp
+ await page.GetByText("Sign up").ClickAsync();
+ await page.GetByLabel("Password").FillAsync("secret");
+ ```
+
+4. **Placeholder Text**:
+ ```csharp
+ await page.GetByPlaceholder("Enter email").FillAsync("test@example.com");
+ ```
+
+## How to Fix It
+
+1. Replace `QuerySelectorAsync()` calls with more specific Playwright locators:
+
+ ```csharp
+ // Before
+ var element = await page.QuerySelectorAsync(".submit-btn");
+
+ // After
+ var element = page.GetByRole(AriaRole.Button, new() { Name = "Submit" });
+ ```
+
+2. Add data-testid attributes to your application's elements:
+ ```html
+
+ ```
+
+ ```csharp
+ await page.Locator("[data-testid='submit-button']").ClickAsync();
+ ```
+
+3. Use semantic HTML with ARIA roles and labels:
+ ```html
+
+ ```
+
+ ```csharp
+ await page.GetByRole(AriaRole.Button, new() { Name = "Submit form" }).ClickAsync();
+ ```
+
+## Exceptions
+
+This rule might be relaxed in the following scenarios:
+- Legacy test code that's pending migration
+- Complex third-party components where other selectors aren't available
+- Testing CSS-specific functionality
+
+## Benefits
+- More reliable tests
+- Better test maintenance
+- Clearer test intentions
+- Improved accessibility testing
+
+## References
+- [ElementHandle is Discouraged by official Documents](https://playwright.dev/dotnet/docs/api/class-elementhandle)
+- [Playwright Locators Documentation](https://playwright.dev/docs/locators)
diff --git a/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs
new file mode 100644
index 0000000..83a776a
--- /dev/null
+++ b/src/Agoda.Analyzers.Test/AgodaCustom/AG0042UnitTests.cs
@@ -0,0 +1,244 @@
+using System.Threading.Tasks;
+using Agoda.Analyzers.AgodaCustom;
+using Agoda.Analyzers.Test.Helpers;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.Playwright;
+using NUnit.Framework;
+
+namespace Agoda.Analyzers.Test.AgodaCustom;
+
+class AG0042UnitTests : DiagnosticVerifier
+{
+ protected override DiagnosticAnalyzer DiagnosticAnalyzer => new AG0042QuerySelectorShouldNotBeUsed();
+
+ protected override string DiagnosticId => AG0042QuerySelectorShouldNotBeUsed.DIAGNOSTIC_ID;
+
+ [Test]
+ public async Task AG0042_WhenUsingQuerySelectorAsyncWithPlaywrightPage_ShowWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ References = new[] { typeof(IPage).Assembly },
+ Code = @"
+ using System.Threading.Tasks;
+ using Microsoft.Playwright;
+
+ class TestClass
+ {
+ public async Task TestMethod(IPage page)
+ {
+ await page.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 31));
+ }
+
+ [Test]
+ public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageInstanceVariable_ShowWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ References = new[] { typeof(IPage).Assembly },
+ Code = @"
+ using System.Threading.Tasks;
+ using Microsoft.Playwright;
+
+ class TestClass
+ {
+ private IPage _page;
+
+ public async Task TestMethod()
+ {
+ await _page.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31));
+ }
+
+ [Test]
+ public async Task AG0042_WhenUsingQuerySelectorAsyncWithLocalIPageVariable_ShowWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ References = new[] { typeof(IPage).Assembly },
+ Code = @"
+ using System.Threading.Tasks;
+ using Microsoft.Playwright;
+
+ class TestClass
+ {
+ public async Task TestMethod()
+ {
+ IPage page = null;
+ await page.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, new DiagnosticLocation(10, 31));
+ }
+
+ [Test]
+ public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageProperty_ShowWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ References = new[] { typeof(IPage).Assembly },
+ Code = @"
+ using System.Threading.Tasks;
+ using Microsoft.Playwright;
+
+ class TestClass
+ {
+ public IPage Page { get; set; }
+
+ public async Task TestMethod()
+ {
+ await Page.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31));
+ }
+
+ [Test]
+ public async Task AG0042_WhenUsingQuerySelectorAsyncWithNonIPageType_NoWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ // No need to reference Microsoft.Playwright
+ Code = @"
+ using System.Threading.Tasks;
+
+ class CustomPage
+ {
+ public async Task QuerySelectorAsync(string selector) { }
+ }
+
+ class TestClass
+ {
+ public async Task TestMethod()
+ {
+ CustomPage page = new CustomPage();
+ await page.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
+ }
+
+ [Test]
+ public async Task AG0042_WhenUsingLocatorMethodName_NoWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ References = new[] { typeof(IPage).Assembly },
+ Code = @"
+ using System.Threading.Tasks;
+ using Microsoft.Playwright;
+
+ class TestClass
+ {
+ public void TestMethod(IPage page)
+ {
+ page.Locator(""#selector"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
+ }
+
+ [Test]
+ public async Task AG0042_WhenSymbolIsNull_NoWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ // Intentionally use an unknown variable to cause symbol to be null
+ Code = @"
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ public async Task TestMethod()
+ {
+ dynamic unknownVariable = null;
+ await unknownVariable.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
+ }
+
+ [Test]
+ public async Task AG0042_WhenTypeSymbolIsNull_NoWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ Code = @"
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ public async Task TestMethod(dynamic page)
+ {
+ await page.QuerySelectorAsync(""#element"");
+ }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
+ }
+
+ [Test]
+ public async Task AG0042_WhenInvocationExpressionIsNotMemberAccess_NoWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ Code = @"
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ public async Task TestMethod()
+ {
+ await QuerySelectorAsync(""#element"");
+ }
+
+ public async Task QuerySelectorAsync(string selector) { }
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
+ }
+
+ [Test]
+ public async Task AG0042_WhenMemberAccessExpressionHasNoIdentifier_NoWarning()
+ {
+ var code = new CodeDescriptor
+ {
+ Code = @"
+ using System.Threading.Tasks;
+
+ class TestClass
+ {
+ public async Task TestMethod()
+ {
+ var func = GetPage();
+ await func().QuerySelectorAsync(""#element"");
+ }
+
+ public System.Func GetPage() => null;
+ }"
+ };
+
+ await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
+ }
+}
\ No newline at end of file
diff --git a/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs
new file mode 100644
index 0000000..176f565
--- /dev/null
+++ b/src/Agoda.Analyzers/AgodaCustom/AG0042QuerySelectorShouldNotBeUsed.cs
@@ -0,0 +1,97 @@
+using System.Collections.Immutable;
+using Agoda.Analyzers.Helpers;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Agoda.Analyzers.AgodaCustom
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class AG0042QuerySelectorShouldNotBeUsed : DiagnosticAnalyzer
+ {
+ public const string DIAGNOSTIC_ID = "AG0042";
+
+ private static readonly LocalizableString Title = new LocalizableResourceString(
+ nameof(CustomRulesResources.AG0042Title), CustomRulesResources.ResourceManager,
+ typeof(CustomRulesResources));
+
+ private static readonly LocalizableString MessageFormat = new LocalizableResourceString(
+ nameof(CustomRulesResources.AG0042Title), CustomRulesResources.ResourceManager,
+ typeof(CustomRulesResources));
+
+ private static readonly LocalizableString Description
+ = DescriptionContentLoader.GetAnalyzerDescription(nameof(AG0042QuerySelectorShouldNotBeUsed));
+
+ public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
+ DIAGNOSTIC_ID,
+ Title,
+ MessageFormat,
+ AnalyzerCategory.CustomQualityRules,
+ DiagnosticSeverity.Warning,
+ AnalyzerConstants.EnabledByDefault,
+ Description,
+ "https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0042.md",
+ WellKnownDiagnosticTags.EditAndContinue);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InvocationExpression);
+ }
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor);
+
+ private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
+ {
+ var invocationExpression = (InvocationExpressionSyntax)context.Node;
+
+ if (!(invocationExpression.Expression is MemberAccessExpressionSyntax memberAccess))
+ return;
+
+ var methodName = memberAccess.Name.Identifier.Text;
+
+ if (methodName != "QuerySelectorAsync")
+ return;
+
+ var semanticModel = context.SemanticModel;
+
+ var symbolInfo = semanticModel.GetSymbolInfo(memberAccess.Expression, context.CancellationToken);
+ var symbol = symbolInfo.Symbol;
+
+ if (symbol == null)
+ return;
+
+ INamedTypeSymbol typeSymbol;
+ switch (symbol)
+ {
+ case ILocalSymbol localSymbol:
+ typeSymbol = localSymbol.Type as INamedTypeSymbol;
+ break;
+ case IParameterSymbol parameterSymbol:
+ typeSymbol = parameterSymbol.Type as INamedTypeSymbol;
+ break;
+ case IFieldSymbol fieldSymbol:
+ typeSymbol = fieldSymbol.Type as INamedTypeSymbol;
+ break;
+ case IPropertySymbol propertySymbol:
+ typeSymbol = propertySymbol.Type as INamedTypeSymbol;
+ break;
+ default:
+ typeSymbol = null;
+ break;
+ }
+
+ if (typeSymbol == null)
+ return;
+
+ if (typeSymbol.ToString() != "Microsoft.Playwright.IPage" &&
+ typeSymbol.ToString() != "Microsoft.Playwright.Page")
+ return;
+
+ var diagnostic = Diagnostic.Create(Descriptor, invocationExpression.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs
index b7dca7a..cb52c81 100644
--- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs
+++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.Designer.cs
@@ -324,6 +324,12 @@ public static string AG0041Title {
}
}
+ public static string AG0042Title {
+ get {
+ return ResourceManager.GetString("AG0042Title", resourceCulture);
+ }
+ }
+
public static string AG0043Title {
get {
return ResourceManager.GetString("AG0043Title", resourceCulture);
diff --git a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx
index 5771a6a..b288e69 100644
--- a/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx
+++ b/src/Agoda.Analyzers/AgodaCustom/CustomRulesResources.resx
@@ -260,6 +260,9 @@ One exception is logging, where it can be useful to see the exact DC / cluster /
You are using either an interpolated string or string concatenation in your logs, change these to the message template format to preserve structure in your logs
+
+ QuerySelectorAsync usage found Replace with Locator or GetByRole for more reliable tests
+
BuildServiceProvider detected in request processing code. This may cause memory leaks. Remove the BuildServiceProvider() call and let the framework manage the service provider lifecycle.
diff --git a/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md b/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md
index 81f6295..c59537d 100644
--- a/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md
+++ b/src/Agoda.Analyzers/AnalyzerReleases.Unshipped.md
@@ -22,6 +22,9 @@ AG0030 | Agoda.CSharp.CustomQualityRules | Warning | AG0030PreventUseOfDynamics,
AG0037 | Agoda.CSharp.CustomQualityRules | Error | AG0037EnsureSeleniumTestHasOwnedByAttribute
AG0038 | Agoda.CSharp.CustomQualityRules | Warning | AG0038PreventUseOfRegionPreprocessorDirective, [Documentation](https://agoda-com.github.io/standards-c-sharp/code-style/regions.html)
AG0039 | Documentation | Hidden | AG0039MethodLineLengthAnalyzer, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/src/Agoda.Analyzers/RuleContent/AG0039MethodLineLengthAnalyzer.html)
+AG0041 | Best Practices | Warning | AG0041LogTemplateAnalyzer, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0041.md)
+AG0042 | Agoda.CSharp.CustomQualityRules | Warning | AG0042QuerySelectorShouldNotBeUsed, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0042.md)
+AG0043 | Agoda.CSharp.CustomQualityRules | Error | AG0043NoBuildServiceProvider, [Documentation](https://github.com/agoda-com/AgodaAnalyzers/blob/master/doc/AG0043.md)
SA1106 | StyleCop.CSharp.ReadabilityRules | Warning | SA1106CodeMustNotContainEmptyStatements
SA1107 | StyleCop.CSharp.ReadabilityRules | Warning | SA1107CodeMustNotContainMultipleStatementsOnOneLine
SA1123 | StyleCop.CSharp.ReadabilityRules | Warning | SA1123DoNotPlaceRegionsWithinElements
\ No newline at end of file