test: add event creation test
Some checks failed
container-scan / Container Scan (pull_request) Failing after 31s
docker-build / docker (pull_request) Failing after 2m9s
tests / Tests (pull_request) Failing after 2m27s

This commit is contained in:
Dominik 2025-07-01 08:42:15 +02:00
parent dbf9809c7b
commit aa5018f77a
Signed by: dominik
GPG key ID: 06A4003FC5049644
9 changed files with 248 additions and 189 deletions

View file

@ -1,12 +0,0 @@
export default function authUser() {
cy.visit('http://127.0.0.1:3000/login');
cy.getBySel('login-header').should('exist');
cy.getBySel('login-form').should('exist');
cy.getBySel('email-input').should('exist');
cy.getBySel('password-input').should('exist');
cy.getBySel('login-button').should('exist');
cy.getBySel('email-input').type('cypress@example.com');
cy.getBySel('password-input').type('Password123!');
cy.getBySel('login-button').click();
cy.url().should('include', '/home');
}

View file

@ -1,9 +1,40 @@
import authUser from './auth-user';
describe('event creation', () => { describe('event creation', () => {
it('loads', () => { it('loads', () => {
authUser(); cy.login();
// cy.visit('http://127.0.0.1:3000/events/new'); // TODO: Add event creation tests cy.visit('http://127.0.0.1:3000/events/new');
cy.getBySel('event-form').should('exist');
cy.getBySel('event-form').within(() => {
cy.getBySel('event-name-input').should('exist');
cy.getBySel('event-start-time-picker').should('exist');
cy.getBySel('event-end-time-picker').should('exist');
cy.getBySel('event-location-input').should('exist');
cy.getBySel('event-description-input').should('exist');
cy.getBySel('event-save-button').should('exist');
});
});
it('creates an event', () => {
cy.login();
cy.visit(
'http://127.0.0.1:3000/events/new?start=2025-07-01T01:00:00.000Z&end=2025-07-01T04:30:00.000Z',
);
cy.getBySel('event-form').should('exist');
cy.getBySel('event-form').within(() => {
cy.getBySel('event-name-input').type('Cypress Test Event');
cy.getBySel('event-location-input').type('Cypress Park');
cy.getBySel('event-description-input').type(
'This is a test event created by Cypress.',
);
cy.getBySel('event-save-button').click();
});
cy.visit('http://127.0.0.1:3000/events');
cy.getBySel('events-page').should('exist');
cy.getBySel('event-list-entry').should('exist');
cy.getBySel('event-list-entry')
.contains('Cypress Test Event')
.should('exist');
cy.getBySel('event-list-entry').contains('Cypress Park').should('exist');
}); });
}); });

View file

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-namespace */
/// <reference types="cypress" /> /// <reference types="cypress" />
// *********************************************** // ***********************************************
// This example commands.ts shows you how to // This example commands.ts shows you how to
@ -44,6 +46,22 @@ Cypress.Commands.add('getBySelLike', (selector, ...args) => {
return cy.get(`[data-cy*=${selector}]`, ...args); return cy.get(`[data-cy*=${selector}]`, ...args);
}); });
Cypress.Commands.add('login', () => {
cy.session('auth', () => {
cy.visit('http://127.0.0.1:3000/login');
cy.getBySel('login-header').should('exist');
cy.getBySel('login-form').should('exist');
cy.getBySel('email-input').should('exist');
cy.getBySel('password-input').should('exist');
cy.getBySel('login-button').should('exist');
cy.getBySel('email-input').type('cypress@example.com');
cy.getBySel('password-input').type('Password123!');
cy.getBySel('login-button').click();
cy.url().should('include', '/home');
cy.getBySel('header').should('exist');
});
});
declare global { declare global {
namespace Cypress { namespace Cypress {
interface Chainable { interface Chainable {
@ -55,6 +73,7 @@ declare global {
selector: string, selector: string,
...args: any[] ...args: any[]
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
login(): Chainable<void>;
} }
} }
} }

View file

@ -17,7 +17,10 @@ export default function Events() {
const events = eventsData?.data?.events || []; const events = eventsData?.data?.events || [];
return ( return (
<div className='relative h-full flex flex-col items-center'> <div
className='relative h-full flex flex-col items-center'
data-cy='events-page'
>
{/* Heading */} {/* Heading */}
<h1 className='text-3xl font-bold mt-8 mb-4 text-center z-10'> <h1 className='text-3xl font-bold mt-8 mb-4 text-center z-10'>
My Events My Events

View file

@ -43,7 +43,7 @@ export default function EventListEntry({
return ( return (
<Link href={`/events/${id}`} className='block'> <Link href={`/events/${id}`} className='block'>
<Card className='w-full'> <Card className='w-full'>
<div className='grid grid-cols-1 gap-2 mx-auto md:mx-4 md:grid-cols-[80px_1fr_250px]'> <div className='grid grid-cols-1 gap-2 mx-auto md:mx-4 md:grid-cols-[80px_1fr_250px]' data-cy='event-list-entry'>
<div className='w-full items-center justify-center grid'> <div className='w-full items-center justify-center grid'>
<Logo colorType='monochrome' logoType='submark' width={50} /> <Logo colorType='monochrome' logoType='submark' width={50} />
</div> </div>

View file

@ -17,6 +17,7 @@ export default function LabeledInput({
variantSize = 'default', variantSize = 'default',
autocomplete, autocomplete,
error, error,
'data-cy': dataCy,
...rest ...rest
}: { }: {
label: string; label: string;
@ -30,6 +31,7 @@ export default function LabeledInput({
variantSize?: 'default' | 'big' | 'textarea'; variantSize?: 'default' | 'big' | 'textarea';
autocomplete?: string; autocomplete?: string;
error?: string; error?: string;
'data-cy'?: string;
} & React.InputHTMLAttributes<HTMLInputElement>) { } & React.InputHTMLAttributes<HTMLInputElement>) {
const [passwordVisible, setPasswordVisible] = React.useState(false); const [passwordVisible, setPasswordVisible] = React.useState(false);
const [inputValue, setInputValue] = React.useState( const [inputValue, setInputValue] = React.useState(
@ -58,6 +60,7 @@ export default function LabeledInput({
id={name} id={name}
name={name} name={name}
rows={3} rows={3}
data-cy={dataCy}
/> />
) : ( ) : (
<span className='relative'> <span className='relative'>
@ -76,6 +79,7 @@ export default function LabeledInput({
id={name} id={name}
name={name} name={name}
autoComplete={autocomplete} autoComplete={autocomplete}
data-cy={dataCy}
{...rest} {...rest}
onChange={handleInputChange} onChange={handleInputChange}
/> />

View file

@ -191,11 +191,6 @@ const EventForm: React.FC<EventFormProps> = (props) => {
router.back(); router.back();
} }
// Calculate values for organiser, created, and updated
const organiserValue = isLoading
? 'Loading...'
: data?.data.user?.name || 'Unknown User';
// Use DB values for created_at/updated_at in edit mode // Use DB values for created_at/updated_at in edit mode
const createdAtValue = const createdAtValue =
props.type === 'edit' && event?.created_at props.type === 'edit' && event?.created_at
@ -210,14 +205,22 @@ const EventForm: React.FC<EventFormProps> = (props) => {
const createdAtDisplay = new Date(createdAtValue).toLocaleDateString(); const createdAtDisplay = new Date(createdAtValue).toLocaleDateString();
const updatedAtDisplay = new Date(updatedAtValue).toLocaleDateString(); const updatedAtDisplay = new Date(updatedAtValue).toLocaleDateString();
const [isClient, setIsClient] = React.useState(false);
React.useEffect(() => {
setIsClient(true);
}, []);
if (props.type === 'edit' && isLoading) return <div>Loading...</div>; if (props.type === 'edit' && isLoading) return <div>Loading...</div>;
if (props.type === 'edit' && fetchError) if (props.type === 'edit' && fetchError)
return <div>Error loading event.</div>; return <div>Error loading event.</div>;
return ( return (
<>
<Dialog open={calendarOpen} onOpenChange={setCalendarOpen}> <Dialog open={calendarOpen} onOpenChange={setCalendarOpen}>
<form className='flex flex-col gap-5 w-full' onSubmit={handleSubmit}> <form
className='flex flex-col gap-5 w-full'
onSubmit={handleSubmit}
data-cy='event-form'
>
<div className='grid grid-row-start:auto gap-4 sm:gap-8 w-full'> <div className='grid grid-row-start:auto gap-4 sm:gap-8 w-full'>
<div className='h-full w-full mt-0 ml-2 mb-16 flex items-center max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'> <div className='h-full w-full mt-0 ml-2 mb-16 flex items-center max-sm:grid max-sm:grid-row-start:auto max-sm:mb-6 max-sm:mt-10 max-sm:ml-0'>
<div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'> <div className='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
@ -234,6 +237,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
variantSize='big' variantSize='big'
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
data-cy='event-name-input'
/> />
</div> </div>
<div className='w-0 sm:w-[50px]'></div> <div className='w-0 sm:w-[50px]'></div>
@ -247,6 +251,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
setDate={setStartDate} setDate={setStartDate}
time={startTime} time={startTime}
setTime={setStartTime} setTime={setStartTime}
data-cy='event-start-time-picker'
/> />
</div> </div>
<div> <div>
@ -257,6 +262,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
setDate={setEndDate} setDate={setEndDate}
time={endTime} time={endTime}
setTime={setEndTime} setTime={setEndTime}
data-cy='event-end-time-picker'
/> />
</div> </div>
<div className='w-54'> <div className='w-54'>
@ -267,6 +273,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
name='eventLocation' name='eventLocation'
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
data-cy='event-location-input'
/> />
</div> </div>
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
@ -289,9 +296,11 @@ const EventForm: React.FC<EventFormProps> = (props) => {
<div className='h-full w-full'> <div className='h-full w-full'>
<div className='flex flex-row gap-2'> <div className='flex flex-row gap-2'>
<Label>Organiser:</Label> <Label>Organiser:</Label>
<Label className='text-[var(--color-neutral-300)]'> <p className='text-[var(--color-neutral-300)]'>
{organiserValue} {!isClient || isLoading
</Label> ? 'Loading...'
: data?.data.user?.name || 'Unknown User'}
</p>
</div> </div>
</div> </div>
<div className='h-full w-full'> <div className='h-full w-full'>
@ -303,6 +312,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
variantSize='textarea' variantSize='textarea'
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
data-cy='event-description-input'
></LabeledInput> ></LabeledInput>
</div> </div>
</div> </div>
@ -356,6 +366,7 @@ const EventForm: React.FC<EventFormProps> = (props) => {
type='submit' type='submit'
variant='primary' variant='primary'
disabled={status === 'pending'} disabled={status === 'pending'}
data-cy='event-save-button'
> >
{status === 'pending' ? 'Saving...' : 'save event'} {status === 'pending' ? 'Saving...' : 'save event'}
</Button> </Button>
@ -391,7 +402,6 @@ const EventForm: React.FC<EventFormProps> = (props) => {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</>
); );
}; };

View file

@ -25,7 +25,10 @@ export default function Header({
}>) { }>) {
return ( return (
<div className='w-full grid grid-rows-[50px_1fr] h-screen'> <div className='w-full grid grid-rows-[50px_1fr] h-screen'>
<header className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'> <header
className='border-b-1 grid-cols-[1fr_3fr_1fr] grid items-center px-2 shadow-md'
data-cy='header'
>
<span className='flex justify-start'> <span className='flex justify-start'>
<SidebarTrigger variant='outline_primary' size='icon' /> <SidebarTrigger variant='outline_primary' size='icon' />
</span> </span>

View file

@ -20,6 +20,7 @@ export default function TimePicker({
setDate, setDate,
time, time,
setTime, setTime,
...props
}: { }: {
dateLabel?: string; dateLabel?: string;
timeLabel?: string; timeLabel?: string;
@ -27,11 +28,11 @@ export default function TimePicker({
setDate?: (date: Date | undefined) => void; setDate?: (date: Date | undefined) => void;
time?: string; time?: string;
setTime?: (time: string) => void; setTime?: (time: string) => void;
}) { } & React.HTMLAttributes<HTMLDivElement>) {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
return ( return (
<div className='flex gap-4'> <div className='flex gap-4' {...props}>
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<Label htmlFor='date' className='px-1'> <Label htmlFor='date' className='px-1'>
{dateLabel} {dateLabel}