From 2d582400ef6353cc47855d7bade3b2c5b7813108 Mon Sep 17 00:00:00 2001 From: Carson Holgate Date: Fri, 13 Mar 2026 13:50:24 -0700 Subject: [PATCH 01/18] Add polyglot widgets (DatePicker, TimePicker, IndexPicker, EmojiLabel), widgets catalog, and per-component Bazel targets - Add native implementations for DatePicker, TimePicker, IndexPicker, EmojiLabel on iOS, macOS, Android, and web - Add WidgetsCatalog playground screen with live-updating demo of each polyglot widget - Add "Browse Widgets Catalog" button in Playground that opens/closes the catalog view - Refactor widgets/BUILD.bazel with named filegroup targets per component (:animation, :button, :pickers, etc.) - Move Android Kotlin binders from nested com/snap/widgets/pickers/ to flat android/ directory - Point WORKSPACE at bleeding edge of public Valdi GitHub (d0cd062) - Add enable_web=true to .bazelrc for Valdi compiler JS output - Add web playground app (webpack) and bazel_web_serve.sh script - Add Share module scaffold (iOS, Android, web, TypeScript) - Add Cursor rules for polyglot modules, custom views, and web polyglot patterns Co-Authored-By: Claude Sonnet 4.6 --- .bazelrc | 7 +- .cursor/rules/README.md | 15 +- .cursor/rules/bazel.md | 80 +- .cursor/rules/custom-view.md | 40 + .cursor/rules/polyglot-module.md | 72 + .cursor/rules/testing.md | 67 +- .cursor/rules/typescript-tsx.md | 145 +- .cursor/rules/web-polyglot.md | 60 + .cursorrules | 51 + .gitignore | 3 + WORKSPACE | 19 +- docs/android-internal-valdi.md | 31 + docs/polyglot-example-suggestions.md | 89 + scripts/bazel_web_serve.sh | 49 + valdi_modules/playground/BUILD.bazel | 14 +- valdi_modules/playground/src/Playground.tsx | 33 +- .../playground/src/WidgetsCatalog.tsx | 87 + .../playground/web_app/package-lock.json | 10222 ++++++++++++++++ valdi_modules/playground/web_app/package.json | 24 + valdi_modules/playground/web_app/src/App.js | 35 + .../playground/web_app/src/index.html | 31 + valdi_modules/playground/web_app/src/index.js | 6 + .../playground/web_app/webpack.config.js | 56 + valdi_modules/share/BUILD.bazel | 60 + valdi_modules/share/android/README.md | 5 + .../share/ShareNativeModuleFactoryImpl.kt | 24 + .../java/com/snap/valdi/share/ShareHelper.kt | 32 + valdi_modules/share/ios/README.md | 8 + valdi_modules/share/ios/ShareHelper.h | 15 + valdi_modules/share/ios/ShareHelper.m | 60 + valdi_modules/share/ios/ShareHelper.swift | 33 + .../share/ios/ShareNativeModuleImpl.m | 29 + valdi_modules/share/src/Share.ts | 14 + valdi_modules/share/src/ShareNative.d.ts | 22 + valdi_modules/share/src/index.ts | 1 + valdi_modules/share/tsconfig.json | 1 + valdi_modules/share/web/src/ShareWeb.ts | 48 + valdi_modules/share/web/tsconfig.json | 1 + valdi_modules/widgets/BUILD.bazel | 390 +- .../ValdiDatePickerAttributesBinder.kt | 85 + .../ValdiIndexPickerAttributesBinder.kt | 62 + .../ValdiTimePickerAttributesBinder.kt | 85 + .../widgets/ios/SCWidgetsDatePicker.h | 16 + .../widgets/ios/SCWidgetsDatePicker.m | 173 + .../widgets/ios/SCWidgetsDatePickerUtils.h | 14 + .../widgets/ios/SCWidgetsDatePickerUtils.m | 34 + .../widgets/ios/SCWidgetsDateTimePicker.h | 9 + .../widgets/ios/SCWidgetsDateTimePicker.m | 126 + .../widgets/ios/SCWidgetsIndexPicker.h | 16 + .../widgets/ios/SCWidgetsIndexPicker.m | 174 + valdi_modules/widgets/ios/SCWidgetsLabel.h | 26 + valdi_modules/widgets/ios/SCWidgetsLabel.m | 673 + .../widgets/ios/SCWidgetsTimePicker.h | 16 + .../widgets/ios/SCWidgetsTimePicker.m | 197 + .../widgets/macos/SCWidgetsMacOSDatePicker.h | 17 + .../widgets/macos/SCWidgetsMacOSDatePicker.m | 71 + .../widgets/macos/SCWidgetsMacOSIndexPicker.h | 18 + .../widgets/macos/SCWidgetsMacOSIndexPicker.m | 76 + .../widgets/macos/SCWidgetsMacOSLabel.h | 18 + .../widgets/macos/SCWidgetsMacOSLabel.m | 74 + .../widgets/macos/SCWidgetsMacOSTimePicker.h | 17 + .../widgets/macos/SCWidgetsMacOSTimePicker.m | 98 + .../src/components/pickers/DatePicker.tsx | 4 +- .../src/components/pickers/IndexPicker.tsx | 4 +- .../src/components/pickers/TimePicker.tsx | 4 +- .../src/components/text/EmojiLabel.tsx | 4 +- valdi_modules/widgets/web/src/WidgetsWeb.js | 101 + 67 files changed, 14124 insertions(+), 67 deletions(-) create mode 100644 .cursor/rules/custom-view.md create mode 100644 .cursor/rules/polyglot-module.md create mode 100644 .cursor/rules/web-polyglot.md create mode 100644 .cursorrules create mode 100644 docs/android-internal-valdi.md create mode 100644 docs/polyglot-example-suggestions.md create mode 100755 scripts/bazel_web_serve.sh create mode 100644 valdi_modules/playground/src/WidgetsCatalog.tsx create mode 100644 valdi_modules/playground/web_app/package-lock.json create mode 100644 valdi_modules/playground/web_app/package.json create mode 100644 valdi_modules/playground/web_app/src/App.js create mode 100644 valdi_modules/playground/web_app/src/index.html create mode 100644 valdi_modules/playground/web_app/src/index.js create mode 100644 valdi_modules/playground/web_app/webpack.config.js create mode 100644 valdi_modules/share/BUILD.bazel create mode 100644 valdi_modules/share/android/README.md create mode 100644 valdi_modules/share/android/src/main/java/com/snap/valdi/modules/share/ShareNativeModuleFactoryImpl.kt create mode 100644 valdi_modules/share/android/src/main/java/com/snap/valdi/share/ShareHelper.kt create mode 100644 valdi_modules/share/ios/README.md create mode 100644 valdi_modules/share/ios/ShareHelper.h create mode 100644 valdi_modules/share/ios/ShareHelper.m create mode 100644 valdi_modules/share/ios/ShareHelper.swift create mode 100644 valdi_modules/share/ios/ShareNativeModuleImpl.m create mode 100644 valdi_modules/share/src/Share.ts create mode 100644 valdi_modules/share/src/ShareNative.d.ts create mode 100644 valdi_modules/share/src/index.ts create mode 100644 valdi_modules/share/tsconfig.json create mode 100644 valdi_modules/share/web/src/ShareWeb.ts create mode 100644 valdi_modules/share/web/tsconfig.json create mode 100644 valdi_modules/widgets/android/ValdiDatePickerAttributesBinder.kt create mode 100644 valdi_modules/widgets/android/ValdiIndexPickerAttributesBinder.kt create mode 100644 valdi_modules/widgets/android/ValdiTimePickerAttributesBinder.kt create mode 100644 valdi_modules/widgets/ios/SCWidgetsDatePicker.h create mode 100644 valdi_modules/widgets/ios/SCWidgetsDatePicker.m create mode 100644 valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.h create mode 100644 valdi_modules/widgets/ios/SCWidgetsDatePickerUtils.m create mode 100644 valdi_modules/widgets/ios/SCWidgetsDateTimePicker.h create mode 100644 valdi_modules/widgets/ios/SCWidgetsDateTimePicker.m create mode 100644 valdi_modules/widgets/ios/SCWidgetsIndexPicker.h create mode 100644 valdi_modules/widgets/ios/SCWidgetsIndexPicker.m create mode 100644 valdi_modules/widgets/ios/SCWidgetsLabel.h create mode 100644 valdi_modules/widgets/ios/SCWidgetsLabel.m create mode 100644 valdi_modules/widgets/ios/SCWidgetsTimePicker.h create mode 100644 valdi_modules/widgets/ios/SCWidgetsTimePicker.m create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.h create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSDatePicker.m create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.h create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSIndexPicker.m create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSLabel.h create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSLabel.m create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.h create mode 100644 valdi_modules/widgets/macos/SCWidgetsMacOSTimePicker.m create mode 100644 valdi_modules/widgets/web/src/WidgetsWeb.js diff --git a/.bazelrc b/.bazelrc index 22d39f0..78fb5d3 100644 --- a/.bazelrc +++ b/.bazelrc @@ -82,4 +82,9 @@ build --experimental_reuse_sandbox_directories # Valdi Open Source Flags build --define=open_source_build=true -build --android_crosstool_top="@snap_client_toolchains//:android_crosstool" \ No newline at end of file +# Enable web compilation (generates JS outputs from Valdi compiler for web targets) +common --define=enable_web=true + +build --android_crosstool_top="@snap_client_toolchains//:android_crosstool" +# Web build configuration (used by scripts/bazel_web_serve.sh) +build:web --build_tag_filters=web diff --git a/.cursor/rules/README.md b/.cursor/rules/README.md index c802020..88f06a5 100644 --- a/.cursor/rules/README.md +++ b/.cursor/rules/README.md @@ -8,17 +8,24 @@ Rules load based on what you're editing: - `valdi_modules/**/*.ts`, `valdi_modules/**/*.tsx` → **typescript-tsx.md** - `**/BUILD.bazel`, `**/*.bzl`, `WORKSPACE` → **bazel.md** -- `**/test/**/*.ts`, `**/*.spec.ts` → **testing.md** +- `**/test/**/*.ts`, `**/*Test.ts` → **testing.md** +- `**/*.tsx` using `` → **custom-view.md** +- `**/android/`, `**/ios/`, `**/web/` platform dirs → **polyglot-module.md** +- `**/web/**/*.ts` in modules → **web-polyglot.md** ## Rules | File | Applies To | Description | |------|-----------|-------------| -| `typescript-tsx.md` | `valdi_modules/**/*.ts`, `valdi_modules/**/*.tsx` | Valdi component patterns, anti-React warnings | -| `bazel.md` | `**/BUILD.bazel`, `**/*.bzl`, `WORKSPACE` | Bazel conventions | -| `testing.md` | `**/test/**/*.ts`, `**/*.spec.ts` | Testing (Jasmine) | +| `typescript-tsx.md` | `valdi_modules/**/*.ts`, `**/*.tsx` | Valdi component patterns, anti-React warnings, styling, events | +| `bazel.md` | `**/BUILD.bazel`, `**/*.bzl`, `WORKSPACE` | Bazel conventions, valdi_module, polyglot targets | +| `testing.md` | `**/test/**/*.ts`, `**/*Test.ts` | Testing (Jasmine), `*Test.ts` naming | +| `polyglot-module.md` | `**/BUILD.bazel`, `android/`, `ios/`, `web/` dirs | Polyglot module structure, native deps, BUILD patterns | +| `custom-view.md` | `**/*.tsx` using `` | `` element, class attributes, platform resolution | +| `web-polyglot.md` | `**/web/**/*.ts` in modules | Web polyglot entry, `webPolyglotViews` export | ## More - Valdi: https://github.com/Snapchat/Valdi - Repo README: `/README.md` +- AGENTS.md: `/AGENTS.md` diff --git a/.cursor/rules/bazel.md b/.cursor/rules/bazel.md index dafd9b8..990c40b 100644 --- a/.cursor/rules/bazel.md +++ b/.cursor/rules/bazel.md @@ -2,10 +2,6 @@ **Applies to**: `BUILD.bazel`, `*.bzl` files, `WORKSPACE` -## Overview - -Valdi Widgets uses Bazel as its build system, with Valdi as an external dependency (`@valdi//...`). - ## Key Commands ```bash @@ -17,36 +13,86 @@ bazel test //valdi_modules/widgets:test //valdi_modules/navigation:test //valdi_ # Build specific module bazel build //valdi_modules/widgets:widgets + +# Build for web +bazel build --config=web //valdi_modules/playground:playground_export_npm ``` ## Valdi Dependency - Valdi is loaded via `http_archive` in WORKSPACE (release tag, e.g. `beta-0.0.2`). +- For local development: use `local_repository(name = "valdi", path = "/path/to/Valdi")` in WORKSPACE. - Valdi build rules live in `@valdi//bzl/valdi/`. -- Custom rules: `valdi_module`, `valdi_application` (from Valdi). -## Conventions +## Valdi Module -### File Naming +```python +load("@valdi//bzl/valdi:valdi_module.bzl", "valdi_module") -- `BUILD.bazel` not `BUILD` (explicit extension) -- `.bzl` for Starlark macros and rules +valdi_module( + name = "my_module", + srcs = glob(["src/**/*.ts", "src/**/*.tsx"]) + ["tsconfig.json"], + deps = [ + "@valdi//src/valdi_modules/src/valdi/valdi_core", + "@valdi//src/valdi_modules/src/valdi/valdi_tsx", + ], + visibility = ["//visibility:public"], +) +``` -### Targets +## Polyglot Module (Native + Web) + +```python +load("@valdi//bzl/valdi:valdi_module.bzl", "valdi_module") +load("@valdi//bzl/valdi:valdi_android_library.bzl", "valdi_android_library") + +# Android: attribute binders, annotated with @RegisterAttributesBinder +valdi_android_library( + name = "my_module_android", + srcs = glob(["android/**/*.kt"]), + deps = ["@valdi//valdi:valdi_java"], +) + +# iOS: native view implementations +objc_library( + name = "my_module_ios", + srcs = glob(["ios/**/*.m"]), + hdrs = glob(["ios/**/*.h"]), + sdk_frameworks = ["UIKit"], + deps = ["@valdi//valdi:valdi_ios"], +) + +# Web: compiled by ts_project (NOT valdi compiler); exports webPolyglotViews +ts_project( + name = "my_module_web", + srcs = glob(["web/**/*.ts"]), + tsconfig = "web/tsconfig.json", +) + +valdi_module( + name = "my_module", + srcs = glob(["src/**/*.ts", "src/**/*.tsx"]) + ["tsconfig.json"], + android_deps = [":my_module_android"], + ios_deps = [":my_module_ios"], + web_deps = [":my_module_web"], + deps = [...], +) +``` -- Use descriptive target names -- One main target per BUILD file usually matches directory name +See `valdi_modules/share/` for a full working example. See `.cursor/rules/polyglot-module.md` for details. -### Dependencies +## Conventions -- Be explicit about dependencies -- Use `@valdi//...` for Valdi module deps (e.g. `@valdi//src/valdi_modules/src/valdi/valdi_core`) -- Use visibility to control access +- `BUILD.bazel` not `BUILD` +- `.bzl` for Starlark macros +- Use `@valdi//...` for all Valdi module deps +- Be explicit about dependencies; don't rely on transitive deps implicitly +- Use `visibility = ["//visibility:public"]` for modules consumed by others ## Configuration - `.bazelrc` – Build flags and configurations -- `WORKSPACE` – Workspace and repository configuration +- `WORKSPACE` – External repository setup ## More Information diff --git a/.cursor/rules/custom-view.md b/.cursor/rules/custom-view.md new file mode 100644 index 0000000..423abd3 --- /dev/null +++ b/.cursor/rules/custom-view.md @@ -0,0 +1,40 @@ +# Custom View Rules + +**Applies to**: `**/*.tsx` files using `` elements. + +## `` Element + +`` renders a platform-native view inside a Valdi component. Each platform resolves the view by class name. + +```typescript + +``` + +## Class Attributes + +| Attribute | Platform | Resolution | +|-----------|----------|------------| +| `androidClass` | Android | Reflection via `ReflectionViewFactory`; needs `@RegisterAttributesBinder` | +| `iosClass` | iOS | ObjC class name; must be linked via `ios_deps` | +| `macosClass` | macOS | `NSClassFromString()`; must be linked via `macos_deps` | +| `webClass` | Web | Looked up in `WebViewClassRegistry`; registered via `webPolyglotViews` export | + +## Platform Discovery + +- **Android**: The view class needs a single-arg `(Context)` constructor. An `@RegisterAttributesBinder`-annotated binder is discovered from assets at runtime. +- **iOS**: The ObjC class must be an `NSView`/`UIView` subclass linked into the binary. +- **macOS**: Same as iOS but with `NSView` subclass. +- **Web**: The `webClass` name is matched against the `WebViewClassRegistry`. Register by exporting `webPolyglotViews` from a web polyglot entry file (see `web-polyglot.md` rule). + +## Common Mistakes + +- Using `` without checking the platform — wrap in `Device.isAndroid()` / `Device.isWeb()` etc. if not all platforms are supported +- Forgetting to link native implementations — the class name string alone isn't enough; the native code must be compiled and linked via platform `_deps` in BUILD.bazel +- Wrong package name in `androidClass` — must match the Kotlin/Java package exactly diff --git a/.cursor/rules/polyglot-module.md b/.cursor/rules/polyglot-module.md new file mode 100644 index 0000000..fc9bc3f --- /dev/null +++ b/.cursor/rules/polyglot-module.md @@ -0,0 +1,72 @@ +# Polyglot Modules (Valdi) + +Follow the **official Valdi polyglot docs** for the native/TS contract and Bazel wiring: + +**[Polyglot Modules (native-polyglot.md)](https://github.com/Snapchat/Valdi/blob/main/docs/docs/native-polyglot.md)** + +Use this rule when editing or creating polyglot modules that expose a TypeScript API backed by iOS/Android (or C++) implementations in this repo. + +## Official pattern (summary) + +- **TypeScript API**: Declare the module in a **`.d.ts`** file with **`@ExportModule`**. The Valdi compiler generates Objective-C, Swift, and Kotlin (and C++) bindings. Example: `src/ShareNative.d.ts` with `@ExportModule` and exported functions/interfaces. +- **Bazel**: The module’s **`valdi_module()`** target uses: + - **`android_deps`** – list of Android implementation targets (e.g. `valdi_android_library`). + - **`ios_deps`** – list of iOS implementation targets (e.g. `objc_library` or `apple_library`). + - **`native_deps`** – list of `cc_library` for cross-platform C++ (optional). +- **Android**: Use **`valdi_android_library`** (from `@valdi//bzl/valdi:valdi_android_library.bzl`) with `deps = [":_api_kt", "@valdi//valdi:valdi_java"]`. Implement the generated Kotlin API; annotate the **factory** class with **`@RegisterValdiModule`** and implement `onLoadModule()`. +- **iOS**: Use **`objc_library`** with `deps = [":_api_objc", "@valdi//valdi_core:valdi_core"]`. Implement the generated Objective-C API; in the factory implementation use **`VALDI_REGISTER_MODULE()`** and implement `onLoadModule`. +- **C++** (optional): Use **`cc_library`** with **`alwayslink = 1`** and `deps = [":_cpp"]`. Bindings are hand-written; register with `RegisterModuleFactory::registerTyped<...>()`. + +Reference implementation in this repo: **`valdi_modules/share/`** (ShareNative.d.ts, BUILD.bazel with `android_deps` / `ios_deps`, `valdi_android_library`, `objc_library`). + +## Two patterns in this repo + +### 1. "Calls down to native" (no custom-view) + +- One TS API: native bridge on iOS/Android, **web APIs** on web. +- **Example:** `valdi_modules/share/` – `Share.share({ title?, text?, url? })`. +- **TS:** Branch on `Device.isIOS()` / `Device.isAndroid()`. On native, call the native module (generated from the `.d.ts`); on web, use browser APIs (e.g. `navigator.share()` or clipboard). +- **Native:** Implement the generated API (Android Kotlin factory + impl, iOS Obj-C factory + impl). No custom views. +- **Web:** Implement in e.g. `web/src/ShareWeb.ts` and wire from TS when not native. Use **`web_deps`** on `valdi_module` for web build. + +### 2. "Includes a custom-view" + +- A **component** that renders **``** on iOS/Android and a **TSX fallback** on web. +- **Example:** Pickers, EmojiLabel; or standalone polyglot module (e.g. tooltip). +- **TS:** In `onRender()`, branch on platform; if native, render ``; else render TSX fallback. +- **Native:** Implement the view class; ensure the app build includes it so the runtime can instantiate by class name. Document in `android/` and `ios/` READMEs. + +## Recommended structure + +``` +valdi_modules// + BUILD.bazel # valdi_module( android_deps, ios_deps, [native_deps], web_deps? ) per native-polyglot.md + tsconfig.json + src/ + index.ts + .ts # or .tsx; may import from .d.ts + .d.ts # @ExportModule API for native (optional for view-only polyglot) + android/ # valdi_android_library srcs + src/main/java/.../...Impl.kt # @RegisterValdiModule factory + impl + ios/ # objc_library srcs + ...Impl.m # VALDI_REGISTER_MODULE() factory + impl + web/ # optional; web_deps filegroup + *.ts +``` + +## Importing polyglot modules (consumers) + +- The Valdi compiler may **not** resolve bare module names like `'share'` or `'tooltip'`. +- Use the **path to the entry file**: e.g. `import { Share } from 'share/src/Share';` (not `from 'share'`). + +## Playground integration + +- Add the module to **`playground/BUILD.bazel`** in `deps`: `"//valdi_modules/"`. +- Use it in a **Section** or the **Polyglot** tab. Run: `bazel build //valdi_modules/playground:app_macos && ./bazel-bin/valdi_modules/playground/app_macos_bin` or `./scripts/bazel_macos_run.sh`. + +## Core rules (all polyglot code) + +- Valdi is NOT React. No functional components or hooks. +- Components are class-based; `onRender()` returns void; JSX is a statement, not `return`ed. +- Prefer existing widgets as the TSX fallback for web when using a custom-view. +- For native views, use `` with the correct platform class names. diff --git a/.cursor/rules/testing.md b/.cursor/rules/testing.md index 95af340..abc5f14 100644 --- a/.cursor/rules/testing.md +++ b/.cursor/rules/testing.md @@ -1,15 +1,13 @@ # Testing Rules -**Applies to**: Test files in `**/test/`, `**/*.spec.ts`, `**/*.test.ts` +**Applies to**: Test files in `**/test/**/*.ts`, `**/*Test.ts` ## Overview -Valdi Widgets uses the same testing approach as Valdi: Jasmine for TypeScript/component tests. +Valdi Widgets uses Jasmine for TypeScript/component tests. Test files live in `test/` directories within each module and are named `*Test.ts` (e.g., `CoreButtonTest.ts`, `EmojiLabelTest.ts`). ## Test Framework -### Jasmine for TypeScript Tests - ```typescript import 'jasmine/src/jasmine'; import { Component } from 'valdi_core/src/Component'; @@ -19,7 +17,7 @@ describe('MyComponent', () => { const component = new MyComponent(); expect(component).toBeDefined(); }); - + it('should handle state updates', () => { const component = new MyStatefulComponent(); component.setState({ count: 1 }); @@ -28,31 +26,64 @@ describe('MyComponent', () => { }); ``` -Test files use `.spec.ts` and live under `test/` in each module. - ## Running Tests ```bash -# Run Valdi Widgets tests +# Run all widget tests bazel test //valdi_modules/widgets:test //valdi_modules/navigation:test //valdi_modules/valdi_standalone_ui:test //valdi_modules/navigation_internal:test -# With output +# With output on failure bazel test //valdi_modules/...:test --test_output=errors + +# Single module +bazel test //valdi_modules/widgets:test ``` ## Test Conventions -- `*.spec.ts` or `*.test.ts` for unit tests -- `test/` directory per module -- Test file should mirror source file name +- **Naming**: `*Test.ts` (e.g., `CoreButtonTest.ts`, `SliderTest.ts`) +- **Location**: `test/` subdirectory mirroring `src/` structure +- **Framework**: Jasmine (`describe`, `it`, `expect`, `beforeEach`, `afterEach`) + +## Test Structure + +```typescript +describe('ComponentName', () => { + beforeEach(() => { + // Setup + }); + + afterEach(() => { + // Cleanup + }); + + it('should do something specific', () => { + // Arrange + const component = new MyComponent(); + // Act + component.doSomething(); + // Assert + expect(component.result).toBe(expected); + }); +}); +``` + +## Testing State + +```typescript +it('should update state correctly', () => { + const component = new MyStatefulComponent(); + component.setState({ count: 1 }); + expect(component.state.count).toBe(1); +}); +``` -## Important +## Important Testing Principles -1. **Test behavior, not implementation** -2. **Isolate tests** – Each test independent -3. **Mock dependencies** when appropriate -4. **Use this.setTimeoutDisposable()** in component code; avoid raw setTimeout/setInterval in tests when testing components +1. **Test behavior, not implementation** – Focus on what the component does, not how +2. **Isolate tests** – Each test should be independent +3. **Use `this.setTimeoutDisposable()`** in component code; avoid raw `setTimeout`/`setInterval` in components ## More Information -- Valdi testing: https://github.com/Snapchat/Valdi +- Valdi: https://github.com/Snapchat/Valdi diff --git a/.cursor/rules/typescript-tsx.md b/.cursor/rules/typescript-tsx.md index 4b84496..80679b0 100644 --- a/.cursor/rules/typescript-tsx.md +++ b/.cursor/rules/typescript-tsx.md @@ -30,15 +30,15 @@ import { StatefulComponent } from 'valdi_core/src/Component'; class MyComponent extends StatefulComponent { state = { count: 0 }; - + onCreate() { } // Component created onViewModelUpdate(prev: ViewModel) { } // Props changed onDestroy() { } // Before removal - + handleClick = () => { this.setState({ count: this.state.count + 1 }); // Auto re-renders }; - + onRender() { // Returns void, not JSX!