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..e49e2fa --- /dev/null +++ b/test/integration/cmd/react/run.go @@ -0,0 +1,98 @@ +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 in a Go container + cli := client.Container(). + From("golang:1.24.3-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") + + // Create the React test container + reactContainer := client.Container(). + From("node:20.18.1-alpine3.19"). + 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 := os.Getwd() + 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..2b4f7a2 100644 --- a/test/integration/cmd/run.go +++ b/test/integration/cmd/run.go @@ -7,45 +7,29 @@ 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) + 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) + } } - // 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) - } - - // Add more tests here as they are available 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..f9b5e67 --- /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.2", + "@openfeature/server-sdk": "1.34.0", + "react": "18.2.0" + }, + "devDependencies": { + "@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 new file mode 100644 index 0000000..15bd37b --- /dev/null +++ b/test/react-integration/src/test.ts @@ -0,0 +1,53 @@ +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, 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, 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`); + } + + // 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 (key !== expectedKey) { + throw new Error(`${name} has incorrect key. Expected '${expectedKey}', but got '${key}'.`); + } + + // 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`); + } + } + + console.log('All generated React hooks are properly structured!'); + console.log('Generated React code compiles successfully!'); + process.exit(0); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + console.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"] +}