@slyxup/ui 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/dist/components/BillingPortal/BillingPortal.d.ts +22 -0
- package/dist/components/BillingPortal/BillingPortal.d.ts.map +1 -0
- package/dist/components/BillingPortal/BillingPortal.js +61 -0
- package/dist/components/BillingPortal/BillingPortal.js.map +1 -0
- package/dist/components/EmailVerification/EmailVerification.d.ts.map +1 -1
- package/dist/components/EmailVerification/EmailVerification.js +2 -0
- package/dist/components/EmailVerification/EmailVerification.js.map +1 -1
- package/dist/components/ForgotPassword/ForgotPassword.d.ts.map +1 -1
- package/dist/components/ForgotPassword/ForgotPassword.js +2 -0
- package/dist/components/ForgotPassword/ForgotPassword.js.map +1 -1
- package/dist/components/PricingTable/PricingTable.d.ts +19 -0
- package/dist/components/PricingTable/PricingTable.d.ts.map +1 -0
- package/dist/components/PricingTable/PricingTable.js +54 -0
- package/dist/components/PricingTable/PricingTable.js.map +1 -0
- package/dist/components/ResetPassword/ResetPassword.d.ts.map +1 -1
- package/dist/components/ResetPassword/ResetPassword.js +2 -0
- package/dist/components/ResetPassword/ResetPassword.js.map +1 -1
- package/dist/components/SignIn/SignIn.d.ts.map +1 -1
- package/dist/components/SignIn/SignIn.js +16 -3
- package/dist/components/SignIn/SignIn.js.map +1 -1
- package/dist/components/SignUp/SignUp.d.ts.map +1 -1
- package/dist/components/SignUp/SignUp.js +16 -3
- package/dist/components/SignUp/SignUp.js.map +1 -1
- package/dist/components/SocialButtons/SocialButtons.d.ts +1 -1
- package/dist/components/SocialButtons/SocialButtons.d.ts.map +1 -1
- package/dist/components/SocialButtons/SocialButtons.js +7 -2
- package/dist/components/SocialButtons/SocialButtons.js.map +1 -1
- package/dist/components/UserButton/UserButton.d.ts.map +1 -1
- package/dist/components/UserButton/UserButton.js +2 -0
- package/dist/components/UserButton/UserButton.js.map +1 -1
- package/dist/components/UserProfile/UserProfile.d.ts.map +1 -1
- package/dist/components/UserProfile/UserProfile.js +2 -0
- package/dist/components/UserProfile/UserProfile.js.map +1 -1
- package/dist/styles.d.ts +3 -2
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +37 -16
- package/dist/styles.js.map +1 -1
- package/package.json +13 -9
- package/src/components/BillingPortal/BillingPortal.tsx +184 -0
- package/src/components/EmailVerification/EmailVerification.tsx +2 -0
- package/src/components/ForgotPassword/ForgotPassword.tsx +2 -0
- package/src/components/PricingTable/PricingTable.tsx +157 -0
- package/src/components/ResetPassword/ResetPassword.tsx +2 -0
- package/src/components/SignIn/SignIn.tsx +29 -2
- package/src/components/SignUp/SignUp.tsx +29 -2
- package/src/components/SocialButtons/SocialButtons.tsx +10 -3
- package/src/components/UserButton/UserButton.tsx +2 -0
- package/src/components/UserProfile/UserProfile.tsx +2 -0
- package/src/styles.ts +38 -16
- package/test/components.test.tsx +135 -0
- package/test/icons.test.tsx +32 -0
- package/test/styles.test.ts +47 -0
- package/vitest.config.ts +2 -0
|
@@ -2,6 +2,7 @@ import { SlyxupError } from '@slyxup/core';
|
|
|
2
2
|
import { useAuth } from '@slyxup/react';
|
|
3
3
|
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
|
|
5
|
+
import { injectStyles } from '../../styles';
|
|
5
6
|
|
|
6
7
|
export interface SignInProps {
|
|
7
8
|
/** Show social buttons (default true) */
|
|
@@ -18,7 +19,11 @@ export function SignIn({
|
|
|
18
19
|
onSuccess,
|
|
19
20
|
onSignUpClick,
|
|
20
21
|
}: SignInProps) {
|
|
21
|
-
|
|
22
|
+
injectStyles();
|
|
23
|
+
const { signIn, client } = useAuth() as unknown as {
|
|
24
|
+
signIn: ReturnType<typeof useAuth>['signIn'];
|
|
25
|
+
client: { publishableKey?: string; apiUrl: string };
|
|
26
|
+
};
|
|
22
27
|
const [email, setEmail] = useState('');
|
|
23
28
|
const [password, setPassword] = useState('');
|
|
24
29
|
const [busy, setBusy] = useState(false);
|
|
@@ -51,11 +56,33 @@ export function SignIn({
|
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function oauth(provider: 'google' | 'github') {
|
|
54
|
-
window.location.href =
|
|
59
|
+
window.location.href = `${client.apiUrl}/v1/oauth/${provider}`;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
const missingKey =
|
|
63
|
+
!client.publishableKey ||
|
|
64
|
+
client.publishableKey === 'pk_test_missing' ||
|
|
65
|
+
client.publishableKey.includes('REPLACE');
|
|
66
|
+
|
|
57
67
|
return (
|
|
58
68
|
<div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
69
|
+
{missingKey && (
|
|
70
|
+
<p
|
|
71
|
+
style={{
|
|
72
|
+
fontSize: 12,
|
|
73
|
+
background: '#fff3cd',
|
|
74
|
+
border: '1px solid #ffe69c',
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
padding: '8px 10px',
|
|
77
|
+
marginBottom: 14,
|
|
78
|
+
lineHeight: 1.4,
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<strong>Setup:</strong> Add{' '}
|
|
82
|
+
<code>NEXT_PUBLIC_SLYXUP_PUBLISHABLE_KEY</code> to{' '}
|
|
83
|
+
<code>.env.local</code> — run <code>npx @slyxup/cli keys create</code>
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
59
86
|
<div className="slx-mark">
|
|
60
87
|
<KeyholeMark />
|
|
61
88
|
</div>
|
|
@@ -2,6 +2,7 @@ import { SlyxupError } from '@slyxup/core';
|
|
|
2
2
|
import { useAuth } from '@slyxup/react';
|
|
3
3
|
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { GitHubIcon, GoogleIcon, KeyholeMark } from '../../icons';
|
|
5
|
+
import { injectStyles } from '../../styles';
|
|
5
6
|
|
|
6
7
|
export interface SignUpProps {
|
|
7
8
|
social?: boolean;
|
|
@@ -15,7 +16,11 @@ export function SignUp({
|
|
|
15
16
|
onSuccess,
|
|
16
17
|
onSignInClick,
|
|
17
18
|
}: SignUpProps) {
|
|
18
|
-
|
|
19
|
+
injectStyles();
|
|
20
|
+
const { signUp, client } = useAuth() as unknown as {
|
|
21
|
+
signUp: ReturnType<typeof useAuth>['signUp'];
|
|
22
|
+
client: { publishableKey?: string; apiUrl: string };
|
|
23
|
+
};
|
|
19
24
|
const [firstName, setFirstName] = useState('');
|
|
20
25
|
const [email, setEmail] = useState('');
|
|
21
26
|
const [password, setPassword] = useState('');
|
|
@@ -49,11 +54,33 @@ export function SignUp({
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
function oauth(provider: 'google' | 'github') {
|
|
52
|
-
window.location.href =
|
|
57
|
+
window.location.href = `${client.apiUrl}/v1/oauth/${provider}`;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
const missingKey =
|
|
61
|
+
!client.publishableKey ||
|
|
62
|
+
client.publishableKey === 'pk_test_missing' ||
|
|
63
|
+
client.publishableKey.includes('REPLACE');
|
|
64
|
+
|
|
55
65
|
return (
|
|
56
66
|
<div ref={cardRef} className={`slx-card${error ? ' slx-card-error' : ''}`}>
|
|
67
|
+
{missingKey && (
|
|
68
|
+
<p
|
|
69
|
+
style={{
|
|
70
|
+
fontSize: 12,
|
|
71
|
+
background: '#fff3cd',
|
|
72
|
+
border: '1px solid #ffe69c',
|
|
73
|
+
borderRadius: 8,
|
|
74
|
+
padding: '8px 10px',
|
|
75
|
+
marginBottom: 14,
|
|
76
|
+
lineHeight: 1.4,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<strong>Setup:</strong> Add{' '}
|
|
80
|
+
<code>NEXT_PUBLIC_SLYXUP_PUBLISHABLE_KEY</code> — run{' '}
|
|
81
|
+
<code>npx @slyxup/cli keys create</code>
|
|
82
|
+
</p>
|
|
83
|
+
)}
|
|
57
84
|
<div className="slx-mark">
|
|
58
85
|
<KeyholeMark />
|
|
59
86
|
</div>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { useAuth } from '@slyxup/react';
|
|
1
2
|
import { GitHubIcon, GoogleIcon } from '../../icons';
|
|
3
|
+
import { injectStyles } from '../../styles';
|
|
2
4
|
|
|
3
5
|
export interface SocialButtonsProps {
|
|
4
6
|
/** Show only these providers. Default: both */
|
|
5
7
|
providers?: Array<'google' | 'github'>;
|
|
6
|
-
/** OAuth start path base
|
|
8
|
+
/** OAuth start path base. Default: `<apiUrl>/v1/oauth` from the provider client */
|
|
7
9
|
basePath?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
@@ -15,8 +17,13 @@ const META = {
|
|
|
15
17
|
/** Provider buttons that redirect to hosted OAuth start. */
|
|
16
18
|
export function SocialButtons({
|
|
17
19
|
providers = ['google', 'github'],
|
|
18
|
-
basePath
|
|
20
|
+
basePath,
|
|
19
21
|
}: SocialButtonsProps) {
|
|
22
|
+
injectStyles();
|
|
23
|
+
const { client } = useAuth() as unknown as {
|
|
24
|
+
client?: { apiUrl?: string };
|
|
25
|
+
};
|
|
26
|
+
const base = basePath ?? `${client?.apiUrl ?? ''}/v1/oauth`;
|
|
20
27
|
return (
|
|
21
28
|
<div className="slx-social">
|
|
22
29
|
{providers.map((p) => {
|
|
@@ -27,7 +34,7 @@ export function SocialButtons({
|
|
|
27
34
|
type="button"
|
|
28
35
|
className="slx-social-btn"
|
|
29
36
|
onClick={() => {
|
|
30
|
-
window.location.href = `${
|
|
37
|
+
window.location.href = `${base}/${p}`;
|
|
31
38
|
}}
|
|
32
39
|
>
|
|
33
40
|
<Icon /> {label}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useAuth, useUser } from '@slyxup/react';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { injectStyles } from '../../styles';
|
|
3
4
|
|
|
4
5
|
function initials(name: string | null | undefined, email: string): string {
|
|
5
6
|
if (name?.trim()) return name.trim().slice(0, 1).toUpperCase();
|
|
@@ -8,6 +9,7 @@ function initials(name: string | null | undefined, email: string): string {
|
|
|
8
9
|
|
|
9
10
|
/** Avatar + dropdown with profile actions and sign out. */
|
|
10
11
|
export function UserButton() {
|
|
12
|
+
injectStyles();
|
|
11
13
|
const { isLoaded, user } = useUser();
|
|
12
14
|
const { signOut } = useAuth();
|
|
13
15
|
const [open, setOpen] = useState(false);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useAuth, useUser } from '@slyxup/react';
|
|
2
2
|
import { type FormEvent, useEffect, useState } from 'react';
|
|
3
|
+
import { injectStyles } from '../../styles';
|
|
3
4
|
|
|
4
5
|
/** Edit first/last name + avatar URL. */
|
|
5
6
|
export function UserProfile() {
|
|
7
|
+
injectStyles();
|
|
6
8
|
const { isLoaded, user, reload } = useUser();
|
|
7
9
|
const { client } = useAuth();
|
|
8
10
|
const [firstName, setFirstName] = useState('');
|
package/src/styles.ts
CHANGED
|
@@ -3,19 +3,24 @@
|
|
|
3
3
|
* Injected once via <SlyxUpStyles />. Theme with CSS variables on :root or .slyxup-scope.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export const FONT_LINK = `<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">`;
|
|
7
|
+
|
|
6
8
|
export const CSS = `
|
|
9
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
|
10
|
+
|
|
7
11
|
.slyxup-root {
|
|
8
12
|
--slx-accent: #5b5bd6;
|
|
9
13
|
--slx-accent-hover: #4c4cc4;
|
|
10
|
-
--slx-accent-soft: rgba(91, 91, 214, 0.
|
|
14
|
+
--slx-accent-soft: rgba(91, 91, 214, 0.14);
|
|
11
15
|
--slx-bg: #ffffff;
|
|
12
|
-
--slx-bg-page: #
|
|
16
|
+
--slx-bg-page: #e9eaf6;
|
|
13
17
|
--slx-ink: #16161d;
|
|
14
18
|
--slx-muted: #6f6f7b;
|
|
15
|
-
--slx-border: #
|
|
19
|
+
--slx-border: #d8d8e8;
|
|
16
20
|
--slx-danger: #d64550;
|
|
17
21
|
--slx-radius: 12px;
|
|
18
|
-
--slx-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
22
|
+
--slx-font: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
23
|
+
--slx-display: "Space Grotesk", "DM Sans", sans-serif;
|
|
19
24
|
--slx-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
20
25
|
|
|
21
26
|
font-family: var(--slx-font);
|
|
@@ -55,9 +60,10 @@ export const CSS = `
|
|
|
55
60
|
border: 1px solid var(--slx-border);
|
|
56
61
|
border-radius: calc(var(--slx-radius) + 4px);
|
|
57
62
|
padding: 32px;
|
|
58
|
-
box-shadow: 0
|
|
63
|
+
box-shadow: 0 4px 12px rgba(16,16,29,.08), 0 16px 40px rgba(16,16,29,.12), 0 0 0 1px rgba(16,16,29,.04);
|
|
59
64
|
box-sizing: border-box;
|
|
60
65
|
}
|
|
66
|
+
.slyxup-root .slx-card { background: #ffffff; }
|
|
61
67
|
.slx-card-error { animation: slx-shake .45s cubic-bezier(.36,.07,.19,.97) both; }
|
|
62
68
|
|
|
63
69
|
/* ── Header / keyhole mark ── */
|
|
@@ -68,7 +74,7 @@ export const CSS = `
|
|
|
68
74
|
margin-bottom: 18px;
|
|
69
75
|
}
|
|
70
76
|
.slx-mark svg { display: block; }
|
|
71
|
-
.slx-title { font-size:
|
|
77
|
+
.slx-title { font-family: var(--slx-display); font-size: 20px; font-weight: 650; letter-spacing: -0.02em; margin: 0 0 6px; }
|
|
72
78
|
.slx-subtitle { font-size: 13.5px; color: var(--slx-muted); margin: 0 0 22px; line-height: 1.5; }
|
|
73
79
|
|
|
74
80
|
/* ── Fields ── */
|
|
@@ -99,17 +105,21 @@ export const CSS = `
|
|
|
99
105
|
/* ── Button ── */
|
|
100
106
|
.slx-btn {
|
|
101
107
|
width: 100%; box-sizing: border-box;
|
|
102
|
-
font:
|
|
103
|
-
color: #fff; background:
|
|
104
|
-
border:
|
|
108
|
+
font-family: var(--slx-font); font-size: 14px; font-weight: 600; letter-spacing: 0.01em;
|
|
109
|
+
color: #fff; background: #0a0a0f;
|
|
110
|
+
border: 1px solid #0a0a0f; border-radius: var(--slx-radius);
|
|
105
111
|
padding: 11px 14px; cursor: pointer;
|
|
106
112
|
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
107
|
-
transition: background .15s, transform .06s;
|
|
113
|
+
transition: background .15s, transform .06s, border-color .15s;
|
|
108
114
|
}
|
|
109
|
-
.slx-btn:hover { background:
|
|
110
|
-
.slx-btn:active { transform: scale(.985); }
|
|
111
|
-
.slx-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px
|
|
115
|
+
.slx-btn:hover { background: #1a1a23; border-color: #1a1a23; }
|
|
116
|
+
.slx-btn:active { transform: scale(.985); background: #000; }
|
|
117
|
+
.slx-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(10,10,15,.14), 0 0 0 1px #0a0a0f; }
|
|
112
118
|
.slx-btn[disabled] { opacity: .6; cursor: not-allowed; }
|
|
119
|
+
@media (prefers-color-scheme: dark) {
|
|
120
|
+
.slyxup-root:not(.slyxup-light) .slx-btn { background: #f2f2f5; color: #0a0a0f; border-color: #f2f2f5; }
|
|
121
|
+
.slyxup-root:not(.slyxup-light) .slx-btn:hover { background: #e6e6eb; border-color: #e6e6eb; }
|
|
122
|
+
}
|
|
113
123
|
.slx-spinner {
|
|
114
124
|
width: 15px; height: 15px; flex: none;
|
|
115
125
|
border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
|
|
@@ -140,8 +150,12 @@ export const CSS = `
|
|
|
140
150
|
content: ""; height: 1px; flex: 1; background: var(--slx-border);
|
|
141
151
|
}
|
|
142
152
|
.slx-footer { font-size: 13px; color: var(--slx-muted); margin-top: 20px; text-align: center; }
|
|
143
|
-
.slx-link {
|
|
144
|
-
|
|
153
|
+
.slx-link {
|
|
154
|
+
color: var(--slx-accent); font-weight: 600; text-decoration: none; cursor: pointer;
|
|
155
|
+
background: none; border: none; font: inherit; font-size: inherit;
|
|
156
|
+
padding: 0; margin: 0;
|
|
157
|
+
}
|
|
158
|
+
.slx-link:hover { text-decoration: underline; color: var(--slx-accent-hover); }
|
|
145
159
|
.slx-link:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--slx-accent-soft); border-radius: 4px; }
|
|
146
160
|
|
|
147
161
|
/* ── Success state ── */
|
|
@@ -188,9 +202,17 @@ export const CSS = `
|
|
|
188
202
|
|
|
189
203
|
let injected = false;
|
|
190
204
|
|
|
191
|
-
/** Inject the SlyxUp stylesheet once per document. */
|
|
205
|
+
/** Inject the SlyxUp stylesheet + DM Sans font once per document. */
|
|
192
206
|
export function injectStyles(): void {
|
|
193
207
|
if (injected || typeof document === 'undefined') return;
|
|
208
|
+
// DM Sans font link
|
|
209
|
+
if (!document.querySelector('link[href*="DM+Sans"]')) {
|
|
210
|
+
const fontLink = document.createElement('link');
|
|
211
|
+
fontLink.rel = 'stylesheet';
|
|
212
|
+
fontLink.href =
|
|
213
|
+
'https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700;9..40,800&family=Space+Grotesk:wght@400;500;600;700&display=swap';
|
|
214
|
+
document.head.appendChild(fontLink);
|
|
215
|
+
}
|
|
194
216
|
const style = document.createElement('style');
|
|
195
217
|
style.setAttribute('data-slyxup', 'styles');
|
|
196
218
|
style.textContent = CSS;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const mockSignIn = vi.fn().mockResolvedValue({ ok: true });
|
|
6
|
+
const mockSignUp = vi.fn().mockResolvedValue({ ok: true });
|
|
7
|
+
const mockSignOut = vi.fn().mockResolvedValue({ ok: true });
|
|
8
|
+
|
|
9
|
+
vi.mock('@slyxup/react', () => ({
|
|
10
|
+
useAuth: () => ({
|
|
11
|
+
isLoaded: true,
|
|
12
|
+
isSignedIn: false,
|
|
13
|
+
userId: null,
|
|
14
|
+
client: {},
|
|
15
|
+
signIn: mockSignIn,
|
|
16
|
+
signUp: mockSignUp,
|
|
17
|
+
signOut: mockSignOut,
|
|
18
|
+
}),
|
|
19
|
+
useUser: () => ({
|
|
20
|
+
isLoaded: true,
|
|
21
|
+
isSignedIn: false,
|
|
22
|
+
user: null,
|
|
23
|
+
isSignedOut: true,
|
|
24
|
+
reload: vi.fn(),
|
|
25
|
+
}),
|
|
26
|
+
useSession: () => ({
|
|
27
|
+
isLoaded: true,
|
|
28
|
+
isSignedIn: false,
|
|
29
|
+
session: null,
|
|
30
|
+
reload: vi.fn(),
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import { SignIn } from '../src/components/SignIn/SignIn';
|
|
35
|
+
import { SignUp } from '../src/components/SignUp/SignUp';
|
|
36
|
+
import { SocialButtons } from '../src/components/SocialButtons/SocialButtons';
|
|
37
|
+
import { UserButton } from '../src/components/UserButton/UserButton';
|
|
38
|
+
|
|
39
|
+
describe('SignIn', () => {
|
|
40
|
+
beforeEach(() => vi.clearAllMocks());
|
|
41
|
+
|
|
42
|
+
it('renders sign in heading', () => {
|
|
43
|
+
render(<SignIn />);
|
|
44
|
+
expect(screen.getByRole('heading', { name: /sign in/i })).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders email and password inputs', () => {
|
|
48
|
+
render(<SignIn />);
|
|
49
|
+
expect(screen.getByLabelText(/email/i)).toBeTruthy();
|
|
50
|
+
expect(screen.getByLabelText(/password/i)).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders submit button', () => {
|
|
54
|
+
render(<SignIn />);
|
|
55
|
+
expect(screen.getByRole('button', { name: /sign in/i })).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('shows social buttons by default', () => {
|
|
59
|
+
render(<SignIn />);
|
|
60
|
+
expect(screen.getByText(/continue with google/i)).toBeTruthy();
|
|
61
|
+
expect(screen.getByText(/continue with github/i)).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('hides social buttons when social=false', () => {
|
|
65
|
+
render(<SignIn social={false} />);
|
|
66
|
+
expect(screen.queryByText(/continue with google/i)).toBeNull();
|
|
67
|
+
expect(screen.queryByText(/continue with github/i)).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('calls signIn on form submit', async () => {
|
|
71
|
+
render(<SignIn />);
|
|
72
|
+
fireEvent.change(screen.getByLabelText(/email/i), { target: { value: 'a@b.com' } });
|
|
73
|
+
fireEvent.change(screen.getByLabelText(/password/i), { target: { value: '12345678' } });
|
|
74
|
+
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
|
|
75
|
+
await waitFor(() => expect(mockSignIn).toHaveBeenCalledWith({ email: 'a@b.com', password: '12345678' }));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('shows sign up link when onSignUpClick provided', () => {
|
|
79
|
+
const onSignUp = vi.fn();
|
|
80
|
+
render(<SignIn onSignUpClick={onSignUp} />);
|
|
81
|
+
expect(screen.getByText(/sign up/i)).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('SignUp', () => {
|
|
86
|
+
beforeEach(() => vi.clearAllMocks());
|
|
87
|
+
|
|
88
|
+
it('renders create your account heading', () => {
|
|
89
|
+
render(<SignUp />);
|
|
90
|
+
expect(screen.getByRole('heading', { name: /create your account/i })).toBeTruthy();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders first name, email, password inputs', () => {
|
|
94
|
+
render(<SignUp />);
|
|
95
|
+
expect(screen.getByLabelText(/first name/i)).toBeTruthy();
|
|
96
|
+
expect(screen.getByLabelText(/email/i)).toBeTruthy();
|
|
97
|
+
expect(screen.getByLabelText(/password/i)).toBeTruthy();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('shows social buttons by default', () => {
|
|
101
|
+
render(<SignUp />);
|
|
102
|
+
expect(screen.getByText(/continue with google/i)).toBeTruthy();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('SocialButtons', () => {
|
|
107
|
+
beforeEach(() => vi.clearAllMocks());
|
|
108
|
+
|
|
109
|
+
it('renders both providers by default', () => {
|
|
110
|
+
render(<SocialButtons />);
|
|
111
|
+
expect(screen.getByText(/continue with google/i)).toBeTruthy();
|
|
112
|
+
expect(screen.getByText(/continue with github/i)).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('renders only specified providers', () => {
|
|
116
|
+
render(<SocialButtons providers={['google']} />);
|
|
117
|
+
expect(screen.getByText(/continue with google/i)).toBeTruthy();
|
|
118
|
+
expect(screen.queryByText(/continue with github/i)).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('UserButton', () => {
|
|
123
|
+
beforeEach(() => vi.clearAllMocks());
|
|
124
|
+
|
|
125
|
+
it('renders avatar', () => {
|
|
126
|
+
render(<UserButton />);
|
|
127
|
+
expect(screen.getByRole('button', { name: /account menu/i })).toBeTruthy();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('opens dropdown on click', async () => {
|
|
131
|
+
render(<UserButton />);
|
|
132
|
+
fireEvent.click(screen.getByRole('button', { name: /account menu/i }));
|
|
133
|
+
await waitFor(() => expect(screen.getByText(/sign out/i)).toBeTruthy());
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { KeyholeMark, GoogleIcon, GitHubIcon, CheckIcon } from '../src/icons';
|
|
5
|
+
|
|
6
|
+
describe('KeyholeMark', () => {
|
|
7
|
+
it('renders an SVG', () => {
|
|
8
|
+
const { container } = render(<KeyholeMark />);
|
|
9
|
+
expect(container.querySelector('svg')).toBeTruthy();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('GoogleIcon', () => {
|
|
14
|
+
it('renders an SVG', () => {
|
|
15
|
+
const { container } = render(<GoogleIcon />);
|
|
16
|
+
expect(container.querySelector('svg')).toBeTruthy();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('GitHubIcon', () => {
|
|
21
|
+
it('renders an SVG', () => {
|
|
22
|
+
const { container } = render(<GitHubIcon />);
|
|
23
|
+
expect(container.querySelector('svg')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('CheckIcon', () => {
|
|
28
|
+
it('renders an SVG', () => {
|
|
29
|
+
const { container } = render(<CheckIcon />);
|
|
30
|
+
expect(container.querySelector('svg')).toBeTruthy();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CSS, injectStyles } from '../src/styles';
|
|
3
|
+
|
|
4
|
+
describe('CSS', () => {
|
|
5
|
+
it('is a non-empty string', () => {
|
|
6
|
+
expect(typeof CSS).toBe('string');
|
|
7
|
+
expect(CSS.length).toBeGreaterThan(0);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('contains .slyxup-root', () => {
|
|
11
|
+
expect(CSS).toContain('.slyxup-root');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('contains --slx-accent', () => {
|
|
15
|
+
expect(CSS).toContain('--slx-accent');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('contains dark mode media query', () => {
|
|
19
|
+
expect(CSS).toContain('prefers-color-scheme: dark');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('contains @keyframes slx-shake', () => {
|
|
23
|
+
expect(CSS).toContain('@keyframes slx-shake');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('contains @keyframes slx-spin', () => {
|
|
27
|
+
expect(CSS).toContain('@keyframes slx-spin');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('contains prefers-reduced-motion', () => {
|
|
31
|
+
expect(CSS).toContain('prefers-reduced-motion');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('contains card styles', () => {
|
|
35
|
+
expect(CSS).toContain('.slx-card');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('contains button styles', () => {
|
|
39
|
+
expect(CSS).toContain('.slx-btn');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('injectStyles', () => {
|
|
44
|
+
it('is a function', () => {
|
|
45
|
+
expect(typeof injectStyles).toBe('function');
|
|
46
|
+
});
|
|
47
|
+
});
|
package/vitest.config.ts
ADDED