test(e2e): test login page
All checks were successful
container-scan / Container Scan (pull_request) Successful in 2m25s
docker-build / docker (pull_request) Successful in 3m52s
tests / Tests (pull_request) Successful in 5m28s

This commit is contained in:
Dominik 2025-05-13 09:04:37 +02:00
parent 5f8a9d0f59
commit 3a1b81c4be
11 changed files with 92 additions and 6 deletions

4
.env.test Normal file
View file

@ -0,0 +1,4 @@
AUTH_SECRET="auth_secret"
AUTH_URL="http://localhost:3000"
DATABASE_URL="file:./dev.db"
AUTH_AUTHENTIK_ISSUER="auth_issuer"

View file

@ -23,5 +23,5 @@ jobs:
uses: https://github.com/cypress-io/github-action@v6 uses: https://github.com/cypress-io/github-action@v6
with: with:
build: yarn run build build: yarn run build
start: yarn start start: yarn cypress:start_server
component: true component: true

1
.gitignore vendored
View file

@ -33,6 +33,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env*
!.env.example !.env.example
!.env.test
# vercel # vercel
.vercel .vercel

View file

@ -7,4 +7,10 @@ export default defineConfig({
bundler: 'webpack', bundler: 'webpack',
}, },
}, },
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
}); });

31
cypress/e2e/login.cy.ts Normal file
View file

@ -0,0 +1,31 @@
describe('login', () => {
it('loads', () => {
cy.visit('http://localhost:3000/');
cy.getBySel('login-header').should('exist');
});
it('shows login form', () => {
cy.visit('http://localhost:3000/');
cy.getBySel('login-form').should('exist');
cy.getBySel('email-input').should('exist');
cy.getBySel('password-input').should('exist');
cy.getBySel('login-button').should('exist');
});
it('shows sso button', () => {
cy.visit('http://localhost:3000/');
cy.getBySel('sso-login-button_authentik').should('exist');
});
it('allows login', () => {
cy.visit('http://localhost:3000/');
cy.getBySel('email-input').type('test@example.com');
cy.getBySel('password-input').type('password');
cy.getBySel('login-button').click();
cy.url().should('include', '/home');
});
});

View file

@ -8,6 +8,7 @@
"start": "node .next/standalone/server.js", "start": "node .next/standalone/server.js",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write .", "format": "prettier --write .",
"cypress:start_server": "cp .next/static/ .next/standalone/.next/ -r && dotenv -e .env.test -- node .next/standalone/server.js",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"cypress:run": "cypress run" "cypress:run": "cypress run"
}, },
@ -43,6 +44,7 @@
"@types/react": "19.1.3", "@types/react": "19.1.3",
"@types/react-dom": "19.1.4", "@types/react-dom": "19.1.4",
"cypress": "14.3.3", "cypress": "14.3.3",
"dotenv-cli": "^8.0.0",
"eslint": "9.26.0", "eslint": "9.26.0",
"eslint-config-next": "15.3.2", "eslint-config-next": "15.3.2",
"eslint-config-prettier": "10.1.5", "eslint-config-prettier": "10.1.5",

View file

@ -30,7 +30,9 @@ export default async function LoginPage() {
<div> <div>
<Card className='w-[350px] max-w-screen'> <Card className='w-[350px] max-w-screen'>
<CardHeader> <CardHeader>
<CardTitle className='text-lg text-center'>Login</CardTitle> <CardTitle className='text-lg text-center' data-cy='login-header'>
Login
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className='gap-6 flex flex-col'> <CardContent className='gap-6 flex flex-col'>
<LoginForm /> <LoginForm />
@ -38,7 +40,11 @@ export default async function LoginPage() {
<hr /> <hr />
{process.env.AUTH_AUTHENTIK_ISSUER && ( {process.env.AUTH_AUTHENTIK_ISSUER && (
<SSOLogin provider='authentik' providerDisplayName='SSO' /> <SSOLogin
provider='authentik'
providerDisplayName='SSO'
data-cy='sso-login-button_authentik'
/>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View file

@ -6,12 +6,13 @@ export default function LabeledInput({
label, label,
placeholder, placeholder,
value, value,
...props
}: { }: {
type: 'text' | 'email' | 'password'; type: 'text' | 'email' | 'password';
label: string; label: string;
placeholder?: string; placeholder?: string;
value?: string; value?: string;
}) { } & React.InputHTMLAttributes<HTMLInputElement>) {
const elementId = Math.random().toString(36).substring(2, 15); const elementId = Math.random().toString(36).substring(2, 15);
return ( return (
@ -23,6 +24,7 @@ export default function LabeledInput({
placeholder={placeholder} placeholder={placeholder}
defaultValue={value} defaultValue={value}
id={elementId} id={elementId}
{...props}
/> />
</div> </div>
); );

View file

@ -3,21 +3,24 @@ import { Button } from '@/components/ui/button';
export default function LoginForm() { export default function LoginForm() {
return ( return (
<form className='flex flex-col gap-5 w-full'> <form className='flex flex-col gap-5 w-full' data-cy='login-form'>
<LabeledInput <LabeledInput
type='email' type='email'
label='E-Mail' label='E-Mail'
placeholder='Enter your E-Mail' placeholder='Enter your E-Mail'
data-cy='email-input'
/> />
<LabeledInput <LabeledInput
type='password' type='password'
label='Password' label='Password'
placeholder='Enter your Password' placeholder='Enter your Password'
data-cy='password-input'
/> />
<Button <Button
className='hover:bg-blue-600 hover:text-white' className='hover:bg-blue-600 hover:text-white'
type='submit' type='submit'
variant='secondary' variant='secondary'
data-cy='login-button'
> >
Login Login
</Button> </Button>

View file

@ -5,10 +5,11 @@ import { faOpenid } from '@fortawesome/free-brands-svg-icons';
export default function SSOLogin({ export default function SSOLogin({
provider, provider,
providerDisplayName, providerDisplayName,
...props
}: { }: {
provider: string; provider: string;
providerDisplayName: string; providerDisplayName: string;
}) { } & React.HTMLProps<HTMLFormElement>) {
return ( return (
<form <form
className='flex flex-col items-center gap-4 w-full' className='flex flex-col items-center gap-4 w-full'
@ -16,6 +17,7 @@ export default function SSOLogin({
'use server'; 'use server';
await signIn(provider); await signIn(provider);
}} }}
{...props}
> >
<IconButton <IconButton
className='w-full' className='w-full'

View file

@ -3133,6 +3133,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dotenv-cli@npm:^8.0.0":
version: 8.0.0
resolution: "dotenv-cli@npm:8.0.0"
dependencies:
cross-spawn: "npm:^7.0.6"
dotenv: "npm:^16.3.0"
dotenv-expand: "npm:^10.0.0"
minimist: "npm:^1.2.6"
bin:
dotenv: cli.js
checksum: 10c0/000469632758b7b44aaaa80cbbbd7f0c94dc170ec02e51aa8d8280341a0108fb7407954c23054257b77235b064033efdb8745836633eb6fd1586924953cf0528
languageName: node
linkType: hard
"dotenv-expand@npm:^10.0.0":
version: 10.0.0
resolution: "dotenv-expand@npm:10.0.0"
checksum: 10c0/298f5018e29cfdcb0b5f463ba8e8627749103fbcf6cf81c561119115754ed582deee37b49dfc7253028aaba875ab7aea5fa90e5dac88e511d009ab0e6677924e
languageName: node
linkType: hard
"dotenv@npm:^16.3.0":
version: 16.5.0
resolution: "dotenv@npm:16.5.0"
checksum: 10c0/5bc94c919fbd955bf0ba44d33922a1e93d1078e64a1db5c30faeded1d996e7a83c55332cb8ea4fae5a9ca4d0be44cbceb95c5811e70f9f095298df09d1997dd9
languageName: node
linkType: hard
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": "dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "dunder-proto@npm:1.0.1" resolution: "dunder-proto@npm:1.0.1"
@ -5372,6 +5400,7 @@ __metadata:
class-variance-authority: "npm:^0.7.1" class-variance-authority: "npm:^0.7.1"
clsx: "npm:^2.1.1" clsx: "npm:^2.1.1"
cypress: "npm:14.3.3" cypress: "npm:14.3.3"
dotenv-cli: "npm:^8.0.0"
eslint: "npm:9.26.0" eslint: "npm:9.26.0"
eslint-config-next: "npm:15.3.2" eslint-config-next: "npm:15.3.2"
eslint-config-prettier: "npm:10.1.5" eslint-config-prettier: "npm:10.1.5"