test: add event creation test
This commit is contained in:
parent
dbf9809c7b
commit
1ec636f3b0
9 changed files with 251 additions and 189 deletions
|
@ -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');
|
||||
}
|
|
@ -1,9 +1,40 @@
|
|||
import authUser from './auth-user';
|
||||
|
||||
describe('event creation', () => {
|
||||
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.wait(1000);
|
||||
cy.visit('http://127.0.0.1:3000/events');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// 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);
|
||||
});
|
||||
|
||||
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 {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
@ -55,6 +73,7 @@ declare global {
|
|||
selector: string,
|
||||
...args: any[]
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
login(): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,10 @@ export default function Events() {
|
|||
const events = eventsData?.data?.events || [];
|
||||
|
||||
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 */}
|
||||
<h1 className='text-3xl font-bold mt-8 mb-4 text-center z-10'>
|
||||
My Events
|
||||
|
|
|
@ -43,7 +43,10 @@ export default function EventListEntry({
|
|||
return (
|
||||
<Link href={`/events/${id}`} className='block'>
|
||||
<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'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ export default function LabeledInput({
|
|||
variantSize = 'default',
|
||||
autocomplete,
|
||||
error,
|
||||
'data-cy': dataCy,
|
||||
...rest
|
||||
}: {
|
||||
label: string;
|
||||
|
@ -30,6 +31,7 @@ export default function LabeledInput({
|
|||
variantSize?: 'default' | 'big' | 'textarea';
|
||||
autocomplete?: string;
|
||||
error?: string;
|
||||
'data-cy'?: string;
|
||||
} & React.InputHTMLAttributes<HTMLInputElement>) {
|
||||
const [passwordVisible, setPasswordVisible] = React.useState(false);
|
||||
const [inputValue, setInputValue] = React.useState(
|
||||
|
@ -58,6 +60,7 @@ export default function LabeledInput({
|
|||
id={name}
|
||||
name={name}
|
||||
rows={3}
|
||||
data-cy={dataCy}
|
||||
/>
|
||||
) : (
|
||||
<span className='relative'>
|
||||
|
@ -76,6 +79,7 @@ export default function LabeledInput({
|
|||
id={name}
|
||||
name={name}
|
||||
autoComplete={autocomplete}
|
||||
data-cy={dataCy}
|
||||
{...rest}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
|
|
|
@ -191,11 +191,6 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
|||
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
|
||||
const createdAtValue =
|
||||
props.type === 'edit' && event?.created_at
|
||||
|
@ -210,188 +205,203 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
|||
const createdAtDisplay = new Date(createdAtValue).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' && fetchError)
|
||||
return <div>Error loading event.</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||
<form className='flex flex-col gap-5 w-full' onSubmit={handleSubmit}>
|
||||
<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='w-[100px] max-sm:w-full max-sm:flex max-sm:justify-center'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Name'
|
||||
placeholder={
|
||||
props.type === 'create' ? 'New Event' : 'Event Name'
|
||||
}
|
||||
name='eventName'
|
||||
variantSize='big'
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[50px]'></div>
|
||||
<Dialog open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||
<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='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'>
|
||||
<Logo colorType='monochrome' logoType='submark' width={50} />
|
||||
</div>
|
||||
<div className='grid grid-cols-4 gap-4 h-full w-full max-lg:grid-cols-2 max-sm:grid-cols-1'>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='start Time'
|
||||
timeLabel=' '
|
||||
date={startDate}
|
||||
setDate={setStartDate}
|
||||
time={startTime}
|
||||
setTime={setStartTime}
|
||||
/>
|
||||
<div className='items-center ml-auto mr-auto max-sm:mb-6 max-sm:w-full'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Name'
|
||||
placeholder={
|
||||
props.type === 'create' ? 'New Event' : 'Event Name'
|
||||
}
|
||||
name='eventName'
|
||||
variantSize='big'
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
data-cy='event-name-input'
|
||||
/>
|
||||
</div>
|
||||
<div className='w-0 sm:w-[50px]'></div>
|
||||
</div>
|
||||
<div className='grid grid-cols-4 gap-4 h-full w-full max-lg:grid-cols-2 max-sm:grid-cols-1'>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='start Time'
|
||||
timeLabel=' '
|
||||
date={startDate}
|
||||
setDate={setStartDate}
|
||||
time={startTime}
|
||||
setTime={setStartTime}
|
||||
data-cy='event-start-time-picker'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='end Time'
|
||||
timeLabel=' '
|
||||
date={endDate}
|
||||
setDate={setEndDate}
|
||||
time={endTime}
|
||||
setTime={setEndTime}
|
||||
data-cy='event-end-time-picker'
|
||||
/>
|
||||
</div>
|
||||
<div className='w-54'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Location'
|
||||
placeholder='where is the event?'
|
||||
name='eventLocation'
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
data-cy='event-location-input'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>created:</Label>
|
||||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
{createdAtDisplay}
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
<TimePicker
|
||||
dateLabel='end Time'
|
||||
timeLabel=' '
|
||||
date={endDate}
|
||||
setDate={setEndDate}
|
||||
time={endTime}
|
||||
setTime={setEndTime}
|
||||
/>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>updated:</Label>
|
||||
<p className='text-[var(--color-neutral-300)]'>
|
||||
{updatedAtDisplay}
|
||||
</p>
|
||||
</div>
|
||||
<div className='w-54'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Location'
|
||||
placeholder='where is the event?'
|
||||
name='eventLocation'
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-4'>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full grid grid-cols-2 gap-4 max-sm:grid-cols-1'>
|
||||
<div className='h-full w-full grid grid-flow-row gap-4'>
|
||||
<div className='h-full w-full'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>created:</Label>
|
||||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
{createdAtDisplay}
|
||||
</Label>
|
||||
</div>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label className='w-[70px]'>updated:</Label>
|
||||
<Label>Organiser:</Label>
|
||||
<p className='text-[var(--color-neutral-300)]'>
|
||||
{updatedAtDisplay}
|
||||
{!isClient || isLoading
|
||||
? 'Loading...'
|
||||
: data?.data.user?.name || 'Unknown User'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full grid grid-cols-2 gap-4 max-sm:grid-cols-1'>
|
||||
<div className='h-full w-full grid grid-flow-row gap-4'>
|
||||
<div className='h-full w-full'>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Label>Organiser:</Label>
|
||||
<Label className='text-[var(--color-neutral-300)]'>
|
||||
{organiserValue}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full'>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Description'
|
||||
placeholder='What is the event about?'
|
||||
name='eventDescription'
|
||||
variantSize='textarea'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
></LabeledInput>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-full w-full'>
|
||||
<Label>Participants</Label>
|
||||
<UserSearchInput
|
||||
selectedUsers={selectedParticipants}
|
||||
addUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.find((u) => u.id === user.id)
|
||||
? current
|
||||
: [...current, user],
|
||||
);
|
||||
}}
|
||||
removeUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.filter((u) => u.id !== user.id),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='primary'>Calendar</Button>
|
||||
</DialogTrigger>
|
||||
<div className='grid grid-cols-1 mt-3 sm:max-h-60 sm:grid-cols-2 sm:overflow-y-auto sm:mb-0'>
|
||||
{selectedParticipants.map((user) => (
|
||||
<ParticipantListEntry
|
||||
key={user.id}
|
||||
user={user}
|
||||
status='PENDING'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<LabeledInput
|
||||
type='text'
|
||||
label='Event Description'
|
||||
placeholder='What is the event about?'
|
||||
name='eventDescription'
|
||||
variantSize='textarea'
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
data-cy='event-description-input'
|
||||
></LabeledInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row gap-2 justify-end mt-4 mb-6'>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='secondary'
|
||||
onClick={() => {
|
||||
router.back();
|
||||
console.log('user aborted - no change in database');
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={status === 'pending'}
|
||||
>
|
||||
{status === 'pending' ? 'Saving...' : 'save event'}
|
||||
</Button>
|
||||
<div className='h-full w-full'>
|
||||
<Label>Participants</Label>
|
||||
<UserSearchInput
|
||||
selectedUsers={selectedParticipants}
|
||||
addUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.find((u) => u.id === user.id)
|
||||
? current
|
||||
: [...current, user],
|
||||
);
|
||||
}}
|
||||
removeUserAction={(user) => {
|
||||
setSelectedParticipants((current) =>
|
||||
current.filter((u) => u.id !== user.id),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant='primary'>Calendar</Button>
|
||||
</DialogTrigger>
|
||||
<div className='grid grid-cols-1 mt-3 sm:max-h-60 sm:grid-cols-2 sm:overflow-y-auto sm:mb-0'>
|
||||
{selectedParticipants.map((user) => (
|
||||
<ParticipantListEntry
|
||||
key={user.id}
|
||||
user={user}
|
||||
status='PENDING'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{isSuccess && <p>Event created!</p>}
|
||||
{error && <p className='text-red-500'>Error: {error.message}</p>}
|
||||
</div>
|
||||
</form>
|
||||
<DialogContent className='sm:max-w-[750px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Calendar</DialogTitle>
|
||||
<DialogDescription>
|
||||
Calendar for selected participants
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className='max-w-[calc(100svw-70px)]'>
|
||||
<Calendar
|
||||
userId={selectedParticipants.map((u) => u.id)}
|
||||
additionalEvents={[
|
||||
{
|
||||
id: 'temp-event',
|
||||
title: title || 'New Event',
|
||||
start: startDate ? new Date(startDate) : new Date(),
|
||||
end: endDate ? new Date(endDate) : new Date(),
|
||||
type: 'event',
|
||||
userId: 'create-event',
|
||||
colorOverride: '#ff9800',
|
||||
},
|
||||
]}
|
||||
height='600px'
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
<div className='flex flex-row gap-2 justify-end mt-4 mb-6'>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='secondary'
|
||||
onClick={() => {
|
||||
router.back();
|
||||
console.log('user aborted - no change in database');
|
||||
}}
|
||||
>
|
||||
cancel
|
||||
</Button>
|
||||
</div>
|
||||
<div className='w-[20%] grid max-sm:w-[40%]'>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='primary'
|
||||
disabled={status === 'pending'}
|
||||
data-cy='event-save-button'
|
||||
>
|
||||
{status === 'pending' ? 'Saving...' : 'save event'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{isSuccess && <p>Event created!</p>}
|
||||
{error && <p className='text-red-500'>Error: {error.message}</p>}
|
||||
</div>
|
||||
</form>
|
||||
<DialogContent className='sm:max-w-[750px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Calendar</DialogTitle>
|
||||
<DialogDescription>
|
||||
Calendar for selected participants
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className='max-w-[calc(100svw-70px)]'>
|
||||
<Calendar
|
||||
userId={selectedParticipants.map((u) => u.id)}
|
||||
additionalEvents={[
|
||||
{
|
||||
id: 'temp-event',
|
||||
title: title || 'New Event',
|
||||
start: startDate ? new Date(startDate) : new Date(),
|
||||
end: endDate ? new Date(endDate) : new Date(),
|
||||
type: 'event',
|
||||
userId: 'create-event',
|
||||
colorOverride: '#ff9800',
|
||||
},
|
||||
]}
|
||||
height='600px'
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -25,7 +25,10 @@ export default function Header({
|
|||
}>) {
|
||||
return (
|
||||
<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'>
|
||||
<SidebarTrigger variant='outline_primary' size='icon' />
|
||||
</span>
|
||||
|
|
|
@ -20,6 +20,7 @@ export default function TimePicker({
|
|||
setDate,
|
||||
time,
|
||||
setTime,
|
||||
...props
|
||||
}: {
|
||||
dateLabel?: string;
|
||||
timeLabel?: string;
|
||||
|
@ -27,11 +28,11 @@ export default function TimePicker({
|
|||
setDate?: (date: Date | undefined) => void;
|
||||
time?: string;
|
||||
setTime?: (time: string) => void;
|
||||
}) {
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<div className='flex gap-4' {...props}>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<Label htmlFor='date' className='px-1'>
|
||||
{dateLabel}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue