@codyswann/lisa 1.73.2 → 1.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -74,7 +74,7 @@
74
74
  "flatted": "^3.4.2"
75
75
  },
76
76
  "name": "@codyswann/lisa",
77
- "version": "1.73.2",
77
+ "version": "1.74.0",
78
78
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
79
79
  "main": "dist/index.js",
80
80
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "1.73.2",
3
+ "version": "1.74.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "1.73.2",
3
+ "version": "1.74.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "1.73.2",
3
+ "version": "1.74.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,45 +1,80 @@
1
1
  ---
2
2
  name: playwright-selectors
3
- description: Best practices for adding testID and aria-label selectors for Playwright E2E testing in Expo web applications. This skill should be used when adding E2E test coverage, creating new components that need test selectors, or reviewing code for testability.
3
+ description: Best practices for writing reliable Playwright E2E tests and adding testID/aria-label selectors in Expo web applications using GlueStack UI and NativeWind. Use this skill when creating, debugging, or modifying Playwright tests, adding E2E test coverage, creating components that need test selectors, reviewing code for testability, or troubleshooting testID/data-testid issues. Trigger on any mention of Playwright, E2E tests, end-to-end tests, testID, data-testid, or GlueStack testing in an Expo web context.
4
4
  ---
5
5
 
6
- # Playwright Selectors Best Practices
6
+ # Playwright E2E Testing for Expo + GlueStack UI
7
7
 
8
- ## Overview
8
+ ## The #1 Rule: Browser First, Code Second
9
9
 
10
- This skill provides guidance for adding effective selectors (testID, aria-labels) to Expo/React Native components for Playwright E2E testing. Proper selector strategy ensures tests are reliable, maintainable, and accessible.
10
+ Before writing ANY Playwright test, open the target page in a browser and manually walk through the flow. Never write tests blind from code reading alone.
11
11
 
12
- ## Selector Priority
12
+ Expo/GlueStack apps have complex rendering pipelines — what you see in source code is not always what renders in the DOM. Components may have state-dependent behavior (a button that opens an actionsheet OR a confirm dialog depending on data), elements may live on different tabs than you expect, and testIDs may or may not forward to the web DOM depending on the component type.
13
13
 
14
- Choose selectors based on their reliability and accessibility impact, in this priority order:
14
+ ### Workflow for each new test
15
15
 
16
- | Priority | Selector Method | When to Use |
17
- | -------- | --------------- | ----------------------------------------- |
18
- | 1 | `getByRole` | Interactive elements with semantic roles |
19
- | 2 | `getByText` | Visible text content |
20
- | 3 | `getByLabel` | Form elements with labels |
21
- | 4 | `getByTestId` | Fallback for elements without semantics |
16
+ 1. **Navigate** to the page using Playwright MCP browser tools
17
+ 2. **Click through** the exact user flow the test will cover
18
+ 3. **Run** `document.querySelectorAll('[data-testid]')` to see which testIDs are actually in the DOM
19
+ 4. **Note** any conditional UI states, loading sequences, and tab navigation required
20
+ 5. **Then** write the test code matching exactly what you observed
22
21
 
23
- ### getByRole (Preferred)
22
+ ### Verify testIDs before scaling
24
23
 
25
- Use semantic roles when possible - they improve both accessibility and test resilience.
24
+ Before writing a batch of tests that depend on testIDs, verify ONE testID end-to-end:
25
+
26
+ ```javascript
27
+ // Run this in the browser console or via Playwright MCP evaluate
28
+ document.querySelectorAll('[data-testid]').length
29
+ // Should return > 0 after page fully loads
30
+ ```
31
+
32
+ Accessibility snapshots from Playwright MCP may NOT show `data-testid` attributes. Always use `document.querySelectorAll` for ground truth. Pages may show very few testIDs before full render (e.g., 3 elements on initial load vs 80+ after data loads) — wait for the page to settle before checking.
33
+
34
+ ---
35
+
36
+ ## Selector Strategy
37
+
38
+ ### Priority order
39
+
40
+ 1. **`getByTestId`** — most stable, survives copy changes and redesigns
41
+ 2. **`getByRole`** — good for interactive elements (`button`, `tab`, `switch`, `heading`)
42
+ 3. **`getByLabel`** — form elements with labels
43
+ 4. **`getByPlaceholder`** — inputs with placeholder text
44
+ 5. **`getByText`** — fragile, use only when no testID or role is available
45
+
46
+ The generic web testing advice to prefer `getByRole` over `getByTestId` doesn't fully apply to React Native Web apps because ARIA role mapping is inconsistent across GlueStack components. testIDs are more reliable when properly set up.
26
47
 
27
48
  ```typescript
28
- // E2E test - preferred selector
29
- const submitButton = page.getByRole("button", { name: "Submit" });
30
- await expect(submitButton).toBeVisible();
49
+ // 1. Preferred testID
50
+ await expect(page.getByTestId("settings:dark-mode-toggle")).toBeVisible();
51
+
52
+ // 2. Good — role + accessible name
53
+ await page.getByRole("button", { name: "Close dialog" }).click();
54
+
55
+ // 3. Good — placeholder
56
+ await page.getByPlaceholder("Search players...").fill("Messi");
57
+
58
+ // 4. Fallback — text (fragile)
59
+ await expect(page.getByText("Settings").first()).toBeVisible();
31
60
  ```
32
61
 
33
- ### getByTestId (Fallback)
62
+ ### Fallback pattern for migration periods
34
63
 
35
- Use testID when semantic selectors are not available, particularly for structural containers.
64
+ When adding testIDs to components that are deployed separately from tests, use `.or()` to fall back gracefully:
36
65
 
37
66
  ```typescript
38
- // E2E test - fallback selector
39
- const container = page.getByTestId("home:container");
40
- await expect(container).toBeVisible();
67
+ // Works before AND after testID is deployed
68
+ const heading = page
69
+ .getByTestId("feature:heading")
70
+ .or(page.getByText("Feature Title").first());
71
+ await expect(heading.first()).toBeVisible();
41
72
  ```
42
73
 
74
+ Remove the `.or()` fallback once the testID is confirmed deployed and working.
75
+
76
+ ---
77
+
43
78
  ## testID Naming Convention
44
79
 
45
80
  Use a namespaced pattern with colons as separators: `screen:element`
@@ -55,13 +90,13 @@ Use a namespaced pattern with colons as separators: `screen:element`
55
90
 
56
91
  ### Examples
57
92
 
58
- | testID | Description |
59
- | -------------------------- | -------------------------------- |
60
- | `home:container` | Main container on home screen |
61
- | `home:title` | Title text on home screen |
62
- | `profile:avatar` | User avatar on profile screen |
63
- | `settings:dark-mode-toggle`| Dark mode toggle in settings |
64
- | `auth:login-button` | Login button on auth screen |
93
+ | testID | Description |
94
+ | --------------------------- | ------------------------------ |
95
+ | `home:container` | Main container on home screen |
96
+ | `home:title` | Title text on home screen |
97
+ | `profile:avatar` | User avatar on profile screen |
98
+ | `settings:dark-mode-toggle` | Dark mode toggle in settings |
99
+ | `auth:login-button` | Login button on auth screen |
65
100
 
66
101
  ### Rules
67
102
 
@@ -71,41 +106,83 @@ Use a namespaced pattern with colons as separators: `screen:element`
71
106
  4. Be descriptive but concise
72
107
  5. Avoid redundant words (e.g., `home:home-title` should be `home:title`)
73
108
 
74
- ## React Native to HTML Mapping
109
+ ---
75
110
 
76
- Understanding how testID propagates from React Native to the web is essential.
111
+ ## testID Forwarding: Which Components Support What
77
112
 
78
- ### How It Works
113
+ This is the most critical technical knowledge for this stack. GlueStack UI components have **different** testID behavior depending on their render pipeline.
79
114
 
80
- 1. React Native's `testID` prop is for native testing (XCUITest, Espresso)
81
- 2. On web (via react-native-web), `testID` renders as `data-testid` in HTML
82
- 3. Playwright's `getByTestId()` queries `data-testid` by default
115
+ ### The render chain
83
116
 
84
- ```typescript
85
- // React Native component
86
- <Box testID="home:container">...</Box>
117
+ React Native Web converts `testID` → `data-testid` through its `createDOMProps` function. But this only happens for components that go through RN Web's `createElement` path. GlueStack wraps many components with NativeWind utilities (`withStyleContext`, `tva`) that bypass this path.
118
+
119
+ ### Rules by component type
120
+
121
+ | Component | testID approach | Why |
122
+ |-----------|----------------|-----|
123
+ | `Pressable` (GlueStack) | `testID={value}` | Wraps RN `Pressable` → `View` → `createDOMProps` ✅ |
124
+ | `View` (react-native) | `testID={value}` | Goes through `createDOMProps` ✅ |
125
+ | `Text` (react-native) | `testID={value}` | Goes through `createDOMProps` ✅ |
126
+ | `Text` (GlueStack `@/components/ui/text`) | `{...{"data-testid": value}}` | NativeWind wrapper renders `<span>`, bypasses `createDOMProps` |
127
+ | `HStack` / `VStack` / `Box` (GlueStack) | `{...{"data-testid": value}}` | Same — NativeWind wrapper bypasses pipeline |
128
+ | `Heading` (GlueStack) | `data-testid={value}` | Renders raw `<h1>`–`<h6>` HTML elements |
129
+ | `Button` (GlueStack) | Unreliable — verify first | May or may not forward depending on version |
130
+ | Third-party (e.g., BouncyCheckbox) | Usually not possible | Use text/role selectors instead |
131
+
132
+ ### How to tell which path a component uses
133
+
134
+ Trace the component's render chain:
135
+
136
+ ```
137
+ GlueStack Pressable → createPressable({ Root: withStyleContext(RNPressable) })
138
+ → withStyleContext passes {...props} to RNPressable
139
+ → RN Web Pressable renders <View {...rest}>
140
+ → View goes through createElement → createDOMProps
141
+ → createDOMProps converts testID → data-testid ✅
142
+ ```
87
143
 
88
- // Rendered HTML on web
89
- <div data-testid="home:container">...</div>
144
+ vs:
90
145
 
91
- // Playwright locator
92
- page.getByTestId("home:container")
146
+ ```
147
+ GlueStack Text → tva-styled component
148
+ → Renders <span> or <p> directly
149
+ → Never hits createDOMProps
150
+ → testID prop is silently ignored ❌
93
151
  ```
94
152
 
95
- ### Gluestack UI Components
153
+ ### Adding a testID to a new component
96
154
 
97
- Gluestack UI web components (Box, Text, etc.) require explicit testID handling because they use native HTML elements instead of react-native-web components. The web versions have been updated to:
155
+ 1. Check the component type against the table above
156
+ 2. If it's a `Pressable` or RN `View`/`Text`, use `testID={value}` directly
157
+ 3. If it's GlueStack `Text`, `HStack`, `VStack`, `Box`, use `{...{"data-testid": value}}`
158
+ 4. If it's GlueStack `Heading`, use `data-testid={value}` as a JSX attribute
159
+ 5. **Build and verify** in the browser before writing tests against it
98
160
 
99
- 1. Accept a `testID` prop in the TypeScript type
100
- 2. Map `testID` to `data-testid` on the rendered HTML element
161
+ ```tsx
162
+ // Pressable testID prop works
163
+ <Pressable testID="feature:action-button" onPress={handlePress}>
164
+ <Text>Click me</Text>
165
+ </Pressable>
101
166
 
102
- ```typescript
103
- // Gluestack Box web implementation
104
- const Box = ({ testID, ...props }) => (
105
- <div data-testid={testID} {...props} />
106
- );
167
+ // GlueStack Text — use data-testid spread
168
+ <Text {...{"data-testid": "feature:section-heading"}}>
169
+ Section Title
170
+ </Text>
171
+
172
+ // GlueStack HStack — use data-testid spread
173
+ <HStack {...{"data-testid": "feature:row"}} className="items-center">
174
+ <Icon as={Star} />
175
+ <Text>Rating</Text>
176
+ </HStack>
177
+
178
+ // GlueStack Heading — use data-testid attribute
179
+ <Heading data-testid="feature:page-title" size="lg">
180
+ Page Title
181
+ </Heading>
107
182
  ```
108
183
 
184
+ ---
185
+
109
186
  ## When to Add testID
110
187
 
111
188
  ### Add testID To
@@ -122,16 +199,16 @@ const Box = ({ testID, ...props }) => (
122
199
  3. Decorative elements not needed for testing
123
200
  4. Elements inside third-party components (may not propagate)
124
201
 
202
+ ---
203
+
125
204
  ## Accessibility Best Practices
126
205
 
127
- Prefer semantic selectors and aria-labels over testID when possible.
206
+ Prefer semantic selectors and aria-labels over testID when possible. This benefits both testing and screen reader users.
128
207
 
129
208
  ### aria-label for Testing and Accessibility
130
209
 
131
- When adding labels for testing, use `aria-label` or `accessibilityLabel` to benefit screen reader users too.
132
-
133
210
  ```typescript
134
- // Correct - benefits both testing and accessibility
211
+ // Correct benefits both testing and accessibility
135
212
  <Pressable
136
213
  accessibilityLabel="Close dialog"
137
214
  onPress={handleClose}
@@ -145,10 +222,8 @@ await page.getByRole("button", { name: "Close dialog" }).click();
145
222
 
146
223
  ### accessibilityRole for Semantic Elements
147
224
 
148
- Use `accessibilityRole` to provide semantic meaning on web.
149
-
150
225
  ```typescript
151
- // Correct - semantic role for assistive technology
226
+ // Correct semantic role for assistive technology
152
227
  <Box accessibilityRole="banner" testID="header:container">
153
228
  <Text accessibilityRole="heading">Welcome</Text>
154
229
  </Box>
@@ -158,15 +233,164 @@ await expect(page.getByRole("banner")).toBeVisible();
158
233
  await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
159
234
  ```
160
235
 
236
+ ---
237
+
238
+ ## CI Architecture: Test the PR's Own Code
239
+
240
+ Playwright must test against the code in the PR, not a remote deployed environment. If CI tests against a deployed app, new testIDs and component changes are invisible until deployed — creating a frustrating push-wait-fail cycle.
241
+
242
+ ### Expo web setup for CI
243
+
244
+ The CI pipeline should:
245
+ 1. Build the web app: `npx expo export --platform web` (creates `dist/`)
246
+ 2. Serve it locally: `npx serve dist -l 8081 -s`
247
+ 3. Run Playwright against `http://localhost:8081/`
248
+
249
+ ### playwright.config.ts
250
+
251
+ ```typescript
252
+ import { defineConfig } from "@playwright/test";
253
+
254
+ export default defineConfig({
255
+ // In CI, serve the static web build locally
256
+ ...(process.env.CI
257
+ ? {
258
+ webServer: {
259
+ command: "npx serve dist -l 8081 -s",
260
+ port: 8081,
261
+ reuseExistingServer: false,
262
+ },
263
+ }
264
+ : {}),
265
+
266
+ use: {
267
+ baseURL: process.env.CI
268
+ ? "http://localhost:8081/"
269
+ : "https://dev.example.com/",
270
+ },
271
+ });
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Writing Robust Tests
277
+
278
+ ### Data independence
279
+
280
+ Never assert on data-dependent elements as required. The CI test user may have different data than your local environment.
281
+
282
+ ```typescript
283
+ // BAD — fails if test user has no data
284
+ const tableRows = page.locator("table tr");
285
+ await expect(tableRows.first()).toBeVisible();
286
+ expect(await tableRows.count()).toBeGreaterThan(1);
287
+
288
+ // GOOD — handles empty state gracefully
289
+ const tableRows = page.locator("table tbody tr");
290
+ const rowCount = await tableRows.count();
291
+ if (rowCount === 0) {
292
+ await expect(page.getByPlaceholder("Search...")).toBeVisible();
293
+ return;
294
+ }
295
+ await tableRows.first().click();
296
+ ```
297
+
298
+ ### Timeouts
299
+
300
+ Use environment-aware timeouts from a shared constants file. CI runners are slower than local machines.
301
+
302
+ ```typescript
303
+ export const TIMEOUT = {
304
+ test: isCI ? 90_000 : 60_000,
305
+ expect: isCI ? 30_000 : 15_000,
306
+ navigation: isCI ? 45_000 : 30_000,
307
+ };
308
+
309
+ // In tests — never hardcode
310
+ await expect(element).toBeVisible({ timeout: TIMEOUT.navigation });
311
+ ```
312
+
313
+ ### Serial vs parallel mode
314
+
315
+ Use serial mode for tests that mutate shared backend state. Read-only tests can run in parallel.
316
+
317
+ ```typescript
318
+ test.describe("Feature with mutations", () => {
319
+ test.describe.configure({ mode: "serial" });
320
+ });
321
+ ```
322
+
323
+ ### State-dependent UI
324
+
325
+ Some UI elements behave differently depending on application state. Discover this during the browser-first step, then handle both cases:
326
+
327
+ ```typescript
328
+ const addButton = page.getByTestId("feature:add-button").first();
329
+ await expect(addButton).toBeVisible();
330
+
331
+ const modal = page.getByText("Add to List");
332
+ const isModalVisible = await modal.isVisible();
333
+
334
+ if (isModalVisible) {
335
+ await page.getByText("Done").click();
336
+ } else {
337
+ await expect(addButton).toBeVisible();
338
+ }
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Common Pitfalls
344
+
345
+ ### SonarCloud security hotspots
346
+
347
+ These patterns trigger SonarCloud security hotspot warnings that block PR merges:
348
+
349
+ ```typescript
350
+ // BAD — triggers security hotspot
351
+ page.on("dialog", dialog => dialog.dismiss());
352
+ const result = await element.waitFor().catch(() => false);
353
+
354
+ // GOOD — use explicit checks instead
355
+ const isVisible = await element.isVisible();
356
+ const count = await elements.count();
357
+ ```
358
+
359
+ ### Page not fully rendered
360
+
361
+ Always wait for a content-dependent element before asserting on testIDs:
362
+
363
+ ```typescript
364
+ // BAD — may run before page renders
365
+ const count = await page.evaluate(() =>
366
+ document.querySelectorAll('[data-testid]').length
367
+ );
368
+
369
+ // GOOD — wait for known element first
370
+ await page.waitForLoadState("domcontentloaded");
371
+ const item = page.getByTestId("feature:item").first();
372
+ await item.waitFor({ state: "visible", timeout: 15000 });
373
+ ```
374
+
375
+ ### Third-party component testIDs
376
+
377
+ Components from third-party libraries (e.g., `react-native-bouncy-checkbox`, `react-native-gifted-chat`) generally do NOT forward `testID` to the web DOM. Use text, role, or structural selectors for these.
378
+
379
+ ---
380
+
161
381
  ## Implementation Checklist
162
382
 
163
383
  When adding E2E test coverage to a component:
164
384
 
385
+ - [ ] Open the page in a browser and walk through the flow first
386
+ - [ ] Run `document.querySelectorAll('[data-testid]')` to see existing testIDs
165
387
  - [ ] Identify elements that need selectors for testing
166
- - [ ] Prefer semantic selectors (role, text, label) when available
167
- - [ ] Use namespaced testID pattern for elements without semantics
168
- - [ ] Verify testID propagates to `data-testid` on web (check Gluestack components)
388
+ - [ ] Check component type against the forwarding rules table
389
+ - [ ] Use namespaced testID pattern (`screen:element`) for elements without semantics
169
390
  - [ ] Add accessibility labels where beneficial
391
+ - [ ] Verify testID propagates to `data-testid` on web before writing tests
392
+ - [ ] Use environment-aware timeouts, not hardcoded values
393
+ - [ ] Handle empty data states gracefully
170
394
  - [ ] Document testIDs in component JSDoc preamble
171
395
 
172
396
  ## Example Component
@@ -178,7 +402,6 @@ When adding E2E test coverage to a component:
178
402
  * Test IDs for E2E testing:
179
403
  * - `profile:container` - Main container
180
404
  * - `profile:avatar` - User avatar image
181
- * - `profile:name` - User display name
182
405
  *
183
406
  * @module features/profile/screens/Main
184
407
  */
@@ -189,7 +412,7 @@ export const ProfileScreen = () => (
189
412
  source={{ uri: user.avatarUrl }}
190
413
  accessibilityLabel={`${user.name}'s profile photo`}
191
414
  />
192
- <Text testID="profile:name" accessibilityRole="heading">
415
+ <Text accessibilityRole="heading">
193
416
  {user.name}
194
417
  </Text>
195
418
  <Pressable
@@ -206,15 +429,24 @@ export const ProfileScreen = () => (
206
429
 
207
430
  ```typescript
208
431
  test.describe("Profile Screen", () => {
432
+ test.use({ viewport: VIEWPORT.desktop });
433
+
434
+ test.beforeEach(async ({ auth }) => {
435
+ await auth.login();
436
+ });
437
+
209
438
  test("displays user information", async ({ page }) => {
210
439
  await page.goto("/profile");
440
+ await page.waitForLoadState("domcontentloaded");
211
441
 
212
442
  // Verify structural container
213
443
  await expect(page.getByTestId("profile:container")).toBeVisible();
214
444
 
215
445
  // Prefer accessible queries when available
216
446
  await expect(page.getByRole("heading")).toHaveText("John Doe");
217
- await expect(page.getByRole("button", { name: "Edit profile" })).toBeVisible();
447
+ await expect(
448
+ page.getByRole("button", { name: "Edit profile" })
449
+ ).toBeVisible();
218
450
 
219
451
  // Use testID for elements without semantic roles
220
452
  await expect(page.getByTestId("profile:avatar")).toBeVisible();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-nestjs",
3
- "version": "1.73.2",
3
+ "version": "1.74.0",
4
4
  "description": "NestJS-specific skills (GraphQL, TypeORM)",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-rails",
3
- "version": "1.73.2",
3
+ "version": "1.74.0",
4
4
  "description": "Ruby on Rails-specific hooks — RuboCop linting/formatting and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-typescript",
3
- "version": "1.73.2",
3
+ "version": "1.74.0",
4
4
  "description": "TypeScript-specific hooks — Prettier formatting, ESLint linting, and ast-grep scanning on edit",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,45 +1,80 @@
1
1
  ---
2
2
  name: playwright-selectors
3
- description: Best practices for adding testID and aria-label selectors for Playwright E2E testing in Expo web applications. This skill should be used when adding E2E test coverage, creating new components that need test selectors, or reviewing code for testability.
3
+ description: Best practices for writing reliable Playwright E2E tests and adding testID/aria-label selectors in Expo web applications using GlueStack UI and NativeWind. Use this skill when creating, debugging, or modifying Playwright tests, adding E2E test coverage, creating components that need test selectors, reviewing code for testability, or troubleshooting testID/data-testid issues. Trigger on any mention of Playwright, E2E tests, end-to-end tests, testID, data-testid, or GlueStack testing in an Expo web context.
4
4
  ---
5
5
 
6
- # Playwright Selectors Best Practices
6
+ # Playwright E2E Testing for Expo + GlueStack UI
7
7
 
8
- ## Overview
8
+ ## The #1 Rule: Browser First, Code Second
9
9
 
10
- This skill provides guidance for adding effective selectors (testID, aria-labels) to Expo/React Native components for Playwright E2E testing. Proper selector strategy ensures tests are reliable, maintainable, and accessible.
10
+ Before writing ANY Playwright test, open the target page in a browser and manually walk through the flow. Never write tests blind from code reading alone.
11
11
 
12
- ## Selector Priority
12
+ Expo/GlueStack apps have complex rendering pipelines — what you see in source code is not always what renders in the DOM. Components may have state-dependent behavior (a button that opens an actionsheet OR a confirm dialog depending on data), elements may live on different tabs than you expect, and testIDs may or may not forward to the web DOM depending on the component type.
13
13
 
14
- Choose selectors based on their reliability and accessibility impact, in this priority order:
14
+ ### Workflow for each new test
15
15
 
16
- | Priority | Selector Method | When to Use |
17
- | -------- | --------------- | ----------------------------------------- |
18
- | 1 | `getByRole` | Interactive elements with semantic roles |
19
- | 2 | `getByText` | Visible text content |
20
- | 3 | `getByLabel` | Form elements with labels |
21
- | 4 | `getByTestId` | Fallback for elements without semantics |
16
+ 1. **Navigate** to the page using Playwright MCP browser tools
17
+ 2. **Click through** the exact user flow the test will cover
18
+ 3. **Run** `document.querySelectorAll('[data-testid]')` to see which testIDs are actually in the DOM
19
+ 4. **Note** any conditional UI states, loading sequences, and tab navigation required
20
+ 5. **Then** write the test code matching exactly what you observed
22
21
 
23
- ### getByRole (Preferred)
22
+ ### Verify testIDs before scaling
24
23
 
25
- Use semantic roles when possible - they improve both accessibility and test resilience.
24
+ Before writing a batch of tests that depend on testIDs, verify ONE testID end-to-end:
25
+
26
+ ```javascript
27
+ // Run this in the browser console or via Playwright MCP evaluate
28
+ document.querySelectorAll('[data-testid]').length
29
+ // Should return > 0 after page fully loads
30
+ ```
31
+
32
+ Accessibility snapshots from Playwright MCP may NOT show `data-testid` attributes. Always use `document.querySelectorAll` for ground truth. Pages may show very few testIDs before full render (e.g., 3 elements on initial load vs 80+ after data loads) — wait for the page to settle before checking.
33
+
34
+ ---
35
+
36
+ ## Selector Strategy
37
+
38
+ ### Priority order
39
+
40
+ 1. **`getByTestId`** — most stable, survives copy changes and redesigns
41
+ 2. **`getByRole`** — good for interactive elements (`button`, `tab`, `switch`, `heading`)
42
+ 3. **`getByLabel`** — form elements with labels
43
+ 4. **`getByPlaceholder`** — inputs with placeholder text
44
+ 5. **`getByText`** — fragile, use only when no testID or role is available
45
+
46
+ The generic web testing advice to prefer `getByRole` over `getByTestId` doesn't fully apply to React Native Web apps because ARIA role mapping is inconsistent across GlueStack components. testIDs are more reliable when properly set up.
26
47
 
27
48
  ```typescript
28
- // E2E test - preferred selector
29
- const submitButton = page.getByRole("button", { name: "Submit" });
30
- await expect(submitButton).toBeVisible();
49
+ // 1. Preferred testID
50
+ await expect(page.getByTestId("settings:dark-mode-toggle")).toBeVisible();
51
+
52
+ // 2. Good — role + accessible name
53
+ await page.getByRole("button", { name: "Close dialog" }).click();
54
+
55
+ // 3. Good — placeholder
56
+ await page.getByPlaceholder("Search players...").fill("Messi");
57
+
58
+ // 4. Fallback — text (fragile)
59
+ await expect(page.getByText("Settings").first()).toBeVisible();
31
60
  ```
32
61
 
33
- ### getByTestId (Fallback)
62
+ ### Fallback pattern for migration periods
34
63
 
35
- Use testID when semantic selectors are not available, particularly for structural containers.
64
+ When adding testIDs to components that are deployed separately from tests, use `.or()` to fall back gracefully:
36
65
 
37
66
  ```typescript
38
- // E2E test - fallback selector
39
- const container = page.getByTestId("home:container");
40
- await expect(container).toBeVisible();
67
+ // Works before AND after testID is deployed
68
+ const heading = page
69
+ .getByTestId("feature:heading")
70
+ .or(page.getByText("Feature Title").first());
71
+ await expect(heading.first()).toBeVisible();
41
72
  ```
42
73
 
74
+ Remove the `.or()` fallback once the testID is confirmed deployed and working.
75
+
76
+ ---
77
+
43
78
  ## testID Naming Convention
44
79
 
45
80
  Use a namespaced pattern with colons as separators: `screen:element`
@@ -55,13 +90,13 @@ Use a namespaced pattern with colons as separators: `screen:element`
55
90
 
56
91
  ### Examples
57
92
 
58
- | testID | Description |
59
- | -------------------------- | -------------------------------- |
60
- | `home:container` | Main container on home screen |
61
- | `home:title` | Title text on home screen |
62
- | `profile:avatar` | User avatar on profile screen |
63
- | `settings:dark-mode-toggle`| Dark mode toggle in settings |
64
- | `auth:login-button` | Login button on auth screen |
93
+ | testID | Description |
94
+ | --------------------------- | ------------------------------ |
95
+ | `home:container` | Main container on home screen |
96
+ | `home:title` | Title text on home screen |
97
+ | `profile:avatar` | User avatar on profile screen |
98
+ | `settings:dark-mode-toggle` | Dark mode toggle in settings |
99
+ | `auth:login-button` | Login button on auth screen |
65
100
 
66
101
  ### Rules
67
102
 
@@ -71,41 +106,83 @@ Use a namespaced pattern with colons as separators: `screen:element`
71
106
  4. Be descriptive but concise
72
107
  5. Avoid redundant words (e.g., `home:home-title` should be `home:title`)
73
108
 
74
- ## React Native to HTML Mapping
109
+ ---
75
110
 
76
- Understanding how testID propagates from React Native to the web is essential.
111
+ ## testID Forwarding: Which Components Support What
77
112
 
78
- ### How It Works
113
+ This is the most critical technical knowledge for this stack. GlueStack UI components have **different** testID behavior depending on their render pipeline.
79
114
 
80
- 1. React Native's `testID` prop is for native testing (XCUITest, Espresso)
81
- 2. On web (via react-native-web), `testID` renders as `data-testid` in HTML
82
- 3. Playwright's `getByTestId()` queries `data-testid` by default
115
+ ### The render chain
83
116
 
84
- ```typescript
85
- // React Native component
86
- <Box testID="home:container">...</Box>
117
+ React Native Web converts `testID` → `data-testid` through its `createDOMProps` function. But this only happens for components that go through RN Web's `createElement` path. GlueStack wraps many components with NativeWind utilities (`withStyleContext`, `tva`) that bypass this path.
118
+
119
+ ### Rules by component type
120
+
121
+ | Component | testID approach | Why |
122
+ |-----------|----------------|-----|
123
+ | `Pressable` (GlueStack) | `testID={value}` | Wraps RN `Pressable` → `View` → `createDOMProps` ✅ |
124
+ | `View` (react-native) | `testID={value}` | Goes through `createDOMProps` ✅ |
125
+ | `Text` (react-native) | `testID={value}` | Goes through `createDOMProps` ✅ |
126
+ | `Text` (GlueStack `@/components/ui/text`) | `{...{"data-testid": value}}` | NativeWind wrapper renders `<span>`, bypasses `createDOMProps` |
127
+ | `HStack` / `VStack` / `Box` (GlueStack) | `{...{"data-testid": value}}` | Same — NativeWind wrapper bypasses pipeline |
128
+ | `Heading` (GlueStack) | `data-testid={value}` | Renders raw `<h1>`–`<h6>` HTML elements |
129
+ | `Button` (GlueStack) | Unreliable — verify first | May or may not forward depending on version |
130
+ | Third-party (e.g., BouncyCheckbox) | Usually not possible | Use text/role selectors instead |
131
+
132
+ ### How to tell which path a component uses
133
+
134
+ Trace the component's render chain:
135
+
136
+ ```
137
+ GlueStack Pressable → createPressable({ Root: withStyleContext(RNPressable) })
138
+ → withStyleContext passes {...props} to RNPressable
139
+ → RN Web Pressable renders <View {...rest}>
140
+ → View goes through createElement → createDOMProps
141
+ → createDOMProps converts testID → data-testid ✅
142
+ ```
87
143
 
88
- // Rendered HTML on web
89
- <div data-testid="home:container">...</div>
144
+ vs:
90
145
 
91
- // Playwright locator
92
- page.getByTestId("home:container")
146
+ ```
147
+ GlueStack Text → tva-styled component
148
+ → Renders <span> or <p> directly
149
+ → Never hits createDOMProps
150
+ → testID prop is silently ignored ❌
93
151
  ```
94
152
 
95
- ### Gluestack UI Components
153
+ ### Adding a testID to a new component
96
154
 
97
- Gluestack UI web components (Box, Text, etc.) require explicit testID handling because they use native HTML elements instead of react-native-web components. The web versions have been updated to:
155
+ 1. Check the component type against the table above
156
+ 2. If it's a `Pressable` or RN `View`/`Text`, use `testID={value}` directly
157
+ 3. If it's GlueStack `Text`, `HStack`, `VStack`, `Box`, use `{...{"data-testid": value}}`
158
+ 4. If it's GlueStack `Heading`, use `data-testid={value}` as a JSX attribute
159
+ 5. **Build and verify** in the browser before writing tests against it
98
160
 
99
- 1. Accept a `testID` prop in the TypeScript type
100
- 2. Map `testID` to `data-testid` on the rendered HTML element
161
+ ```tsx
162
+ // Pressable testID prop works
163
+ <Pressable testID="feature:action-button" onPress={handlePress}>
164
+ <Text>Click me</Text>
165
+ </Pressable>
101
166
 
102
- ```typescript
103
- // Gluestack Box web implementation
104
- const Box = ({ testID, ...props }) => (
105
- <div data-testid={testID} {...props} />
106
- );
167
+ // GlueStack Text — use data-testid spread
168
+ <Text {...{"data-testid": "feature:section-heading"}}>
169
+ Section Title
170
+ </Text>
171
+
172
+ // GlueStack HStack — use data-testid spread
173
+ <HStack {...{"data-testid": "feature:row"}} className="items-center">
174
+ <Icon as={Star} />
175
+ <Text>Rating</Text>
176
+ </HStack>
177
+
178
+ // GlueStack Heading — use data-testid attribute
179
+ <Heading data-testid="feature:page-title" size="lg">
180
+ Page Title
181
+ </Heading>
107
182
  ```
108
183
 
184
+ ---
185
+
109
186
  ## When to Add testID
110
187
 
111
188
  ### Add testID To
@@ -122,16 +199,16 @@ const Box = ({ testID, ...props }) => (
122
199
  3. Decorative elements not needed for testing
123
200
  4. Elements inside third-party components (may not propagate)
124
201
 
202
+ ---
203
+
125
204
  ## Accessibility Best Practices
126
205
 
127
- Prefer semantic selectors and aria-labels over testID when possible.
206
+ Prefer semantic selectors and aria-labels over testID when possible. This benefits both testing and screen reader users.
128
207
 
129
208
  ### aria-label for Testing and Accessibility
130
209
 
131
- When adding labels for testing, use `aria-label` or `accessibilityLabel` to benefit screen reader users too.
132
-
133
210
  ```typescript
134
- // Correct - benefits both testing and accessibility
211
+ // Correct benefits both testing and accessibility
135
212
  <Pressable
136
213
  accessibilityLabel="Close dialog"
137
214
  onPress={handleClose}
@@ -145,10 +222,8 @@ await page.getByRole("button", { name: "Close dialog" }).click();
145
222
 
146
223
  ### accessibilityRole for Semantic Elements
147
224
 
148
- Use `accessibilityRole` to provide semantic meaning on web.
149
-
150
225
  ```typescript
151
- // Correct - semantic role for assistive technology
226
+ // Correct semantic role for assistive technology
152
227
  <Box accessibilityRole="banner" testID="header:container">
153
228
  <Text accessibilityRole="heading">Welcome</Text>
154
229
  </Box>
@@ -158,15 +233,164 @@ await expect(page.getByRole("banner")).toBeVisible();
158
233
  await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible();
159
234
  ```
160
235
 
236
+ ---
237
+
238
+ ## CI Architecture: Test the PR's Own Code
239
+
240
+ Playwright must test against the code in the PR, not a remote deployed environment. If CI tests against a deployed app, new testIDs and component changes are invisible until deployed — creating a frustrating push-wait-fail cycle.
241
+
242
+ ### Expo web setup for CI
243
+
244
+ The CI pipeline should:
245
+ 1. Build the web app: `npx expo export --platform web` (creates `dist/`)
246
+ 2. Serve it locally: `npx serve dist -l 8081 -s`
247
+ 3. Run Playwright against `http://localhost:8081/`
248
+
249
+ ### playwright.config.ts
250
+
251
+ ```typescript
252
+ import { defineConfig } from "@playwright/test";
253
+
254
+ export default defineConfig({
255
+ // In CI, serve the static web build locally
256
+ ...(process.env.CI
257
+ ? {
258
+ webServer: {
259
+ command: "npx serve dist -l 8081 -s",
260
+ port: 8081,
261
+ reuseExistingServer: false,
262
+ },
263
+ }
264
+ : {}),
265
+
266
+ use: {
267
+ baseURL: process.env.CI
268
+ ? "http://localhost:8081/"
269
+ : "https://dev.example.com/",
270
+ },
271
+ });
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Writing Robust Tests
277
+
278
+ ### Data independence
279
+
280
+ Never assert on data-dependent elements as required. The CI test user may have different data than your local environment.
281
+
282
+ ```typescript
283
+ // BAD — fails if test user has no data
284
+ const tableRows = page.locator("table tr");
285
+ await expect(tableRows.first()).toBeVisible();
286
+ expect(await tableRows.count()).toBeGreaterThan(1);
287
+
288
+ // GOOD — handles empty state gracefully
289
+ const tableRows = page.locator("table tbody tr");
290
+ const rowCount = await tableRows.count();
291
+ if (rowCount === 0) {
292
+ await expect(page.getByPlaceholder("Search...")).toBeVisible();
293
+ return;
294
+ }
295
+ await tableRows.first().click();
296
+ ```
297
+
298
+ ### Timeouts
299
+
300
+ Use environment-aware timeouts from a shared constants file. CI runners are slower than local machines.
301
+
302
+ ```typescript
303
+ export const TIMEOUT = {
304
+ test: isCI ? 90_000 : 60_000,
305
+ expect: isCI ? 30_000 : 15_000,
306
+ navigation: isCI ? 45_000 : 30_000,
307
+ };
308
+
309
+ // In tests — never hardcode
310
+ await expect(element).toBeVisible({ timeout: TIMEOUT.navigation });
311
+ ```
312
+
313
+ ### Serial vs parallel mode
314
+
315
+ Use serial mode for tests that mutate shared backend state. Read-only tests can run in parallel.
316
+
317
+ ```typescript
318
+ test.describe("Feature with mutations", () => {
319
+ test.describe.configure({ mode: "serial" });
320
+ });
321
+ ```
322
+
323
+ ### State-dependent UI
324
+
325
+ Some UI elements behave differently depending on application state. Discover this during the browser-first step, then handle both cases:
326
+
327
+ ```typescript
328
+ const addButton = page.getByTestId("feature:add-button").first();
329
+ await expect(addButton).toBeVisible();
330
+
331
+ const modal = page.getByText("Add to List");
332
+ const isModalVisible = await modal.isVisible();
333
+
334
+ if (isModalVisible) {
335
+ await page.getByText("Done").click();
336
+ } else {
337
+ await expect(addButton).toBeVisible();
338
+ }
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Common Pitfalls
344
+
345
+ ### SonarCloud security hotspots
346
+
347
+ These patterns trigger SonarCloud security hotspot warnings that block PR merges:
348
+
349
+ ```typescript
350
+ // BAD — triggers security hotspot
351
+ page.on("dialog", dialog => dialog.dismiss());
352
+ const result = await element.waitFor().catch(() => false);
353
+
354
+ // GOOD — use explicit checks instead
355
+ const isVisible = await element.isVisible();
356
+ const count = await elements.count();
357
+ ```
358
+
359
+ ### Page not fully rendered
360
+
361
+ Always wait for a content-dependent element before asserting on testIDs:
362
+
363
+ ```typescript
364
+ // BAD — may run before page renders
365
+ const count = await page.evaluate(() =>
366
+ document.querySelectorAll('[data-testid]').length
367
+ );
368
+
369
+ // GOOD — wait for known element first
370
+ await page.waitForLoadState("domcontentloaded");
371
+ const item = page.getByTestId("feature:item").first();
372
+ await item.waitFor({ state: "visible", timeout: 15000 });
373
+ ```
374
+
375
+ ### Third-party component testIDs
376
+
377
+ Components from third-party libraries (e.g., `react-native-bouncy-checkbox`, `react-native-gifted-chat`) generally do NOT forward `testID` to the web DOM. Use text, role, or structural selectors for these.
378
+
379
+ ---
380
+
161
381
  ## Implementation Checklist
162
382
 
163
383
  When adding E2E test coverage to a component:
164
384
 
385
+ - [ ] Open the page in a browser and walk through the flow first
386
+ - [ ] Run `document.querySelectorAll('[data-testid]')` to see existing testIDs
165
387
  - [ ] Identify elements that need selectors for testing
166
- - [ ] Prefer semantic selectors (role, text, label) when available
167
- - [ ] Use namespaced testID pattern for elements without semantics
168
- - [ ] Verify testID propagates to `data-testid` on web (check Gluestack components)
388
+ - [ ] Check component type against the forwarding rules table
389
+ - [ ] Use namespaced testID pattern (`screen:element`) for elements without semantics
169
390
  - [ ] Add accessibility labels where beneficial
391
+ - [ ] Verify testID propagates to `data-testid` on web before writing tests
392
+ - [ ] Use environment-aware timeouts, not hardcoded values
393
+ - [ ] Handle empty data states gracefully
170
394
  - [ ] Document testIDs in component JSDoc preamble
171
395
 
172
396
  ## Example Component
@@ -178,7 +402,6 @@ When adding E2E test coverage to a component:
178
402
  * Test IDs for E2E testing:
179
403
  * - `profile:container` - Main container
180
404
  * - `profile:avatar` - User avatar image
181
- * - `profile:name` - User display name
182
405
  *
183
406
  * @module features/profile/screens/Main
184
407
  */
@@ -189,7 +412,7 @@ export const ProfileScreen = () => (
189
412
  source={{ uri: user.avatarUrl }}
190
413
  accessibilityLabel={`${user.name}'s profile photo`}
191
414
  />
192
- <Text testID="profile:name" accessibilityRole="heading">
415
+ <Text accessibilityRole="heading">
193
416
  {user.name}
194
417
  </Text>
195
418
  <Pressable
@@ -206,15 +429,24 @@ export const ProfileScreen = () => (
206
429
 
207
430
  ```typescript
208
431
  test.describe("Profile Screen", () => {
432
+ test.use({ viewport: VIEWPORT.desktop });
433
+
434
+ test.beforeEach(async ({ auth }) => {
435
+ await auth.login();
436
+ });
437
+
209
438
  test("displays user information", async ({ page }) => {
210
439
  await page.goto("/profile");
440
+ await page.waitForLoadState("domcontentloaded");
211
441
 
212
442
  // Verify structural container
213
443
  await expect(page.getByTestId("profile:container")).toBeVisible();
214
444
 
215
445
  // Prefer accessible queries when available
216
446
  await expect(page.getByRole("heading")).toHaveText("John Doe");
217
- await expect(page.getByRole("button", { name: "Edit profile" })).toBeVisible();
447
+ await expect(
448
+ page.getByRole("button", { name: "Edit profile" })
449
+ ).toBeVisible();
218
450
 
219
451
  // Use testID for elements without semantic roles
220
452
  await expect(page.getByTestId("profile:avatar")).toBeVisible();