From fc0a5974777920e271710e2ec8e96dca526e188e Mon Sep 17 00:00:00 2001 From: vikasrao23 Date: Fri, 20 Feb 2026 20:49:02 -0800 Subject: [PATCH 1/5] feat: add integration tests for React generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements integration test coverage for the React generator following the standardized Dagger-based testing pattern. Changes: - test/integration/cmd/react/run.go - Dagger test runner - test/react-integration/package.json - npm configuration - test/react-integration/tsconfig.json - TypeScript config - test/react-integration/src/test.ts - Hook structure validation - test/integration/cmd/run.go - Added React test to main runner - Makefile - Added test-integration-react target The test: 1. Builds the CLI from source 2. Generates React/TypeScript hooks 3. Compiles with TypeScript 4. Validates hook API structure (useFlag, useFlagWithDetails, getKey) 5. Verifies all 5 flag types compile correctly Note: Full runtime testing of hooks requires React environment (browser/JSDOM). This test validates compilation and API structure. Acceptance criteria met: ✅ React generator covered by integration test ✅ Generated code compiles successfully ✅ Test integrated into CI suite ✅ Follows documented integration testing structure Closes #118 Signed-off-by: vikasrao23 --- Makefile | 6 ++ test/integration/cmd/react/run.go | 99 ++++++++++++++++++++++++++++ test/integration/cmd/run.go | 9 ++- test/react-integration/package.json | 19 ++++++ test/react-integration/src/test.ts | 69 +++++++++++++++++++ test/react-integration/tsconfig.json | 18 +++++ 6 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 test/integration/cmd/react/run.go create mode 100644 test/react-integration/package.json create mode 100644 test/react-integration/src/test.ts create mode 100644 test/react-integration/tsconfig.json diff --git a/Makefile b/Makefile index b889d85..be9ed6c 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ help: @echo " test-integration-csharp - Run C# integration tests" @echo " test-integration-go - Run Go integration tests" @echo " test-integration-nodejs - Run NodeJS integration tests" + @echo " test-integration-react - Run React integration tests" @echo " generate - Generate all code (API clients, docs, schema)" @echo " generate-api - Generate API clients from OpenAPI specs" @echo " generate-docs - Generate documentation" @@ -67,6 +68,11 @@ test-integration-angular: @echo "Running Angular integration test with Dagger..." @go run ./test/integration/cmd/angular/run.go +.PHONY: test-integration-react +test-integration-react: + @echo "Running React integration test with Dagger..." + @go run ./test/integration/cmd/react/run.go + .PHONY: test-integration test-integration: @echo "Running all integration tests with Dagger..." diff --git a/test/integration/cmd/react/run.go b/test/integration/cmd/react/run.go new file mode 100644 index 0000000..fb50b62 --- /dev/null +++ b/test/integration/cmd/react/run.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "dagger.io/dagger" + "github.com/open-feature/cli/test/integration" +) + +// Test implements the integration test for the React generator +type Test struct { + // ProjectDir is the absolute path to the root of the project + ProjectDir string + // TestDir is the absolute path to the test directory + TestDir string +} + +// New creates a new Test +func New(projectDir, testDir string) *Test { + return &Test{ + ProjectDir: projectDir, + TestDir: testDir, + } +} + +// Run executes the React integration test using Dagger +func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) { + // Source code container + source := client.Host().Directory(t.ProjectDir) + testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{ + Include: []string{"package.json", "tsconfig.json", "src/**/*.ts", "src/**/*.tsx"}, + }) + + // Build the CLI + cli := client.Container(). + From("golang:1.24-alpine"). + WithExec([]string{"apk", "add", "--no-cache", "git"}). + WithDirectory("/src", source). + WithWorkdir("/src"). + WithExec([]string{"go", "mod", "tidy"}). + WithExec([]string{"go", "mod", "download"}). + WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"}) + + // Generate React client + generated := cli.WithExec([]string{ + "./cli", "generate", "react", + "--manifest=/src/sample/sample_manifest.json", + "--output=/tmp/generated", + }) + + // Get generated files + generatedFiles := generated.Directory("/tmp/generated") + + // Test React compilation with the generated files + reactContainer := client.Container(). + From("node:20-alpine"). + WithWorkdir("/app"). + WithDirectory("/app", testFiles). + WithDirectory("/app/src/generated", generatedFiles). + WithExec([]string{"npm", "install"}). + WithExec([]string{"npm", "run", "build"}). + WithExec([]string{"node", "dist/test.js"}) + + return reactContainer, nil +} + +// Name returns the name of the integration test +func (t *Test) Name() string { + return "react" +} + +func main() { + ctx := context.Background() + + // Get project root + projectDir, err := filepath.Abs(os.Getenv("PWD")) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err) + os.Exit(1) + } + + // Get test directory + testDir, err := filepath.Abs(filepath.Join(projectDir, "test/react-integration")) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to get test dir: %v\n", err) + os.Exit(1) + } + + // Create and run the React integration test + test := New(projectDir, testDir) + + if err := integration.RunTest(ctx, test); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/test/integration/cmd/run.go b/test/integration/cmd/run.go index 5923c26..9d7ad2b 100644 --- a/test/integration/cmd/run.go +++ b/test/integration/cmd/run.go @@ -45,7 +45,14 @@ func main() { os.Exit(1) } - // Add more tests here as they are available + // Run the React integration test + reactCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/react") + reactCmd.Stdout = os.Stdout + reactCmd.Stderr = os.Stderr + if err := reactCmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running React integration test: %v\n", err) + os.Exit(1) + } fmt.Println("=== All integration tests passed successfully ===") } diff --git a/test/react-integration/package.json b/test/react-integration/package.json new file mode 100644 index 0000000..9ffea5c --- /dev/null +++ b/test/react-integration/package.json @@ -0,0 +1,19 @@ +{ + "name": "cli-react-integration-test", + "version": "1.0.0", + "description": "Integration test for OpenFeature CLI React generator", + "scripts": { + "build": "tsc", + "test": "node dist/test.js" + }, + "dependencies": { + "@openfeature/react-sdk": "^1.0.0", + "@openfeature/server-sdk": "^1.34.0", + "react": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "typescript": "^5.3.0" + } +} diff --git a/test/react-integration/src/test.ts b/test/react-integration/src/test.ts new file mode 100644 index 0000000..affcc3e --- /dev/null +++ b/test/react-integration/src/test.ts @@ -0,0 +1,69 @@ +import * as generated from './generated'; + +async function main() { + try { + // Validate that all generated exports exist and have the expected structure + const flags = [ + { name: 'EnableFeatureA', flag: generated.EnableFeatureA }, + { name: 'DiscountPercentage', flag: generated.DiscountPercentage }, + { name: 'GreetingMessage', flag: generated.GreetingMessage }, + { name: 'UsernameMaxLength', flag: generated.UsernameMaxLength }, + { name: 'ThemeCustomization', flag: generated.ThemeCustomization }, + ]; + + for (const { name, flag } of flags) { + // Validate the flag object has the expected properties + if (typeof flag !== 'object' || flag === null) { + throw new Error(`${name} is not an object`); + } + + // Check for getKey method + if (typeof flag.getKey !== 'function') { + throw new Error(`${name}.getKey is not a function`); + } + + const key = flag.getKey(); + console.log(`${name} flag key:`, key); + + if (typeof key !== 'string' || key.length === 0) { + throw new Error(`${name}.getKey() did not return a valid string`); + } + + // Check for useFlag hook + if (typeof flag.useFlag !== 'function') { + throw new Error(`${name}.useFlag is not a function`); + } + + // Check for useFlagWithDetails hook + if (typeof flag.useFlagWithDetails !== 'function') { + throw new Error(`${name}.useFlagWithDetails is not a function`); + } + } + + // Verify expected flag keys + if (generated.EnableFeatureA.getKey() !== 'enableFeatureA') { + throw new Error('EnableFeatureA has incorrect key'); + } + if (generated.DiscountPercentage.getKey() !== 'discountPercentage') { + throw new Error('DiscountPercentage has incorrect key'); + } + if (generated.GreetingMessage.getKey() !== 'greetingMessage') { + throw new Error('GreetingMessage has incorrect key'); + } + if (generated.UsernameMaxLength.getKey() !== 'usernameMaxLength') { + throw new Error('UsernameMaxLength has incorrect key'); + } + if (generated.ThemeCustomization.getKey() !== 'themeCustomization') { + throw new Error('ThemeCustomization has incorrect key'); + } + + console.log('All generated React hooks are properly structured!'); + console.log('Generated React code compiles successfully!'); + process.exit(0); + } catch (error: any) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/test/react-integration/tsconfig.json b/test/react-integration/tsconfig.json new file mode 100644 index 0000000..1cafe6d --- /dev/null +++ b/test/react-integration/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "commonjs", + "lib": ["ES2021"], + "jsx": "react", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} From c7ba41008da00ac3dbfcdc3d5d5b94e2b2297ac8 Mon Sep 17 00:00:00 2001 From: Vikas Rao Date: Sat, 21 Feb 2026 09:40:05 -0800 Subject: [PATCH 2/5] fix: address Gemini code review comments - Pin exact Go version to golang:1.24.3-alpine for reproducible builds - Pin exact Node version to node:20.18.1-alpine for consistency - Use exact dependency versions in package.json to prevent unexpected updates - Replace 'error: any' with 'error: unknown' for better type safety - Add proper error message handling with type narrowing All changes improve test reproducibility and code robustness. Refs: #118 Signed-off-by: Vikas Rao --- test/integration/cmd/react/run.go | 8 ++++---- test/react-integration/package.json | 12 ++++++------ test/react-integration/src/test.ts | 5 +++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/test/integration/cmd/react/run.go b/test/integration/cmd/react/run.go index fb50b62..b3b6925 100644 --- a/test/integration/cmd/react/run.go +++ b/test/integration/cmd/react/run.go @@ -34,9 +34,9 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe Include: []string{"package.json", "tsconfig.json", "src/**/*.ts", "src/**/*.tsx"}, }) - // Build the CLI + // Build the CLI in a Go container cli := client.Container(). - From("golang:1.24-alpine"). + From("golang:1.24.3-alpine"). WithExec([]string{"apk", "add", "--no-cache", "git"}). WithDirectory("/src", source). WithWorkdir("/src"). @@ -54,9 +54,9 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe // Get generated files generatedFiles := generated.Directory("/tmp/generated") - // Test React compilation with the generated files + // Create the React test container reactContainer := client.Container(). - From("node:20-alpine"). + From("node:20.18.1-alpine"). WithWorkdir("/app"). WithDirectory("/app", testFiles). WithDirectory("/app/src/generated", generatedFiles). diff --git a/test/react-integration/package.json b/test/react-integration/package.json index 9ffea5c..f9b5e67 100644 --- a/test/react-integration/package.json +++ b/test/react-integration/package.json @@ -7,13 +7,13 @@ "test": "node dist/test.js" }, "dependencies": { - "@openfeature/react-sdk": "^1.0.0", - "@openfeature/server-sdk": "^1.34.0", - "react": "^18.2.0" + "@openfeature/react-sdk": "1.0.2", + "@openfeature/server-sdk": "1.34.0", + "react": "18.2.0" }, "devDependencies": { - "@types/node": "^20.0.0", - "@types/react": "^18.2.0", - "typescript": "^5.3.0" + "@types/node": "20.17.10", + "@types/react": "18.2.79", + "typescript": "5.3.3" } } diff --git a/test/react-integration/src/test.ts b/test/react-integration/src/test.ts index affcc3e..91be4f5 100644 --- a/test/react-integration/src/test.ts +++ b/test/react-integration/src/test.ts @@ -60,8 +60,9 @@ async function main() { console.log('All generated React hooks are properly structured!'); console.log('Generated React code compiles successfully!'); process.exit(0); - } catch (error: any) { - console.error('Error:', error.message); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.error('Error:', message); process.exit(1); } } From 6dd76a34003842c4d1aae3f966939ce39af47e1b Mon Sep 17 00:00:00 2001 From: Vikas Rao Date: Sun, 22 Feb 2026 08:47:39 -0800 Subject: [PATCH 3/5] Address Gemini code review feedback - Use os.Getwd() instead of os.Getenv("PWD") for better reliability - Consolidate flag validation and key verification into single loop - Refactor integration test runner to use loop pattern for better maintainability --- test/integration/cmd/react/run.go | 2 +- test/integration/cmd/run.go | 59 +++++++++--------------------- test/react-integration/src/test.ts | 33 ++++------------- 3 files changed, 27 insertions(+), 67 deletions(-) diff --git a/test/integration/cmd/react/run.go b/test/integration/cmd/react/run.go index b3b6925..c1d80b4 100644 --- a/test/integration/cmd/react/run.go +++ b/test/integration/cmd/react/run.go @@ -76,7 +76,7 @@ func main() { ctx := context.Background() // Get project root - projectDir, err := filepath.Abs(os.Getenv("PWD")) + projectDir, err := os.Getwd() if err != nil { fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err) os.Exit(1) diff --git a/test/integration/cmd/run.go b/test/integration/cmd/run.go index 9d7ad2b..2b4f7a2 100644 --- a/test/integration/cmd/run.go +++ b/test/integration/cmd/run.go @@ -7,51 +7,28 @@ import ( ) func main() { - // Run the language-specific tests fmt.Println("=== Running all integration tests ===") - // Run the C# integration test - csharpCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/csharp") - csharpCmd.Stdout = os.Stdout - csharpCmd.Stderr = os.Stderr - if err := csharpCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error running C# integration test: %v\n", err) - os.Exit(1) + tests := []struct { + name string + path string + }{ + {"C#", "github.com/open-feature/cli/test/integration/cmd/csharp"}, + {"Go", "github.com/open-feature/cli/test/integration/cmd/go"}, + {"NodeJS", "github.com/open-feature/cli/test/integration/cmd/nodejs"}, + {"Angular", "github.com/open-feature/cli/test/integration/cmd/angular"}, + {"React", "github.com/open-feature/cli/test/integration/cmd/react"}, } - // Run the Go integration test - goCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/go") - goCmd.Stdout = os.Stdout - goCmd.Stderr = os.Stderr - if err := goCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error running Go integration test: %v\n", err) - os.Exit(1) - } - // Run the nodejs test - nodeCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/nodejs") - nodeCmd.Stdout = os.Stdout - nodeCmd.Stderr = os.Stderr - if err := nodeCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error running nodejs integration test: %v\n", err) - os.Exit(1) - } - - // Run the Angular integration test - angularCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/angular") - angularCmd.Stdout = os.Stdout - angularCmd.Stderr = os.Stderr - if err := angularCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error running Angular integration test: %v\n", err) - os.Exit(1) - } - - // Run the React integration test - reactCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/react") - reactCmd.Stdout = os.Stdout - reactCmd.Stderr = os.Stderr - if err := reactCmd.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Error running React integration test: %v\n", err) - os.Exit(1) + for _, test := range tests { + fmt.Printf("--- Running %s integration test ---\n", test.name) + cmd := exec.Command("go", "run", test.path) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintf(os.Stderr, "Error running %s integration test: %v\n", test.name, err) + os.Exit(1) + } } fmt.Println("=== All integration tests passed successfully ===") diff --git a/test/react-integration/src/test.ts b/test/react-integration/src/test.ts index 91be4f5..15bd37b 100644 --- a/test/react-integration/src/test.ts +++ b/test/react-integration/src/test.ts @@ -4,14 +4,14 @@ async function main() { try { // Validate that all generated exports exist and have the expected structure const flags = [ - { name: 'EnableFeatureA', flag: generated.EnableFeatureA }, - { name: 'DiscountPercentage', flag: generated.DiscountPercentage }, - { name: 'GreetingMessage', flag: generated.GreetingMessage }, - { name: 'UsernameMaxLength', flag: generated.UsernameMaxLength }, - { name: 'ThemeCustomization', flag: generated.ThemeCustomization }, + { name: 'EnableFeatureA', flag: generated.EnableFeatureA, expectedKey: 'enableFeatureA' }, + { name: 'DiscountPercentage', flag: generated.DiscountPercentage, expectedKey: 'discountPercentage' }, + { name: 'GreetingMessage', flag: generated.GreetingMessage, expectedKey: 'greetingMessage' }, + { name: 'UsernameMaxLength', flag: generated.UsernameMaxLength, expectedKey: 'usernameMaxLength' }, + { name: 'ThemeCustomization', flag: generated.ThemeCustomization, expectedKey: 'themeCustomization' }, ]; - for (const { name, flag } of flags) { + for (const { name, flag, expectedKey } of flags) { // Validate the flag object has the expected properties if (typeof flag !== 'object' || flag === null) { throw new Error(`${name} is not an object`); @@ -25,8 +25,8 @@ async function main() { const key = flag.getKey(); console.log(`${name} flag key:`, key); - if (typeof key !== 'string' || key.length === 0) { - throw new Error(`${name}.getKey() did not return a valid string`); + if (key !== expectedKey) { + throw new Error(`${name} has incorrect key. Expected '${expectedKey}', but got '${key}'.`); } // Check for useFlag hook @@ -40,23 +40,6 @@ async function main() { } } - // Verify expected flag keys - if (generated.EnableFeatureA.getKey() !== 'enableFeatureA') { - throw new Error('EnableFeatureA has incorrect key'); - } - if (generated.DiscountPercentage.getKey() !== 'discountPercentage') { - throw new Error('DiscountPercentage has incorrect key'); - } - if (generated.GreetingMessage.getKey() !== 'greetingMessage') { - throw new Error('GreetingMessage has incorrect key'); - } - if (generated.UsernameMaxLength.getKey() !== 'usernameMaxLength') { - throw new Error('UsernameMaxLength has incorrect key'); - } - if (generated.ThemeCustomization.getKey() !== 'themeCustomization') { - throw new Error('ThemeCustomization has incorrect key'); - } - console.log('All generated React hooks are properly structured!'); console.log('Generated React code compiles successfully!'); process.exit(0); From c7d6335c48ab682f87da2437b36b0b3e9d616c13 Mon Sep 17 00:00:00 2001 From: Vikas Rao <100244218+vikasrao23@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:17:46 -0800 Subject: [PATCH 4/5] Update test/integration/cmd/react/run.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Vikas Rao <100244218+vikasrao23@users.noreply.github.com> --- test/integration/cmd/react/run.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/integration/cmd/react/run.go b/test/integration/cmd/react/run.go index c1d80b4..11f01ea 100644 --- a/test/integration/cmd/react/run.go +++ b/test/integration/cmd/react/run.go @@ -56,8 +56,7 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe // Create the React test container reactContainer := client.Container(). - From("node:20.18.1-alpine"). - WithWorkdir("/app"). +From("node:20.18.1-alpine3.19"). WithDirectory("/app", testFiles). WithDirectory("/app/src/generated", generatedFiles). WithExec([]string{"npm", "install"}). From 43808e6949a2e40352a905135eb3b74f67cc7081 Mon Sep 17 00:00:00 2001 From: Vikas Rao <100244218+vikasrao23@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:03:10 -0800 Subject: [PATCH 5/5] Update test/integration/cmd/react/run.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Vikas Rao <100244218+vikasrao23@users.noreply.github.com> --- test/integration/cmd/react/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/cmd/react/run.go b/test/integration/cmd/react/run.go index 11f01ea..e49e2fa 100644 --- a/test/integration/cmd/react/run.go +++ b/test/integration/cmd/react/run.go @@ -56,7 +56,7 @@ func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Containe // Create the React test container reactContainer := client.Container(). -From("node:20.18.1-alpine3.19"). + From("node:20.18.1-alpine3.19"). WithDirectory("/app", testFiles). WithDirectory("/app/src/generated", generatedFiles). WithExec([]string{"npm", "install"}).