Skip to content

Commit 7e9c705

Browse files
rannesjoeldickson
andauthored
Add analyzer for QuerySelector (#191)
* feat: Add analyzer for QuerySelector * docs: add MD doc for rule * feat: fix code and add more tests * feat: Add analyzer for QuerySelector * docs: add MD doc for rule * feat: fix code and add more tests * docs: move the MD to correct folder * remove docs folder and files * docs: let Claude generate docs * Update CustomRulesResources.resx * fix: AnalyzerRelease warning --------- Co-authored-by: Joel Dickson <[email protected]>
1 parent 57aeae5 commit 7e9c705

File tree

6 files changed

+476
-0
lines changed

6 files changed

+476
-0
lines changed

doc/AG0042.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# AG0042: QuerySelector should not be used with Playwright
2+
3+
## Problem Description
4+
5+
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.
6+
7+
## Rule Details
8+
9+
This rule raises an issue when `QuerySelectorAsync()` is called on Playwright `IPage` or `Page` objects.
10+
11+
### Noncompliant Code Example
12+
13+
```csharp
14+
public async Task ClickLoginButton(IPage page)
15+
{
16+
// Noncompliant: Using QuerySelectorAsync with CSS selector
17+
var loginButton = await page.QuerySelectorAsync(".login-button");
18+
await loginButton.ClickAsync();
19+
}
20+
```
21+
22+
### Compliant Solution
23+
24+
```csharp
25+
public async Task ClickLoginButton(IPage page)
26+
{
27+
// Compliant: Using Locator with data-testid
28+
await page.Locator("[data-testid='login-button']").ClickAsync();
29+
30+
// Compliant: Using role-based selector
31+
await page.GetByRole(AriaRole.Button, new() { Name = "Login" }).ClickAsync();
32+
33+
// Compliant: Using text content
34+
await page.GetByText("Login").ClickAsync();
35+
}
36+
```
37+
38+
## Why is this an Issue?
39+
40+
1. **Fragile Selectors**: CSS selectors are tightly coupled to the DOM structure and styling classes, making tests brittle when:
41+
- CSS classes are renamed or removed
42+
- DOM hierarchy changes
43+
- Styling frameworks are updated
44+
45+
2. **Maintainability**: CSS selectors can be complex and hard to maintain, especially when dealing with nested elements or specific combinations of classes.
46+
47+
3. **Best Practices**: Playwright provides better alternatives that are:
48+
- More resilient to changes
49+
- More readable and maintainable
50+
- Better aligned with testing best practices
51+
52+
## Better Alternatives
53+
54+
Playwright provides several better methods for selecting elements:
55+
56+
1. **Data Test IDs**:
57+
```csharp
58+
await page.Locator("[data-testid='submit-button']").ClickAsync();
59+
```
60+
61+
2. **Role-based Selectors**:
62+
```csharp
63+
await page.GetByRole(AriaRole.Button).ClickAsync();
64+
await page.GetByRole(AriaRole.Textbox, new() { Name = "Username" }).FillAsync("user");
65+
```
66+
67+
3. **Text Content**:
68+
```csharp
69+
await page.GetByText("Sign up").ClickAsync();
70+
await page.GetByLabel("Password").FillAsync("secret");
71+
```
72+
73+
4. **Placeholder Text**:
74+
```csharp
75+
await page.GetByPlaceholder("Enter email").FillAsync("[email protected]");
76+
```
77+
78+
## How to Fix It
79+
80+
1. Replace `QuerySelectorAsync()` calls with more specific Playwright locators:
81+
82+
```csharp
83+
// Before
84+
var element = await page.QuerySelectorAsync(".submit-btn");
85+
86+
// After
87+
var element = page.GetByRole(AriaRole.Button, new() { Name = "Submit" });
88+
```
89+
90+
2. Add data-testid attributes to your application's elements:
91+
```html
92+
<button data-testid="submit-button">Submit</button>
93+
```
94+
95+
```csharp
96+
await page.Locator("[data-testid='submit-button']").ClickAsync();
97+
```
98+
99+
3. Use semantic HTML with ARIA roles and labels:
100+
```html
101+
<button role="button" aria-label="Submit form">Submit</button>
102+
```
103+
104+
```csharp
105+
await page.GetByRole(AriaRole.Button, new() { Name = "Submit form" }).ClickAsync();
106+
```
107+
108+
## Exceptions
109+
110+
This rule might be relaxed in the following scenarios:
111+
- Legacy test code that's pending migration
112+
- Complex third-party components where other selectors aren't available
113+
- Testing CSS-specific functionality
114+
115+
## Benefits
116+
- More reliable tests
117+
- Better test maintenance
118+
- Clearer test intentions
119+
- Improved accessibility testing
120+
121+
## References
122+
- [ElementHandle is Discouraged by official Documents](https://playwright.dev/dotnet/docs/api/class-elementhandle)
123+
- [Playwright Locators Documentation](https://playwright.dev/docs/locators)
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
using System.Threading.Tasks;
2+
using Agoda.Analyzers.AgodaCustom;
3+
using Agoda.Analyzers.Test.Helpers;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using Microsoft.Playwright;
6+
using NUnit.Framework;
7+
8+
namespace Agoda.Analyzers.Test.AgodaCustom;
9+
10+
class AG0042UnitTests : DiagnosticVerifier
11+
{
12+
protected override DiagnosticAnalyzer DiagnosticAnalyzer => new AG0042QuerySelectorShouldNotBeUsed();
13+
14+
protected override string DiagnosticId => AG0042QuerySelectorShouldNotBeUsed.DIAGNOSTIC_ID;
15+
16+
[Test]
17+
public async Task AG0042_WhenUsingQuerySelectorAsyncWithPlaywrightPage_ShowWarning()
18+
{
19+
var code = new CodeDescriptor
20+
{
21+
References = new[] { typeof(IPage).Assembly },
22+
Code = @"
23+
using System.Threading.Tasks;
24+
using Microsoft.Playwright;
25+
26+
class TestClass
27+
{
28+
public async Task TestMethod(IPage page)
29+
{
30+
await page.QuerySelectorAsync(""#element"");
31+
}
32+
}"
33+
};
34+
35+
await VerifyDiagnosticsAsync(code, new DiagnosticLocation(9, 31));
36+
}
37+
38+
[Test]
39+
public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageInstanceVariable_ShowWarning()
40+
{
41+
var code = new CodeDescriptor
42+
{
43+
References = new[] { typeof(IPage).Assembly },
44+
Code = @"
45+
using System.Threading.Tasks;
46+
using Microsoft.Playwright;
47+
48+
class TestClass
49+
{
50+
private IPage _page;
51+
52+
public async Task TestMethod()
53+
{
54+
await _page.QuerySelectorAsync(""#element"");
55+
}
56+
}"
57+
};
58+
59+
await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31));
60+
}
61+
62+
[Test]
63+
public async Task AG0042_WhenUsingQuerySelectorAsyncWithLocalIPageVariable_ShowWarning()
64+
{
65+
var code = new CodeDescriptor
66+
{
67+
References = new[] { typeof(IPage).Assembly },
68+
Code = @"
69+
using System.Threading.Tasks;
70+
using Microsoft.Playwright;
71+
72+
class TestClass
73+
{
74+
public async Task TestMethod()
75+
{
76+
IPage page = null;
77+
await page.QuerySelectorAsync(""#element"");
78+
}
79+
}"
80+
};
81+
82+
await VerifyDiagnosticsAsync(code, new DiagnosticLocation(10, 31));
83+
}
84+
85+
[Test]
86+
public async Task AG0042_WhenUsingQuerySelectorAsyncWithIPageProperty_ShowWarning()
87+
{
88+
var code = new CodeDescriptor
89+
{
90+
References = new[] { typeof(IPage).Assembly },
91+
Code = @"
92+
using System.Threading.Tasks;
93+
using Microsoft.Playwright;
94+
95+
class TestClass
96+
{
97+
public IPage Page { get; set; }
98+
99+
public async Task TestMethod()
100+
{
101+
await Page.QuerySelectorAsync(""#element"");
102+
}
103+
}"
104+
};
105+
106+
await VerifyDiagnosticsAsync(code, new DiagnosticLocation(11, 31));
107+
}
108+
109+
[Test]
110+
public async Task AG0042_WhenUsingQuerySelectorAsyncWithNonIPageType_NoWarning()
111+
{
112+
var code = new CodeDescriptor
113+
{
114+
// No need to reference Microsoft.Playwright
115+
Code = @"
116+
using System.Threading.Tasks;
117+
118+
class CustomPage
119+
{
120+
public async Task QuerySelectorAsync(string selector) { }
121+
}
122+
123+
class TestClass
124+
{
125+
public async Task TestMethod()
126+
{
127+
CustomPage page = new CustomPage();
128+
await page.QuerySelectorAsync(""#element"");
129+
}
130+
}"
131+
};
132+
133+
await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
134+
}
135+
136+
[Test]
137+
public async Task AG0042_WhenUsingLocatorMethodName_NoWarning()
138+
{
139+
var code = new CodeDescriptor
140+
{
141+
References = new[] { typeof(IPage).Assembly },
142+
Code = @"
143+
using System.Threading.Tasks;
144+
using Microsoft.Playwright;
145+
146+
class TestClass
147+
{
148+
public void TestMethod(IPage page)
149+
{
150+
page.Locator(""#selector"");
151+
}
152+
}"
153+
};
154+
155+
await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
156+
}
157+
158+
[Test]
159+
public async Task AG0042_WhenSymbolIsNull_NoWarning()
160+
{
161+
var code = new CodeDescriptor
162+
{
163+
// Intentionally use an unknown variable to cause symbol to be null
164+
Code = @"
165+
using System.Threading.Tasks;
166+
167+
class TestClass
168+
{
169+
public async Task TestMethod()
170+
{
171+
dynamic unknownVariable = null;
172+
await unknownVariable.QuerySelectorAsync(""#element"");
173+
}
174+
}"
175+
};
176+
177+
await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
178+
}
179+
180+
[Test]
181+
public async Task AG0042_WhenTypeSymbolIsNull_NoWarning()
182+
{
183+
var code = new CodeDescriptor
184+
{
185+
Code = @"
186+
using System.Threading.Tasks;
187+
188+
class TestClass
189+
{
190+
public async Task TestMethod(dynamic page)
191+
{
192+
await page.QuerySelectorAsync(""#element"");
193+
}
194+
}"
195+
};
196+
197+
await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
198+
}
199+
200+
[Test]
201+
public async Task AG0042_WhenInvocationExpressionIsNotMemberAccess_NoWarning()
202+
{
203+
var code = new CodeDescriptor
204+
{
205+
Code = @"
206+
using System.Threading.Tasks;
207+
208+
class TestClass
209+
{
210+
public async Task TestMethod()
211+
{
212+
await QuerySelectorAsync(""#element"");
213+
}
214+
215+
public async Task QuerySelectorAsync(string selector) { }
216+
}"
217+
};
218+
219+
await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
220+
}
221+
222+
[Test]
223+
public async Task AG0042_WhenMemberAccessExpressionHasNoIdentifier_NoWarning()
224+
{
225+
var code = new CodeDescriptor
226+
{
227+
Code = @"
228+
using System.Threading.Tasks;
229+
230+
class TestClass
231+
{
232+
public async Task TestMethod()
233+
{
234+
var func = GetPage();
235+
await func().QuerySelectorAsync(""#element"");
236+
}
237+
238+
public System.Func<dynamic> GetPage() => null;
239+
}"
240+
};
241+
242+
await VerifyDiagnosticsAsync(code, EmptyDiagnosticResults);
243+
}
244+
}

0 commit comments

Comments
 (0)