Skip to content

Commit 0a9d6e7

Browse files
authored
Merge pull request #562 from objectstack-ai/copilot/fix-ci-errors-build-test-one-more-time
2 parents dfd5d27 + 2f07eb2 commit 0a9d6e7

15 files changed

Lines changed: 1561 additions & 0 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi } from 'vitest';
10+
import React from 'react';
11+
import { render, screen, waitFor } from '@testing-library/react';
12+
import userEvent from '@testing-library/user-event';
13+
import { AuthCtx, type AuthContextValue } from '../AuthContext';
14+
import { ForgotPasswordForm } from '../ForgotPasswordForm';
15+
16+
function createAuthContext(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
17+
return {
18+
user: null,
19+
session: null,
20+
isAuthenticated: false,
21+
isLoading: false,
22+
error: null,
23+
isPreviewMode: false,
24+
previewMode: null,
25+
signIn: vi.fn().mockResolvedValue(undefined),
26+
signUp: vi.fn().mockResolvedValue(undefined),
27+
signOut: vi.fn().mockResolvedValue(undefined),
28+
updateUser: vi.fn().mockResolvedValue(undefined),
29+
forgotPassword: vi.fn().mockResolvedValue(undefined),
30+
resetPassword: vi.fn().mockResolvedValue(undefined),
31+
...overrides,
32+
};
33+
}
34+
35+
function renderWithAuth(ui: React.ReactElement, authOverrides: Partial<AuthContextValue> = {}) {
36+
const ctx = createAuthContext(authOverrides);
37+
return {
38+
ctx,
39+
...render(<AuthCtx.Provider value={ctx}>{ui}</AuthCtx.Provider>),
40+
};
41+
}
42+
43+
describe('ForgotPasswordForm', () => {
44+
it('renders with default title', () => {
45+
renderWithAuth(<ForgotPasswordForm />);
46+
expect(screen.getByText('Reset your password')).toBeTruthy();
47+
});
48+
49+
it('renders email field', () => {
50+
renderWithAuth(<ForgotPasswordForm />);
51+
expect(screen.getByLabelText('Email')).toBeTruthy();
52+
});
53+
54+
it('shows loading state', () => {
55+
renderWithAuth(<ForgotPasswordForm />, { isLoading: true });
56+
expect(screen.getByRole('button', { name: 'Sending...' })).toBeTruthy();
57+
});
58+
59+
it('shows login link', () => {
60+
renderWithAuth(<ForgotPasswordForm loginUrl="/login" />);
61+
expect(screen.getByText('Sign in')).toBeTruthy();
62+
});
63+
64+
it('shows success message after submission', async () => {
65+
const onSuccess = vi.fn();
66+
const { ctx } = renderWithAuth(<ForgotPasswordForm onSuccess={onSuccess} />);
67+
const user = userEvent.setup();
68+
69+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
70+
await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
71+
72+
await waitFor(() => {
73+
expect(ctx.forgotPassword).toHaveBeenCalledWith('test@example.com');
74+
expect(screen.getByText('Check your email')).toBeTruthy();
75+
});
76+
expect(onSuccess).toHaveBeenCalled();
77+
});
78+
79+
it('shows error on failure', async () => {
80+
const onError = vi.fn();
81+
const forgotPassword = vi.fn().mockRejectedValue(new Error('User not found'));
82+
renderWithAuth(<ForgotPasswordForm onError={onError} />, { forgotPassword });
83+
const user = userEvent.setup();
84+
85+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
86+
await user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
87+
88+
await waitFor(() => {
89+
expect(screen.getByText('User not found')).toBeTruthy();
90+
});
91+
expect(onError).toHaveBeenCalled();
92+
});
93+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi } from 'vitest';
10+
import React from 'react';
11+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
12+
import userEvent from '@testing-library/user-event';
13+
import { AuthCtx, type AuthContextValue } from '../AuthContext';
14+
import { LoginForm } from '../LoginForm';
15+
16+
function createAuthContext(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
17+
return {
18+
user: null,
19+
session: null,
20+
isAuthenticated: false,
21+
isLoading: false,
22+
error: null,
23+
isPreviewMode: false,
24+
previewMode: null,
25+
signIn: vi.fn().mockResolvedValue(undefined),
26+
signUp: vi.fn().mockResolvedValue(undefined),
27+
signOut: vi.fn().mockResolvedValue(undefined),
28+
updateUser: vi.fn().mockResolvedValue(undefined),
29+
forgotPassword: vi.fn().mockResolvedValue(undefined),
30+
resetPassword: vi.fn().mockResolvedValue(undefined),
31+
...overrides,
32+
};
33+
}
34+
35+
function renderWithAuth(ui: React.ReactElement, authOverrides: Partial<AuthContextValue> = {}) {
36+
const ctx = createAuthContext(authOverrides);
37+
return {
38+
ctx,
39+
...render(<AuthCtx.Provider value={ctx}>{ui}</AuthCtx.Provider>),
40+
};
41+
}
42+
43+
describe('LoginForm', () => {
44+
it('renders with default title and description', () => {
45+
renderWithAuth(<LoginForm />);
46+
expect(screen.getByText('Sign in to your account')).toBeTruthy();
47+
expect(screen.getByText('Enter your email and password to continue')).toBeTruthy();
48+
});
49+
50+
it('renders with custom title and description', () => {
51+
renderWithAuth(<LoginForm title="Custom Login" description="Custom desc" />);
52+
expect(screen.getByText('Custom Login')).toBeTruthy();
53+
expect(screen.getByText('Custom desc')).toBeTruthy();
54+
});
55+
56+
it('renders email and password fields', () => {
57+
renderWithAuth(<LoginForm />);
58+
expect(screen.getByLabelText('Email')).toBeTruthy();
59+
expect(screen.getByLabelText('Password')).toBeTruthy();
60+
});
61+
62+
it('renders sign in button', () => {
63+
renderWithAuth(<LoginForm />);
64+
expect(screen.getByRole('button', { name: 'Sign In' })).toBeTruthy();
65+
});
66+
67+
it('shows loading state', () => {
68+
renderWithAuth(<LoginForm />, { isLoading: true });
69+
expect(screen.getByRole('button', { name: 'Signing in...' })).toBeTruthy();
70+
});
71+
72+
it('renders registration and forgot password links', () => {
73+
renderWithAuth(<LoginForm registerUrl="/register" forgotPasswordUrl="/forgot" />);
74+
expect(screen.getByText('Sign up')).toBeTruthy();
75+
expect(screen.getByText('Forgot password?')).toBeTruthy();
76+
});
77+
78+
it('calls signIn on form submission', async () => {
79+
const onSuccess = vi.fn();
80+
const { ctx } = renderWithAuth(<LoginForm onSuccess={onSuccess} />);
81+
const user = userEvent.setup();
82+
83+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
84+
await user.type(screen.getByLabelText('Password'), 'password123');
85+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
86+
87+
await waitFor(() => {
88+
expect(ctx.signIn).toHaveBeenCalledWith('test@example.com', 'password123');
89+
});
90+
expect(onSuccess).toHaveBeenCalled();
91+
});
92+
93+
it('shows error on failed sign in', async () => {
94+
const onError = vi.fn();
95+
const signIn = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
96+
renderWithAuth(<LoginForm onError={onError} />, { signIn });
97+
const user = userEvent.setup();
98+
99+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
100+
await user.type(screen.getByLabelText('Password'), 'wrong');
101+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
102+
103+
await waitFor(() => {
104+
expect(screen.getByRole('alert')).toBeTruthy();
105+
expect(screen.getByText('Invalid credentials')).toBeTruthy();
106+
});
107+
expect(onError).toHaveBeenCalled();
108+
});
109+
110+
it('handles non-Error rejection', async () => {
111+
const signIn = vi.fn().mockRejectedValue('string error');
112+
renderWithAuth(<LoginForm />, { signIn });
113+
const user = userEvent.setup();
114+
115+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
116+
await user.type(screen.getByLabelText('Password'), 'wrong');
117+
await user.click(screen.getByRole('button', { name: 'Sign In' }));
118+
119+
await waitFor(() => {
120+
expect(screen.getByRole('alert')).toBeTruthy();
121+
});
122+
});
123+
});
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi } from 'vitest';
10+
import React from 'react';
11+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
12+
import userEvent from '@testing-library/user-event';
13+
import { AuthCtx, type AuthContextValue } from '../AuthContext';
14+
import { RegisterForm } from '../RegisterForm';
15+
16+
function createAuthContext(overrides: Partial<AuthContextValue> = {}): AuthContextValue {
17+
return {
18+
user: null,
19+
session: null,
20+
isAuthenticated: false,
21+
isLoading: false,
22+
error: null,
23+
isPreviewMode: false,
24+
previewMode: null,
25+
signIn: vi.fn().mockResolvedValue(undefined),
26+
signUp: vi.fn().mockResolvedValue(undefined),
27+
signOut: vi.fn().mockResolvedValue(undefined),
28+
updateUser: vi.fn().mockResolvedValue(undefined),
29+
forgotPassword: vi.fn().mockResolvedValue(undefined),
30+
resetPassword: vi.fn().mockResolvedValue(undefined),
31+
...overrides,
32+
};
33+
}
34+
35+
function renderWithAuth(ui: React.ReactElement, authOverrides: Partial<AuthContextValue> = {}) {
36+
const ctx = createAuthContext(authOverrides);
37+
return {
38+
ctx,
39+
...render(<AuthCtx.Provider value={ctx}>{ui}</AuthCtx.Provider>),
40+
};
41+
}
42+
43+
describe('RegisterForm', () => {
44+
it('renders with default title', () => {
45+
renderWithAuth(<RegisterForm />);
46+
expect(screen.getByText('Create an account')).toBeTruthy();
47+
});
48+
49+
it('renders all form fields', () => {
50+
renderWithAuth(<RegisterForm />);
51+
expect(screen.getByLabelText('Name')).toBeTruthy();
52+
expect(screen.getByLabelText('Email')).toBeTruthy();
53+
expect(screen.getByLabelText('Password')).toBeTruthy();
54+
expect(screen.getByLabelText('Confirm Password')).toBeTruthy();
55+
});
56+
57+
it('shows loading state', () => {
58+
renderWithAuth(<RegisterForm />, { isLoading: true });
59+
expect(screen.getByRole('button', { name: 'Creating account...' })).toBeTruthy();
60+
});
61+
62+
it('shows login link', () => {
63+
renderWithAuth(<RegisterForm loginUrl="/login" />);
64+
expect(screen.getByText('Sign in')).toBeTruthy();
65+
});
66+
67+
it('shows error when passwords do not match', async () => {
68+
renderWithAuth(<RegisterForm />);
69+
const user = userEvent.setup();
70+
71+
await user.type(screen.getByLabelText('Name'), 'John');
72+
await user.type(screen.getByLabelText('Email'), 'john@example.com');
73+
await user.type(screen.getByLabelText('Password'), 'password123');
74+
await user.type(screen.getByLabelText('Confirm Password'), 'different');
75+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
76+
77+
await waitFor(() => {
78+
expect(screen.getByText('Passwords do not match')).toBeTruthy();
79+
});
80+
});
81+
82+
it('shows error when password is too short', async () => {
83+
renderWithAuth(<RegisterForm />);
84+
const user = userEvent.setup();
85+
86+
await user.type(screen.getByLabelText('Name'), 'John');
87+
await user.type(screen.getByLabelText('Email'), 'john@example.com');
88+
// Type passwords that match but are under 8 chars
89+
// Use fireEvent to bypass native minLength validation
90+
const pwField = screen.getByLabelText('Password');
91+
const confirmField = screen.getByLabelText('Confirm Password');
92+
fireEvent.change(pwField, { target: { value: 'short' } });
93+
fireEvent.change(confirmField, { target: { value: 'short' } });
94+
fireEvent.submit(screen.getByRole('button', { name: 'Create Account' }).closest('form')!);
95+
96+
await waitFor(() => {
97+
expect(screen.getByText('Password must be at least 8 characters')).toBeTruthy();
98+
});
99+
});
100+
101+
it('calls signUp on valid submission', async () => {
102+
const onSuccess = vi.fn();
103+
const { ctx } = renderWithAuth(<RegisterForm onSuccess={onSuccess} />);
104+
const user = userEvent.setup();
105+
106+
await user.type(screen.getByLabelText('Name'), 'John Doe');
107+
await user.type(screen.getByLabelText('Email'), 'john@example.com');
108+
await user.type(screen.getByLabelText('Password'), 'password123');
109+
await user.type(screen.getByLabelText('Confirm Password'), 'password123');
110+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
111+
112+
await waitFor(() => {
113+
expect(ctx.signUp).toHaveBeenCalledWith('John Doe', 'john@example.com', 'password123');
114+
});
115+
expect(onSuccess).toHaveBeenCalled();
116+
});
117+
118+
it('shows error on signUp failure', async () => {
119+
const onError = vi.fn();
120+
const signUp = vi.fn().mockRejectedValue(new Error('Email taken'));
121+
renderWithAuth(<RegisterForm onError={onError} />, { signUp });
122+
const user = userEvent.setup();
123+
124+
await user.type(screen.getByLabelText('Name'), 'John');
125+
await user.type(screen.getByLabelText('Email'), 'john@example.com');
126+
await user.type(screen.getByLabelText('Password'), 'password123');
127+
await user.type(screen.getByLabelText('Confirm Password'), 'password123');
128+
await user.click(screen.getByRole('button', { name: 'Create Account' }));
129+
130+
await waitFor(() => {
131+
expect(screen.getByText('Email taken')).toBeTruthy();
132+
});
133+
expect(onError).toHaveBeenCalled();
134+
});
135+
});

0 commit comments

Comments
 (0)