diff --git a/packages/clerk_flutter/test/integration/authentication_flow_test.dart b/packages/clerk_flutter/test/integration/authentication_flow_test.dart deleted file mode 100644 index 467af3f5..00000000 --- a/packages/clerk_flutter/test/integration/authentication_flow_test.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:clerk_auth/clerk_auth.dart' as clerk; -import 'package:clerk_flutter/src/widgets/authentication/clerk_sign_in_panel.dart'; -import 'package:clerk_flutter/src/widgets/authentication/clerk_sign_up_panel.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../test_support/test_support.dart'; - -void main() { - group('Authentication Flow Integration Tests', () { - group('Sign Up Flow', () { - testWidgets('complete sign up flow with email and password', - (tester) async { - // Create initial auth state with sign-up - final signUp = createTestSignUp( - status: clerk.Status.missingRequirements, - requiredFields: [clerk.Field.emailAddress, clerk.Field.password], - missingFields: [clerk.Field.emailAddress, clerk.Field.password], - ); - final client = createTestClient(signUp: signUp); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignUpPanel(), - ), - ); - await tester.pump(); - - // Verify the sign-up panel is rendered - expect(find.byType(ClerkSignUpPanel), findsOneWidget); - - authState.terminate(); - }); - - testWidgets('sign up flow with phone number verification', - (tester) async { - final signUp = createTestSignUp( - status: clerk.Status.missingRequirements, - requiredFields: [clerk.Field.phoneNumber], - missingFields: [clerk.Field.phoneNumber], - ); - final client = createTestClient(signUp: signUp); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignUpPanel(), - ), - ); - await tester.pump(); - - expect(find.byType(ClerkSignUpPanel), findsOneWidget); - - authState.terminate(); - }); - - testWidgets('sign up flow with optional fields', (tester) async { - final signUp = createTestSignUp( - status: clerk.Status.missingRequirements, - requiredFields: [clerk.Field.emailAddress], - optionalFields: [clerk.Field.firstName, clerk.Field.lastName], - missingFields: [clerk.Field.emailAddress], - ); - final client = createTestClient(signUp: signUp); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignUpPanel(), - ), - ); - await tester.pump(); - - expect(find.byType(ClerkSignUpPanel), findsOneWidget); - - authState.terminate(); - }); - - testWidgets('sign up with email verification required', (tester) async { - final verification = createTestVerification( - status: clerk.Status.unverified, - strategy: clerk.Strategy.emailCode, - ); - final signUp = createTestSignUp( - status: clerk.Status.missingRequirements, - emailAddress: 'test@example.com', - unverifiedFields: [clerk.Field.emailAddress], - verifications: {clerk.Field.emailAddress: verification}, - ); - final client = createTestClient(signUp: signUp); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignUpPanel(), - ), - ); - await tester.pump(); - - expect(find.byType(ClerkSignUpPanel), findsOneWidget); - - authState.terminate(); - }); - }); - - group('Sign In Flow', () { - testWidgets('complete sign in flow with email and password', - (tester) async { - final signIn = createTestSignIn( - status: clerk.Status.needsFirstFactor, - identifier: 'test@example.com', - supportedFirstFactors: [ - createTestFactor(strategy: clerk.Strategy.password), - ], - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - - testWidgets('sign in with email code verification', (tester) async { - final signIn = createTestSignIn( - status: clerk.Status.needsFirstFactor, - identifier: 'test@example.com', - firstFactorVerification: createTestVerification( - status: clerk.Status.unverified, - strategy: clerk.Strategy.emailCode, - ), - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - }); - }); -} diff --git a/packages/clerk_flutter/test/integration/sign_in_flow_test.dart b/packages/clerk_flutter/test/integration/sign_in_flow_test.dart deleted file mode 100644 index f4ee4495..00000000 --- a/packages/clerk_flutter/test/integration/sign_in_flow_test.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:clerk_auth/clerk_auth.dart' as clerk; -import 'package:clerk_flutter/src/widgets/authentication/clerk_sign_in_panel.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../test_support/test_support.dart'; - -void main() { - group('Sign In Flow Integration Tests', () { - group('Email and Password Sign In', () { - testWidgets('complete sign in flow with email and password', - (tester) async { - // Create initial auth state with sign-in - final signIn = createTestSignIn( - status: clerk.Status.needsFirstFactor, - supportedFirstFactors: [ - createTestFactor(strategy: clerk.Strategy.password), - createTestFactor(strategy: clerk.Strategy.emailCode), - ], - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - // Verify the sign-in panel is rendered - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - - testWidgets('sign in flow with password factor', (tester) async { - final signIn = createTestSignIn( - status: clerk.Status.needsFirstFactor, - supportedFirstFactors: [ - createTestFactor(strategy: clerk.Strategy.password), - ], - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - // Verify the sign-in panel is rendered - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - }); - - group('Phone Number Sign In', () { - testWidgets('sign in flow with phone code verification', (tester) async { - final signIn = createTestSignIn( - status: clerk.Status.needsFirstFactor, - supportedFirstFactors: [ - createTestFactor(strategy: clerk.Strategy.phoneCode), - ], - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - // Verify the sign-in panel is rendered - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - }); - - group('Email Code Sign In', () { - testWidgets('sign in flow with email code verification', (tester) async { - final signIn = createTestSignIn( - status: clerk.Status.needsFirstFactor, - supportedFirstFactors: [ - createTestFactor(strategy: clerk.Strategy.emailCode), - ], - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - // Verify the sign-in panel is rendered - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - }); - - group('Two-Factor Authentication', () { - testWidgets('sign in flow with second factor required', (tester) async { - final signIn = createTestSignIn( - status: clerk.Status.needsSecondFactor, - supportedSecondFactors: [ - createTestFactor(strategy: clerk.Strategy.totp), - createTestFactor(strategy: clerk.Strategy.backupCode), - ], - ); - final client = createTestClient(signIn: signIn); - final authState = await createTestAuthState(client: client); - - await tester.pumpWidget( - TestClerkAuthWrapper( - authState: authState, - child: const ClerkSignInPanel(), - ), - ); - await tester.pump(); - - // Verify the sign-in panel is rendered - expect(find.byType(ClerkSignInPanel), findsOneWidget); - - authState.terminate(); - }); - }); - }); -} diff --git a/packages/clerk_flutter/test/test_support/src/test_data.dart b/packages/clerk_flutter/test/test_support/src/test_data.dart index 47a38ee3..98e2bf02 100644 --- a/packages/clerk_flutter/test/test_support/src/test_data.dart +++ b/packages/clerk_flutter/test/test_support/src/test_data.dart @@ -110,6 +110,45 @@ User createTestUser({ ); } +/// Creates a test Environment with sensible defaults +/// +/// A non-empty [Environment] is needed for the auth panels to render: +/// they return an empty widget when `authState.isNotAvailable`, and the +/// form fields they display are driven by the enabled attributes in +/// `env.user.attributes` rather than by the sign-up/sign-in object. +/// `ClerkSignInPanel` additionally requires at least one identification +/// strategy in `env.config` before it renders anything. +Environment createTestEnvironment({ + List requiredAttributes = const [], + List optionalAttributes = const [], + List identificationStrategies = const [], + bool legalConsentEnabled = false, +}) { + UserAttributeData dataFor(UserAttribute attr, {required bool isRequired}) { + return UserAttributeData( + isRequired: isRequired, + verifications: switch (attr) { + UserAttribute.emailAddress => const [Strategy.emailCode], + UserAttribute.phoneNumber => const [Strategy.phoneCode], + _ => const [], + }, + ); + } + + return Environment( + config: Config(identificationStrategies: identificationStrategies), + user: UserSettings( + attributes: { + for (final attr in requiredAttributes) // + attr: dataFor(attr, isRequired: true), + for (final attr in optionalAttributes) // + attr: dataFor(attr, isRequired: false), + }, + signUp: SignUpSettings(legalConsentEnabled: legalConsentEnabled), + ), + ); +} + /// Creates a test Session with sensible defaults Session createTestSession({ String id = 'sess_test_123', diff --git a/packages/clerk_flutter/test/test_support/src/test_wrapper.dart b/packages/clerk_flutter/test/test_support/src/test_wrapper.dart index 839d1c46..05a537c9 100644 --- a/packages/clerk_flutter/test/test_support/src/test_wrapper.dart +++ b/packages/clerk_flutter/test/test_support/src/test_wrapper.dart @@ -9,13 +9,15 @@ Future createTestAuthState({ TestClerkAuthConfig? config, clerk.Client? client, clerk.User? user, + clerk.Environment? environment, }) async { final effectiveConfig = config ?? (user != null ? TestClerkAuthConfig.signedIn(user: user) - : (client != null - ? TestClerkAuthConfig(initialClient: client) - : TestClerkAuthConfig())); + : TestClerkAuthConfig( + initialClient: client, + initialEnvironment: environment, + )); return ClerkAuthState.create(config: effectiveConfig); } diff --git a/packages/clerk_flutter/test/widgets/clerk_sign_in_panel_test.dart b/packages/clerk_flutter/test/widgets/clerk_sign_in_panel_test.dart new file mode 100644 index 00000000..7981b435 --- /dev/null +++ b/packages/clerk_flutter/test/widgets/clerk_sign_in_panel_test.dart @@ -0,0 +1,174 @@ +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:clerk_flutter/src/widgets/authentication/clerk_sign_in_panel.dart'; +import 'package:clerk_flutter/src/widgets/ui/clerk_text_form_field.dart'; +import 'package:clerk_flutter/src/widgets/ui/strategy_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../test_support/test_support.dart'; + +void main() { + group('ClerkSignInPanel', () { + testWidgets('renders password field when password is the only first factor', + (tester) async { + final signIn = createTestSignIn( + status: Status.needsFirstFactor, + identifier: 'test@example.com', + supportedFirstFactors: [ + createTestFactor(strategy: Strategy.password), + ], + ); + final client = createTestClient(signIn: signIn); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + identificationStrategies: [Strategy.emailAddress], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignInPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignInPanel), findsOneWidget); + expect(find.byType(ClerkTextFormField), findsOneWidget); + expect(find.text('Password'), findsOneWidget); + + authState.terminate(); + }); + + testWidgets( + 'renders strategy button when email code is the only first factor', + (tester) async { + final signIn = createTestSignIn( + status: Status.needsFirstFactor, + identifier: 'test@example.com', + supportedFirstFactors: [ + createTestFactor(strategy: Strategy.emailCode), + ], + ); + final client = createTestClient(signIn: signIn); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + identificationStrategies: [Strategy.emailAddress], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignInPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignInPanel), findsOneWidget); + expect(find.byType(StrategyButton), findsOneWidget); + expect(find.byType(ClerkTextFormField), findsNothing); + + authState.terminate(); + }); + + testWidgets( + 'renders password field and strategy button when both are ' + 'supported first factors', (tester) async { + final signIn = createTestSignIn( + status: Status.needsFirstFactor, + supportedFirstFactors: [ + createTestFactor(strategy: Strategy.password), + createTestFactor(strategy: Strategy.emailCode), + ], + ); + final client = createTestClient(signIn: signIn); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + identificationStrategies: [Strategy.emailAddress], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignInPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignInPanel), findsOneWidget); + expect(find.byType(ClerkTextFormField), findsOneWidget); + expect(find.byType(StrategyButton), findsOneWidget); + + authState.terminate(); + }); + + testWidgets( + 'renders strategy button when phone code is the only first factor', + (tester) async { + final signIn = createTestSignIn( + status: Status.needsFirstFactor, + supportedFirstFactors: [ + createTestFactor(strategy: Strategy.phoneCode), + ], + ); + final client = createTestClient(signIn: signIn); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + identificationStrategies: [Strategy.phoneNumber], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignInPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignInPanel), findsOneWidget); + expect(find.byType(StrategyButton), findsOneWidget); + + authState.terminate(); + }); + + testWidgets('renders strategy buttons when a second factor is required', + (tester) async { + final signIn = createTestSignIn( + status: Status.needsSecondFactor, + supportedSecondFactors: [ + createTestFactor(strategy: Strategy.totp), + createTestFactor(strategy: Strategy.backupCode), + ], + ); + final client = createTestClient(signIn: signIn); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + identificationStrategies: [Strategy.emailAddress], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignInPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignInPanel), findsOneWidget); + + // One button each for authenticator app and backup code + expect(find.byType(StrategyButton), findsNWidgets(2)); + + authState.terminate(); + }); + }); +} diff --git a/packages/clerk_flutter/test/widgets/clerk_sign_up_panel_test.dart b/packages/clerk_flutter/test/widgets/clerk_sign_up_panel_test.dart new file mode 100644 index 00000000..97e4e6d1 --- /dev/null +++ b/packages/clerk_flutter/test/widgets/clerk_sign_up_panel_test.dart @@ -0,0 +1,149 @@ +import 'package:clerk_auth/clerk_auth.dart'; +import 'package:clerk_flutter/src/widgets/authentication/clerk_sign_up_panel.dart'; +import 'package:clerk_flutter/src/widgets/ui/clerk_phone_number_form_field.dart'; +import 'package:clerk_flutter/src/widgets/ui/clerk_text_form_field.dart'; +import 'package:clerk_flutter/src/widgets/ui/strategy_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../test_support/test_support.dart'; + +void main() { + group('ClerkSignUpPanel', () { + testWidgets('renders email and password fields when they are missing', + (tester) async { + final signUp = createTestSignUp( + status: Status.missingRequirements, + requiredFields: [Field.emailAddress, Field.password], + missingFields: [Field.emailAddress, Field.password], + ); + final client = createTestClient(signUp: signUp); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + requiredAttributes: [ + UserAttribute.emailAddress, + UserAttribute.password, + ], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignUpPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignUpPanel), findsOneWidget); + + // Email, password and password confirmation fields + expect(find.byType(ClerkTextFormField), findsNWidgets(3)); + expect(find.text('Email address'), findsOneWidget); + expect(find.text('Password'), findsOneWidget); + + authState.terminate(); + }); + + testWidgets('renders phone number field when phone number is missing', + (tester) async { + final signUp = createTestSignUp( + status: Status.missingRequirements, + requiredFields: [Field.phoneNumber], + missingFields: [Field.phoneNumber], + ); + final client = createTestClient(signUp: signUp); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + requiredAttributes: [UserAttribute.phoneNumber], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignUpPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignUpPanel), findsOneWidget); + expect(find.byType(ClerkPhoneNumberFormField), findsOneWidget); + + authState.terminate(); + }); + + testWidgets('renders optional name fields alongside required email field', + (tester) async { + final signUp = createTestSignUp( + status: Status.missingRequirements, + requiredFields: [Field.emailAddress], + optionalFields: [Field.firstName, Field.lastName], + ); + final client = createTestClient(signUp: signUp); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + requiredAttributes: [UserAttribute.emailAddress], + optionalAttributes: [ + UserAttribute.firstName, + UserAttribute.lastName, + ], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignUpPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignUpPanel), findsOneWidget); + + // First name, last name and email fields + expect(find.byType(ClerkTextFormField), findsNWidgets(3)); + + authState.terminate(); + }); + + testWidgets('renders strategy button when email verification is required', + (tester) async { + final verification = createTestVerification( + status: Status.unverified, + strategy: Strategy.emailCode, + ); + final signUp = createTestSignUp( + status: Status.missingRequirements, + emailAddress: 'test@example.com', + unverifiedFields: [Field.emailAddress], + verifications: {Field.emailAddress: verification}, + ); + final client = createTestClient(signUp: signUp); + final authState = await createTestAuthState( + client: client, + environment: createTestEnvironment( + requiredAttributes: [UserAttribute.emailAddress], + ), + ); + + await tester.pumpWidget( + TestClerkAuthWrapper( + authState: authState, + child: const Scaffold(body: ClerkSignUpPanel()), + ), + ); + await tester.pump(); + + expect(find.byType(ClerkSignUpPanel), findsOneWidget); + + // Email code is the only supported strategy for verification + expect(find.byType(StrategyButton), findsOneWidget); + + authState.terminate(); + }); + }); +}