Merge pull request 'test: add event creation test' (#127)
Reviewed-on: #127 Reviewed-by: Maximilian Liebmann <lima@noreply.git.dominikstahl.dev>
This commit is contained in:
commit
8dd014ead1
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', () => {
|
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.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" />
|
/// <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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -43,7 +43,10 @@ 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>
|
||||||
|
|
|
@ -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(
|
||||||
|
@ -64,6 +66,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'>
|
||||||
|
@ -82,6 +85,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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -190,11 +190,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' && eventData?.data?.event?.created_at
|
props.type === 'edit' && eventData?.data?.event?.created_at
|
||||||
|
@ -209,14 +204,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'>
|
||||||
|
@ -233,6 +236,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>
|
||||||
|
@ -246,6 +250,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>
|
||||||
|
@ -256,6 +261,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'>
|
||||||
|
@ -266,6 +272,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'>
|
||||||
|
@ -288,9 +295,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'>
|
||||||
|
@ -302,6 +311,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>
|
||||||
|
@ -355,6 +365,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>
|
||||||
|
@ -390,7 +401,6 @@ const EventForm: React.FC<EventFormProps> = (props) => {
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue