@codyswann/lisa 4.65.8 → 4.66.1
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/all/copy-overwrite/scripts/lisa-hooks/worktree-binding-guard.mjs +34 -10
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +8 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +16 -7
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/workflows/maestro-e2e.yml +6 -0
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/worktree-binding-guard.mjs +34 -10
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/worktree-binding-guard.mjs +34 -10
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/worktree-binding-guard.mjs +34 -10
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/skills/expo-router-best-practices/SKILL.md +1 -0
- package/plugins/lisa-expo/.codex-plugin/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/lisa-expo/.codex-plugin/skills/expo-tailwind-setup/SKILL.md +7 -0
- package/plugins/lisa-expo/.codex-plugin/skills/playwright-selectors/SKILL.md +25 -0
- package/plugins/lisa-expo/.codex-plugin/skills/testing-library/references/mocking-patterns.md +22 -0
- package/plugins/lisa-expo/skills/expo-router-best-practices/SKILL.md +1 -0
- package/plugins/lisa-expo/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/lisa-expo/skills/expo-tailwind-setup/SKILL.md +7 -0
- package/plugins/lisa-expo/skills/playwright-selectors/SKILL.md +25 -0
- package/plugins/lisa-expo/skills/testing-library/references/mocking-patterns.md +22 -0
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/expo-router-best-practices/SKILL.md +1 -0
- package/plugins/lisa-expo-agy/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/lisa-expo-agy/skills/expo-tailwind-setup/SKILL.md +7 -0
- package/plugins/lisa-expo-agy/skills/playwright-selectors/SKILL.md +25 -0
- package/plugins/lisa-expo-agy/skills/testing-library/references/mocking-patterns.md +22 -0
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/expo-router-best-practices/SKILL.md +1 -0
- package/plugins/lisa-expo-copilot/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/lisa-expo-copilot/skills/expo-tailwind-setup/SKILL.md +7 -0
- package/plugins/lisa-expo-copilot/skills/playwright-selectors/SKILL.md +25 -0
- package/plugins/lisa-expo-copilot/skills/testing-library/references/mocking-patterns.md +22 -0
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/expo-router-best-practices/SKILL.md +1 -0
- package/plugins/lisa-expo-cursor/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/lisa-expo-cursor/skills/expo-tailwind-setup/SKILL.md +7 -0
- package/plugins/lisa-expo-cursor/skills/playwright-selectors/SKILL.md +25 -0
- package/plugins/lisa-expo-cursor/skills/testing-library/references/mocking-patterns.md +22 -0
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/worktree-binding-guard.mjs +34 -10
- package/plugins/src/expo/skills/expo-router-best-practices/SKILL.md +1 -0
- package/plugins/src/expo/skills/expo-router-best-practices/references/static-export-verification.md +54 -0
- package/plugins/src/expo/skills/expo-tailwind-setup/SKILL.md +7 -0
- package/plugins/src/expo/skills/playwright-selectors/SKILL.md +25 -0
- package/plugins/src/expo/skills/testing-library/references/mocking-patterns.md +22 -0
|
@@ -394,6 +394,31 @@ Components from third-party libraries (e.g., `react-native-bouncy-checkbox`, `re
|
|
|
394
394
|
|
|
395
395
|
---
|
|
396
396
|
|
|
397
|
+
## Reveal-on-Focus Assertions
|
|
398
|
+
|
|
399
|
+
`toBeVisible()` proves the element exists and is not hidden — it does NOT
|
|
400
|
+
prove the element crossed onto the screen. A focused element scrolled off-view
|
|
401
|
+
still reports visible. For any "focus reveals the element" behavior (skip
|
|
402
|
+
links, focus-trapped modals, scroll-into-view containers), assert on layout,
|
|
403
|
+
not just visibility:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// Rejects when the element has no box at all (display: none, detached).
|
|
407
|
+
const box = await page.getByTestId("dialog:panel").boundingBox();
|
|
408
|
+
expect(box).not.toBeNull();
|
|
409
|
+
|
|
410
|
+
// Then prove it is actually inside the viewport — the part toBeVisible()
|
|
411
|
+
// cannot say.
|
|
412
|
+
await expect(page.getByTestId("dialog:panel")).toBeInViewport();
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Pair them: `boundingBox()` answers "is there a box" (null means never laid
|
|
416
|
+
out), `toBeInViewport()` answers "did it cross onto the screen". An assertion
|
|
417
|
+
that skips the null check reads a missing layout as a viewport miss, which is
|
|
418
|
+
a different bug.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
397
422
|
## Implementation Checklist
|
|
398
423
|
|
|
399
424
|
When adding E2E test coverage to a component:
|
|
@@ -588,3 +588,25 @@ afterAll(() => {
|
|
|
588
588
|
console.error = originalError;
|
|
589
589
|
});
|
|
590
590
|
```
|
|
591
|
+
|
|
592
|
+
## Migrating jest.mock Usage
|
|
593
|
+
|
|
594
|
+
When moving a test file onto the managed setup, scope the migration to the
|
|
595
|
+
file under your hand — a suite-wide mock sweep is where subtle behavior
|
|
596
|
+
changes hide.
|
|
597
|
+
|
|
598
|
+
- **Prefer renderer-compatible mocks.** A mock that works in the Node
|
|
599
|
+
environment but reads `window`, native modules, or ESM-only entry points
|
|
600
|
+
fails under the jsdom/renderer the Expo preset provides. Mock the seam your
|
|
601
|
+
component actually imports (`jest.mock("nativewind", ...)`,
|
|
602
|
+
`jest.mock("@/lib/env", ...)`) — never reach around it to mock a transitive
|
|
603
|
+
dependency.
|
|
604
|
+
- **Keep factories stable and hoisted.** Jest hoists `jest.mock` calls above
|
|
605
|
+
imports, so a factory cannot close over outer variables. Everything the
|
|
606
|
+
factory needs must come from `jest.requireActual`, `jest.requireMock`, or a
|
|
607
|
+
`mock`-prefixed name. A factory that silently returns a different shape per
|
|
608
|
+
run is the migration failure that surfaces as unrelated flakes.
|
|
609
|
+
- **Check version compatibility, not history.** A pinned workaround for an
|
|
610
|
+
old jest-expo/Expo SDK issue is not a permanent blocker — verify whether the
|
|
611
|
+
current versions still need it before carrying it forward. Historical Expo
|
|
612
|
+
issues expire; copy them forward only when they reproduce.
|
|
@@ -426,6 +426,7 @@ const { width } = useWindowDimensions();
|
|
|
426
426
|
For detailed documentation on specific topics, refer to:
|
|
427
427
|
|
|
428
428
|
- `references/official-docs.md` - Condensed official Expo Router documentation
|
|
429
|
+
- `references/static-export-verification.md` - Asserting enumerated routes, per-route titles, and initial HTML after `expo export`
|
|
429
430
|
- `scripts/generate-route.py` - Route scaffolding script
|
|
430
431
|
|
|
431
432
|
Official Documentation: https://docs.expo.dev/router/introduction/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Static Export Verification
|
|
2
|
+
|
|
3
|
+
When `expo export` runs with static output (`output: "static"` in app.json),
|
|
4
|
+
every route must produce an HTML file with the right title and the expected
|
|
5
|
+
initial markup. `expo export` succeeding only proves the bundle built — a
|
|
6
|
+
missing route, a wrong title, or an empty shell all ship green unless the
|
|
7
|
+
exported output itself is checked.
|
|
8
|
+
|
|
9
|
+
## Enumerate the dynamic routes
|
|
10
|
+
|
|
11
|
+
Static export prerenders each route it can enumerate. Dynamic segments need
|
|
12
|
+
`generateStaticParams` — an unenumerated `[id]` route produces no page at all.
|
|
13
|
+
After export, enumerate the routes the app *declares* and assert each one has
|
|
14
|
+
a file under `dist/`:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const DIST = path.resolve("dist");
|
|
21
|
+
|
|
22
|
+
// The routes the app owns — keep this in sync with app/, or derive it from
|
|
23
|
+
// the route manifest expo-router emits.
|
|
24
|
+
const EXPECTED_ROUTES = ["index", "settings", "players/[id]"];
|
|
25
|
+
|
|
26
|
+
for (const route of EXPECTED_ROUTES) {
|
|
27
|
+
const html = path.join(DIST, `${route}.html`);
|
|
28
|
+
expect(existsSync(html)).toBe(true);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Per-route titles
|
|
33
|
+
|
|
34
|
+
`<Stack.Screen options={{ title: "..." }}>` sets the native header title; the
|
|
35
|
+
web `<title>` comes from `Head` (`expo-router/head`). Assert the title the
|
|
36
|
+
exported HTML actually carries:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const html = readFileSync(path.join(DIST, "settings.html"), "utf8");
|
|
40
|
+
expect(html).toMatch(/<title>[^<]*Settings[^<]*<\/title>/);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Expected initial content
|
|
44
|
+
|
|
45
|
+
The exported page should ship its prerendered content, not a bare shell —
|
|
46
|
+
that is what makes static export worth shipping at all. Assert a stable marker
|
|
47
|
+
of the route's expected first paint:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
expect(html).toContain("Sign in to continue");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Keep the marker to content that exists in the static HTML, never something
|
|
54
|
+
hydration adds later.
|
|
@@ -478,3 +478,10 @@ Add className to component props:
|
|
|
478
478
|
```tsx
|
|
479
479
|
type Props = React.ComponentProps<typeof RNView> & { className?: string };
|
|
480
480
|
```
|
|
481
|
+
|
|
482
|
+
The same rule applies at every styling API boundary — `useCssElement`,
|
|
483
|
+
`cssInterop`, or `styled()`: type the wrapped component's props explicitly
|
|
484
|
+
(`React.ComponentProps<typeof X> & { className?: string }`) and pass props
|
|
485
|
+
through, rather than casting to a wider type. A cast compiles over a
|
|
486
|
+
prop-shape drift that an explicit type would refuse — the boundary is where
|
|
487
|
+
the drift is cheapest to catch.
|
|
@@ -394,6 +394,31 @@ Components from third-party libraries (e.g., `react-native-bouncy-checkbox`, `re
|
|
|
394
394
|
|
|
395
395
|
---
|
|
396
396
|
|
|
397
|
+
## Reveal-on-Focus Assertions
|
|
398
|
+
|
|
399
|
+
`toBeVisible()` proves the element exists and is not hidden — it does NOT
|
|
400
|
+
prove the element crossed onto the screen. A focused element scrolled off-view
|
|
401
|
+
still reports visible. For any "focus reveals the element" behavior (skip
|
|
402
|
+
links, focus-trapped modals, scroll-into-view containers), assert on layout,
|
|
403
|
+
not just visibility:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// Rejects when the element has no box at all (display: none, detached).
|
|
407
|
+
const box = await page.getByTestId("dialog:panel").boundingBox();
|
|
408
|
+
expect(box).not.toBeNull();
|
|
409
|
+
|
|
410
|
+
// Then prove it is actually inside the viewport — the part toBeVisible()
|
|
411
|
+
// cannot say.
|
|
412
|
+
await expect(page.getByTestId("dialog:panel")).toBeInViewport();
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Pair them: `boundingBox()` answers "is there a box" (null means never laid
|
|
416
|
+
out), `toBeInViewport()` answers "did it cross onto the screen". An assertion
|
|
417
|
+
that skips the null check reads a missing layout as a viewport miss, which is
|
|
418
|
+
a different bug.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
397
422
|
## Implementation Checklist
|
|
398
423
|
|
|
399
424
|
When adding E2E test coverage to a component:
|
|
@@ -588,3 +588,25 @@ afterAll(() => {
|
|
|
588
588
|
console.error = originalError;
|
|
589
589
|
});
|
|
590
590
|
```
|
|
591
|
+
|
|
592
|
+
## Migrating jest.mock Usage
|
|
593
|
+
|
|
594
|
+
When moving a test file onto the managed setup, scope the migration to the
|
|
595
|
+
file under your hand — a suite-wide mock sweep is where subtle behavior
|
|
596
|
+
changes hide.
|
|
597
|
+
|
|
598
|
+
- **Prefer renderer-compatible mocks.** A mock that works in the Node
|
|
599
|
+
environment but reads `window`, native modules, or ESM-only entry points
|
|
600
|
+
fails under the jsdom/renderer the Expo preset provides. Mock the seam your
|
|
601
|
+
component actually imports (`jest.mock("nativewind", ...)`,
|
|
602
|
+
`jest.mock("@/lib/env", ...)`) — never reach around it to mock a transitive
|
|
603
|
+
dependency.
|
|
604
|
+
- **Keep factories stable and hoisted.** Jest hoists `jest.mock` calls above
|
|
605
|
+
imports, so a factory cannot close over outer variables. Everything the
|
|
606
|
+
factory needs must come from `jest.requireActual`, `jest.requireMock`, or a
|
|
607
|
+
`mock`-prefixed name. A factory that silently returns a different shape per
|
|
608
|
+
run is the migration failure that surfaces as unrelated flakes.
|
|
609
|
+
- **Check version compatibility, not history.** A pinned workaround for an
|
|
610
|
+
old jest-expo/Expo SDK issue is not a permanent blocker — verify whether the
|
|
611
|
+
current versions still need it before carrying it forward. Historical Expo
|
|
612
|
+
issues expire; copy them forward only when they reproduce.
|
|
@@ -426,6 +426,7 @@ const { width } = useWindowDimensions();
|
|
|
426
426
|
For detailed documentation on specific topics, refer to:
|
|
427
427
|
|
|
428
428
|
- `references/official-docs.md` - Condensed official Expo Router documentation
|
|
429
|
+
- `references/static-export-verification.md` - Asserting enumerated routes, per-route titles, and initial HTML after `expo export`
|
|
429
430
|
- `scripts/generate-route.py` - Route scaffolding script
|
|
430
431
|
|
|
431
432
|
Official Documentation: https://docs.expo.dev/router/introduction/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Static Export Verification
|
|
2
|
+
|
|
3
|
+
When `expo export` runs with static output (`output: "static"` in app.json),
|
|
4
|
+
every route must produce an HTML file with the right title and the expected
|
|
5
|
+
initial markup. `expo export` succeeding only proves the bundle built — a
|
|
6
|
+
missing route, a wrong title, or an empty shell all ship green unless the
|
|
7
|
+
exported output itself is checked.
|
|
8
|
+
|
|
9
|
+
## Enumerate the dynamic routes
|
|
10
|
+
|
|
11
|
+
Static export prerenders each route it can enumerate. Dynamic segments need
|
|
12
|
+
`generateStaticParams` — an unenumerated `[id]` route produces no page at all.
|
|
13
|
+
After export, enumerate the routes the app *declares* and assert each one has
|
|
14
|
+
a file under `dist/`:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const DIST = path.resolve("dist");
|
|
21
|
+
|
|
22
|
+
// The routes the app owns — keep this in sync with app/, or derive it from
|
|
23
|
+
// the route manifest expo-router emits.
|
|
24
|
+
const EXPECTED_ROUTES = ["index", "settings", "players/[id]"];
|
|
25
|
+
|
|
26
|
+
for (const route of EXPECTED_ROUTES) {
|
|
27
|
+
const html = path.join(DIST, `${route}.html`);
|
|
28
|
+
expect(existsSync(html)).toBe(true);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Per-route titles
|
|
33
|
+
|
|
34
|
+
`<Stack.Screen options={{ title: "..." }}>` sets the native header title; the
|
|
35
|
+
web `<title>` comes from `Head` (`expo-router/head`). Assert the title the
|
|
36
|
+
exported HTML actually carries:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const html = readFileSync(path.join(DIST, "settings.html"), "utf8");
|
|
40
|
+
expect(html).toMatch(/<title>[^<]*Settings[^<]*<\/title>/);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Expected initial content
|
|
44
|
+
|
|
45
|
+
The exported page should ship its prerendered content, not a bare shell —
|
|
46
|
+
that is what makes static export worth shipping at all. Assert a stable marker
|
|
47
|
+
of the route's expected first paint:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
expect(html).toContain("Sign in to continue");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Keep the marker to content that exists in the static HTML, never something
|
|
54
|
+
hydration adds later.
|
|
@@ -478,3 +478,10 @@ Add className to component props:
|
|
|
478
478
|
```tsx
|
|
479
479
|
type Props = React.ComponentProps<typeof RNView> & { className?: string };
|
|
480
480
|
```
|
|
481
|
+
|
|
482
|
+
The same rule applies at every styling API boundary — `useCssElement`,
|
|
483
|
+
`cssInterop`, or `styled()`: type the wrapped component's props explicitly
|
|
484
|
+
(`React.ComponentProps<typeof X> & { className?: string }`) and pass props
|
|
485
|
+
through, rather than casting to a wider type. A cast compiles over a
|
|
486
|
+
prop-shape drift that an explicit type would refuse — the boundary is where
|
|
487
|
+
the drift is cheapest to catch.
|
|
@@ -394,6 +394,31 @@ Components from third-party libraries (e.g., `react-native-bouncy-checkbox`, `re
|
|
|
394
394
|
|
|
395
395
|
---
|
|
396
396
|
|
|
397
|
+
## Reveal-on-Focus Assertions
|
|
398
|
+
|
|
399
|
+
`toBeVisible()` proves the element exists and is not hidden — it does NOT
|
|
400
|
+
prove the element crossed onto the screen. A focused element scrolled off-view
|
|
401
|
+
still reports visible. For any "focus reveals the element" behavior (skip
|
|
402
|
+
links, focus-trapped modals, scroll-into-view containers), assert on layout,
|
|
403
|
+
not just visibility:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// Rejects when the element has no box at all (display: none, detached).
|
|
407
|
+
const box = await page.getByTestId("dialog:panel").boundingBox();
|
|
408
|
+
expect(box).not.toBeNull();
|
|
409
|
+
|
|
410
|
+
// Then prove it is actually inside the viewport — the part toBeVisible()
|
|
411
|
+
// cannot say.
|
|
412
|
+
await expect(page.getByTestId("dialog:panel")).toBeInViewport();
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Pair them: `boundingBox()` answers "is there a box" (null means never laid
|
|
416
|
+
out), `toBeInViewport()` answers "did it cross onto the screen". An assertion
|
|
417
|
+
that skips the null check reads a missing layout as a viewport miss, which is
|
|
418
|
+
a different bug.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
397
422
|
## Implementation Checklist
|
|
398
423
|
|
|
399
424
|
When adding E2E test coverage to a component:
|
|
@@ -588,3 +588,25 @@ afterAll(() => {
|
|
|
588
588
|
console.error = originalError;
|
|
589
589
|
});
|
|
590
590
|
```
|
|
591
|
+
|
|
592
|
+
## Migrating jest.mock Usage
|
|
593
|
+
|
|
594
|
+
When moving a test file onto the managed setup, scope the migration to the
|
|
595
|
+
file under your hand — a suite-wide mock sweep is where subtle behavior
|
|
596
|
+
changes hide.
|
|
597
|
+
|
|
598
|
+
- **Prefer renderer-compatible mocks.** A mock that works in the Node
|
|
599
|
+
environment but reads `window`, native modules, or ESM-only entry points
|
|
600
|
+
fails under the jsdom/renderer the Expo preset provides. Mock the seam your
|
|
601
|
+
component actually imports (`jest.mock("nativewind", ...)`,
|
|
602
|
+
`jest.mock("@/lib/env", ...)`) — never reach around it to mock a transitive
|
|
603
|
+
dependency.
|
|
604
|
+
- **Keep factories stable and hoisted.** Jest hoists `jest.mock` calls above
|
|
605
|
+
imports, so a factory cannot close over outer variables. Everything the
|
|
606
|
+
factory needs must come from `jest.requireActual`, `jest.requireMock`, or a
|
|
607
|
+
`mock`-prefixed name. A factory that silently returns a different shape per
|
|
608
|
+
run is the migration failure that surfaces as unrelated flakes.
|
|
609
|
+
- **Check version compatibility, not history.** A pinned workaround for an
|
|
610
|
+
old jest-expo/Expo SDK issue is not a permanent blocker — verify whether the
|
|
611
|
+
current versions still need it before carrying it forward. Historical Expo
|
|
612
|
+
issues expire; copy them forward only when they reproduce.
|
|
@@ -426,6 +426,7 @@ const { width } = useWindowDimensions();
|
|
|
426
426
|
For detailed documentation on specific topics, refer to:
|
|
427
427
|
|
|
428
428
|
- `references/official-docs.md` - Condensed official Expo Router documentation
|
|
429
|
+
- `references/static-export-verification.md` - Asserting enumerated routes, per-route titles, and initial HTML after `expo export`
|
|
429
430
|
- `scripts/generate-route.py` - Route scaffolding script
|
|
430
431
|
|
|
431
432
|
Official Documentation: https://docs.expo.dev/router/introduction/
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Static Export Verification
|
|
2
|
+
|
|
3
|
+
When `expo export` runs with static output (`output: "static"` in app.json),
|
|
4
|
+
every route must produce an HTML file with the right title and the expected
|
|
5
|
+
initial markup. `expo export` succeeding only proves the bundle built — a
|
|
6
|
+
missing route, a wrong title, or an empty shell all ship green unless the
|
|
7
|
+
exported output itself is checked.
|
|
8
|
+
|
|
9
|
+
## Enumerate the dynamic routes
|
|
10
|
+
|
|
11
|
+
Static export prerenders each route it can enumerate. Dynamic segments need
|
|
12
|
+
`generateStaticParams` — an unenumerated `[id]` route produces no page at all.
|
|
13
|
+
After export, enumerate the routes the app *declares* and assert each one has
|
|
14
|
+
a file under `dist/`:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
|
|
20
|
+
const DIST = path.resolve("dist");
|
|
21
|
+
|
|
22
|
+
// The routes the app owns — keep this in sync with app/, or derive it from
|
|
23
|
+
// the route manifest expo-router emits.
|
|
24
|
+
const EXPECTED_ROUTES = ["index", "settings", "players/[id]"];
|
|
25
|
+
|
|
26
|
+
for (const route of EXPECTED_ROUTES) {
|
|
27
|
+
const html = path.join(DIST, `${route}.html`);
|
|
28
|
+
expect(existsSync(html)).toBe(true);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Per-route titles
|
|
33
|
+
|
|
34
|
+
`<Stack.Screen options={{ title: "..." }}>` sets the native header title; the
|
|
35
|
+
web `<title>` comes from `Head` (`expo-router/head`). Assert the title the
|
|
36
|
+
exported HTML actually carries:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const html = readFileSync(path.join(DIST, "settings.html"), "utf8");
|
|
40
|
+
expect(html).toMatch(/<title>[^<]*Settings[^<]*<\/title>/);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Expected initial content
|
|
44
|
+
|
|
45
|
+
The exported page should ship its prerendered content, not a bare shell —
|
|
46
|
+
that is what makes static export worth shipping at all. Assert a stable marker
|
|
47
|
+
of the route's expected first paint:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
expect(html).toContain("Sign in to continue");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Keep the marker to content that exists in the static HTML, never something
|
|
54
|
+
hydration adds later.
|
|
@@ -478,3 +478,10 @@ Add className to component props:
|
|
|
478
478
|
```tsx
|
|
479
479
|
type Props = React.ComponentProps<typeof RNView> & { className?: string };
|
|
480
480
|
```
|
|
481
|
+
|
|
482
|
+
The same rule applies at every styling API boundary — `useCssElement`,
|
|
483
|
+
`cssInterop`, or `styled()`: type the wrapped component's props explicitly
|
|
484
|
+
(`React.ComponentProps<typeof X> & { className?: string }`) and pass props
|
|
485
|
+
through, rather than casting to a wider type. A cast compiles over a
|
|
486
|
+
prop-shape drift that an explicit type would refuse — the boundary is where
|
|
487
|
+
the drift is cheapest to catch.
|
|
@@ -394,6 +394,31 @@ Components from third-party libraries (e.g., `react-native-bouncy-checkbox`, `re
|
|
|
394
394
|
|
|
395
395
|
---
|
|
396
396
|
|
|
397
|
+
## Reveal-on-Focus Assertions
|
|
398
|
+
|
|
399
|
+
`toBeVisible()` proves the element exists and is not hidden — it does NOT
|
|
400
|
+
prove the element crossed onto the screen. A focused element scrolled off-view
|
|
401
|
+
still reports visible. For any "focus reveals the element" behavior (skip
|
|
402
|
+
links, focus-trapped modals, scroll-into-view containers), assert on layout,
|
|
403
|
+
not just visibility:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// Rejects when the element has no box at all (display: none, detached).
|
|
407
|
+
const box = await page.getByTestId("dialog:panel").boundingBox();
|
|
408
|
+
expect(box).not.toBeNull();
|
|
409
|
+
|
|
410
|
+
// Then prove it is actually inside the viewport — the part toBeVisible()
|
|
411
|
+
// cannot say.
|
|
412
|
+
await expect(page.getByTestId("dialog:panel")).toBeInViewport();
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Pair them: `boundingBox()` answers "is there a box" (null means never laid
|
|
416
|
+
out), `toBeInViewport()` answers "did it cross onto the screen". An assertion
|
|
417
|
+
that skips the null check reads a missing layout as a viewport miss, which is
|
|
418
|
+
a different bug.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
397
422
|
## Implementation Checklist
|
|
398
423
|
|
|
399
424
|
When adding E2E test coverage to a component:
|
|
@@ -588,3 +588,25 @@ afterAll(() => {
|
|
|
588
588
|
console.error = originalError;
|
|
589
589
|
});
|
|
590
590
|
```
|
|
591
|
+
|
|
592
|
+
## Migrating jest.mock Usage
|
|
593
|
+
|
|
594
|
+
When moving a test file onto the managed setup, scope the migration to the
|
|
595
|
+
file under your hand — a suite-wide mock sweep is where subtle behavior
|
|
596
|
+
changes hide.
|
|
597
|
+
|
|
598
|
+
- **Prefer renderer-compatible mocks.** A mock that works in the Node
|
|
599
|
+
environment but reads `window`, native modules, or ESM-only entry points
|
|
600
|
+
fails under the jsdom/renderer the Expo preset provides. Mock the seam your
|
|
601
|
+
component actually imports (`jest.mock("nativewind", ...)`,
|
|
602
|
+
`jest.mock("@/lib/env", ...)`) — never reach around it to mock a transitive
|
|
603
|
+
dependency.
|
|
604
|
+
- **Keep factories stable and hoisted.** Jest hoists `jest.mock` calls above
|
|
605
|
+
imports, so a factory cannot close over outer variables. Everything the
|
|
606
|
+
factory needs must come from `jest.requireActual`, `jest.requireMock`, or a
|
|
607
|
+
`mock`-prefixed name. A factory that silently returns a different shape per
|
|
608
|
+
run is the migration failure that surfaces as unrelated flakes.
|
|
609
|
+
- **Check version compatibility, not history.** A pinned workaround for an
|
|
610
|
+
old jest-expo/Expo SDK issue is not a permanent blocker — verify whether the
|
|
611
|
+
current versions still need it before carrying it forward. Historical Expo
|
|
612
|
+
issues expire; copy them forward only when they reproduce.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.66.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.66.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.66.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.66.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.66.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|