login - register

This commit is contained in:
Mario Peters
2025-06-09 02:24:38 +02:00
parent 711aee4ccc
commit 03859020d9
16 changed files with 541 additions and 159 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "kt-vite",
"version": "3.1.0",
"version": "3.2.0",
"private": true,
"homepage": "https://karateturniere.de",
"type": "module",

View File

@@ -12,13 +12,14 @@
},
"cancel": "",
"category": "Kategorie",
"changeID": "",
"city": "Město",
"closing-date": "Uzávěrka",
"club": "Klub",
"count": "Spočítat",
"date": "datum",
"dicipline": "",
"discipline": "disciplína",
"Download Liste": "",
"email": "E-mailem",
"female": "",
@@ -39,6 +40,7 @@
},
"login": "",
"logo": "Logo",
"logout": "",
"male": "",
"man": "Muž",
"mixed": "",
@@ -50,6 +52,7 @@
"belt": "Pás",
"birthDate": "Datum narození",
"club": "Klub",
"edit": "",
"edit-participant": "Upravit účastníka",
"female": "ženský",
"forename": "křestní jméno",

View File

@@ -12,11 +12,13 @@
},
"cancel": "Abbrechen",
"category": "Kategorie",
"changeID": "",
"city": "Stadt",
"closing-date": "Meldeschluss",
"club": "Verein",
"count": "Anzahl",
"date": "Datum",
"dicipline": "",
"discipline": "Disziplin",
"Download Liste": "Download Liste",
"email": "E-Mail",
@@ -38,6 +40,7 @@
},
"login": "Anmelden",
"logo": "Logo",
"logout": "",
"male": "Männlich",
"man": "Mann",
"mixed": "Gemischt",
@@ -49,6 +52,7 @@
"belt": "Gürtel",
"birthDate": "Geburtstag",
"club": "Verein",
"edit": "",
"edit-participant": "Teilnehmer bearbeiten",
"female": "Weiblich",
"forename": "Vorname",
@@ -107,4 +111,4 @@
"street": "Straße"
},
"woman": "Frau"
}
}

View File

@@ -12,11 +12,13 @@
},
"cancel": "cancel",
"category": "category",
"changeID": "changeID",
"city": "city",
"closing-date": "closing date",
"club": "club",
"count": "count",
"date": "date",
"dicipline": "dicipline",
"discipline": "discipline",
"Download Liste": "Download Liste",
"email": "email",
@@ -38,6 +40,7 @@
},
"login": "login",
"logo": "logo",
"logout": "logout",
"male": "male",
"man": "man",
"mixed": "mixed",
@@ -49,6 +52,7 @@
"belt": "belt",
"birthDate": "birth Date",
"club": "club",
"edit": "participant.edit",
"edit-participant": "Edit Participant",
"female": "female",
"forename": "forename",

View File

@@ -12,11 +12,13 @@
},
"cancel": "",
"category": "CATEGORÍA",
"changeID": "",
"city": "ciudad",
"closing-date": "Fecha de cierre",
"club": "Club",
"count": "Contar",
"date": "Fecha",
"dicipline": "",
"discipline": "Disciplina",
"Download Liste": "",
"email": "Email",
@@ -38,6 +40,7 @@
},
"login": "",
"logo": "logo",
"logout": "",
"male": "",
"man": "hombre",
"mixed": "",
@@ -49,6 +52,7 @@
"belt": "Cinturón",
"birthDate": "Fecha de nacimiento",
"club": "Club",
"edit": "",
"edit-participant": "Editar participante",
"female": "Hembra",
"forename": "Nombre",

View File

@@ -12,11 +12,13 @@
},
"cancel": "Annuler",
"category": "Catégorie",
"changeID": "",
"city": "Ville",
"closing-date": "Date de clôture",
"club": "Club",
"count": "Nombre",
"date": "Date",
"dicipline": "",
"discipline": "Discipline",
"Download Liste": "Télécharger la liste",
"email": "E-mail",
@@ -38,6 +40,7 @@
},
"login": "Connexion",
"logo": "Logo",
"logout": "",
"male": "Homme",
"man": "Homme",
"mixed": "Mixte",
@@ -49,6 +52,7 @@
"belt": "Ceinture",
"birthDate": "Date de naissance",
"club": "Club",
"edit": "",
"edit-participant": "Modifier le participant",
"female": "Femme",
"forename": "Prénom",
@@ -107,4 +111,4 @@
"street": "Rue"
},
"woman": "Femme"
}
}

View File

@@ -12,11 +12,13 @@
},
"cancel": "Annulla",
"category": "Categoria",
"changeID": "",
"city": "Città",
"closing-date": "Data di chiusura",
"club": "Club",
"count": "Numero",
"date": "Data",
"dicipline": "",
"discipline": "Disciplina",
"Download Liste": "Scarica elenco",
"email": "E-mail",
@@ -38,6 +40,7 @@
},
"login": "Accedi",
"logo": "Logo",
"logout": "",
"male": "Maschio",
"man": "Uomo",
"mixed": "Misto",
@@ -49,6 +52,7 @@
"belt": "Cintura",
"birthDate": "Data di nascita",
"club": "Club",
"edit": "",
"edit-participant": "Modifica partecipante",
"female": "Femmina",
"forename": "Nome",
@@ -107,4 +111,4 @@
"street": "Via"
},
"woman": "Donna"
}
}

View File

@@ -12,11 +12,13 @@
},
"cancel": "Avbryt",
"category": "Kategori",
"changeID": "",
"city": "By",
"closing-date": "Utløpsdato",
"club": "Klubb",
"count": "Antall",
"date": "Dato",
"dicipline": "",
"discipline": "Disiplin",
"Download Liste": "Last ned liste",
"email": "E-post",
@@ -38,6 +40,7 @@
},
"login": "Logg inn",
"logo": "Logo",
"logout": "",
"male": "Mann",
"man": "Mann",
"mixed": "Blandet",
@@ -49,6 +52,7 @@
"belt": "Belte",
"birthDate": "Fødselsdato",
"club": "Klubb",
"edit": "",
"edit-participant": "Rediger deltaker",
"female": "Kvinne",
"forename": "Fornavn",
@@ -107,4 +111,4 @@
"street": "Gate"
},
"woman": "Kvinne"
}
}

View File

@@ -61,8 +61,6 @@ export async function signUpEdit(apiServer, token, newData) {
}
export async function login(apiServer, email, password, fakeID) {
console.log('Logging in with email:', email, 'and fakeID:', fakeID)
console.log('Using API server:', apiServer)
const url = apiServer + '/authentications'
const data = { login: { email, password, fakeID } }
console.log('Request URL:', url)

View File

@@ -3,8 +3,9 @@ import { AccountCircle } from '@mui/icons-material'
import { Link } from 'react-router-dom'
import { styled, AppBar, Toolbar, Typography, IconButton, MenuItem, Menu } from '@mui/material'
import { useTranslation } from 'react-i18next'
const PREFIX = 'AppBar'
import { useUser } from '../../context/UserContext'
const PREFIX = 'AppBar'
const classes = {
root: `${PREFIX}-root`,
title: `${PREFIX}-title`,
@@ -22,45 +23,42 @@ const Root = styled(AppBar)(() => ({
},
}))
export default function MenuAppBar({ openLoginDialog, user }) {
const { i18n } = useTranslation('common')
const { t } = useTranslation('common')
const languages = [
{ code: 'de', flag: '/flags/de.svg', alt: 'DE' },
{ code: 'en', flag: '/flags/gb.svg', alt: 'EN' },
{ code: 'fr', flag: '/flags/fr.svg', alt: 'FR' },
]
export default function MenuAppBar({ openLoginDialog }) {
const { user } = useUser()
const { t, i18n } = useTranslation('common')
const [anchorEl, setAnchorEl] = useState(null)
const MenuOpen = Boolean(anchorEl)
const menuOpen = Boolean(anchorEl)
const handleMenu = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleMenu = (event) => setAnchorEl(event.currentTarget)
const handleClose = () => setAnchorEl(null)
const handleLogout = () => {
localStorage.removeItem('token')
window.location.replace('/')
window.location.reload()
}
return (
<Root position="static">
<Toolbar>
<IconButton size="small" onClick={() => i18n.changeLanguage('de')}>
<img src="/flags/de.svg" alt="DE" width="32" height="32" />
</IconButton>
<IconButton size="small" onClick={() => i18n.changeLanguage('en')}>
<img src="/flags/gb.svg" alt="EN" width="32" height="32" />
</IconButton>
<IconButton size="small" onClick={() => i18n.changeLanguage('fr')}>
<img src="/flags/fr.svg" alt="FR" width="32" height="32" />
</IconButton>
{languages.map(({ code, flag, alt }) => (
<IconButton key={code} size="small" onClick={() => i18n.changeLanguage(code)}>
<img src={flag} alt={alt} width="32" height="32" />
</IconButton>
))}
<Typography variant="h5" className={classes.title}>
<Link className={classes.headerTitle} to="/">
{import.meta.env.VITE_APP_TITLE}
</Link>
</Typography>
{!user && (
<IconButton onClick={openLoginDialog.bind(this, null)} color="inherit" size="large">
<IconButton onClick={() => openLoginDialog(null)} color="inherit" size="large">
<AccountCircle />
</IconButton>
)}
@@ -81,12 +79,12 @@ export default function MenuAppBar({ openLoginDialog, user }) {
vertical: 'top',
horizontal: 'right',
}}
open={MenuOpen}
open={menuOpen}
onClose={handleClose}
>
<MenuItem onClick={openLoginDialog.bind(this, t('signUp.signUp'))}>My account</MenuItem>
{user?.admin > 9 && <MenuItem onClick={openLoginDialog.bind(this, 'Login')}>changeID</MenuItem>}
<MenuItem onClick={handleLogout}>Logout</MenuItem>
<MenuItem onClick={() => openLoginDialog(t('participant.edit'))}>{t('profile.myAccount', 'Mein Konto')}</MenuItem>
{user?.admin > 9 && <MenuItem onClick={() => openLoginDialog('Login')}>{t('changeID', 'ID ändern')}</MenuItem>}
<MenuItem onClick={handleLogout}>{t('logout', 'Logout')}</MenuItem>
</Menu>
</div>
)}

View File

@@ -1,104 +0,0 @@
import { useTranslation } from 'react-i18next'
import { useContext, useState } from 'react'
import { Button, TextField, DialogActions, DialogContent, DialogTitle, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, Autocomplete } from '@mui/material'
import useFetch from '../UseFetch/UseFetch'
import { DialogContext } from './Context'
import { useUser } from '../../context/UserContext'
export default function ProfileDialog({ edit }) {
const { token, apiServer } = useUser()
const { t } = useTranslation('common')
const { data: clubs = [], loading } = useFetch(apiServer + '/getClubs' + token)
const dialog = useContext(DialogContext)
const initialFormData = {
gender: 'M',
name: '',
prename: '',
club: '',
email: '',
email2: '',
password: '',
password2: '',
phone: '',
}
const [formData, setFormData] = useState(initialFormData)
// Wenn edit übergeben wird, initialisiere das Formular damit
// useEffect(() => {
// if (edit) setFormData(edit)
// }, [edit])
const handleClose = () => {
dialog.onClose()
setFormData(initialFormData)
}
const handleReset = () => {
setFormData(edit ? edit : initialFormData)
}
const handleChange = (e) => {
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
}
// Spezieller Handler für Autocomplete (verein)
const handleClubChange = (event, newValue) => {
setFormData((prev) => ({
...prev,
club: newValue || '',
}))
}
const handleSave = (e) => {
e.preventDefault()
// Hier könntest du add/edit-Logik einbauen
dialog.onClose()
}
return (
!loading && (
<>
<DialogTitle id="dialog-title">{t('profile.myAccount')}</DialogTitle>
<DialogContent>
<FormControl component="fieldset" sx={{ mb: 2 }}>
<FormLabel component="legend">{t('profile.gender')}</FormLabel>
<RadioGroup aria-label="gender" name="gender" value={formData.gender} onChange={handleChange} row>
<FormControlLabel value="W" control={<Radio />} label={t('profile.female')} />
<FormControlLabel value="M" control={<Radio color="primary" />} label={t('profile.male')} />
</RadioGroup>
</FormControl>
<TextField autoFocus margin="dense" name="name" variant="standard" label={t('participant.name')} type="text" fullWidth value={formData.name} onChange={handleChange} />
<TextField margin="dense" name="prename" variant="standard" label={t('participant.forename')} type="text" fullWidth value={formData.prename} onChange={handleChange} />
<Autocomplete
id="club"
freeSolo
options={clubs}
value={formData.club}
onChange={handleClubChange}
renderInput={(params) => <TextField {...params} variant="standard" label={t('participant.club')} margin="dense" name="club" onChange={handleChange} />}
/>
<TextField margin="dense" name="email" variant="standard" label={t('email')} type="email" fullWidth value={formData.email} onChange={handleChange} />
<TextField margin="dense" name="password" variant="standard" label={t('password')} type="password" fullWidth value={formData.password} onChange={handleChange} />
<TextField margin="dense" name="password2" variant="standard" label={t('password2')} type="password" fullWidth value={formData.password2} onChange={handleChange} />
<TextField margin="dense" name="phone" variant="standard" label={t('phone')} type="text" fullWidth value={formData.phone} onChange={handleChange} />
</DialogContent>
<DialogActions>
<Button onClick={handleReset} color="secondary">
reset
</Button>
<Button onClick={handleClose} color="secondary">
Cancel
</Button>
<Button onClick={handleSave} variant="outlined" color="primary">
OK
</Button>
</DialogActions>
</>
)
)
}

View File

@@ -25,7 +25,7 @@ const Root = styled('div')({
'& .MuiCard-root': { margin: '360px 10px' },
},
[`& .${classes.round4}`]: {
'& .MuiCard-root': { margin: '0 10px' },
'& .MuiCard-root': { margin: '0 -120px' },
},
})

View File

@@ -22,7 +22,7 @@ export default function LoginDialog({ handleToken, view }) {
</div>
{state === 'Login' && <LoginContent handleToken={handleToken} setState={setState} />}
{(state === t('signUp.signUp') || state === 'EditAccount') && <SignUpEditContent state={state} setState={setState} />}
{state === t('participant.edit') || (state === t('signUp.signUp') && <SignUpEditContent />)}
{state === t('signUp.pwReset') && <PwResetContent handleToken={handleToken} setState={setState} />}
<br />
</>

View File

@@ -6,12 +6,11 @@ import { DialogContext } from '../../context/DialogContext.jsx'
import { getAccount, checkAccountExist, signUp, signUpEdit } from '../../api/account.js'
import { useUser } from '../../context/UserContext.jsx'
export const SignUpEditContent = ({ state, setState }) => {
const { token, apiServer } = useUser()
export const SignUpEditContent = () => {
const { token, apiServer, user } = useUser()
const { t } = useTranslation('common')
const dialog = useContext(DialogContext)
const isEdit = state === 'EditAccount'
const token2 = token || ''
const isEdit = user
const { data: clubs = [], loading } = useFetch(apiServer + '/getClubs')
const initialFormData = {
@@ -32,17 +31,16 @@ export const SignUpEditContent = ({ state, setState }) => {
// Account laden, falls Bearbeitung
useEffect(() => {
if (isEdit && token2) {
getAccount(apiServer, token2)
if (isEdit && token) {
getAccount(apiServer, token)
.then((responseData) => {
setAccount(responseData[0])
setFormData(responseData[0])
setState('EditAccount')
})
.catch(console.error)
}
// eslint-disable-next-line
}, [isEdit, token2])
}, [isEdit, token])
const handleClose = () => {
dialog.onClose()
@@ -51,7 +49,7 @@ export const SignUpEditContent = ({ state, setState }) => {
}
const handleReset = () => {
setFormData(account || initialFormData)
setFormData(initialFormData)
}
const handleChange = (e, value) => {
@@ -65,6 +63,10 @@ export const SignUpEditContent = ({ state, setState }) => {
...prev,
[name]: val,
}))
// Passwort-Validierung immer prüfen, wenn passwort/passwort2 geändert wird
if (name === 'passwort' || name === 'passwort2') {
setPwError(name === 'passwort' ? val !== formData.passwort2 : formData.passwort !== val)
}
}
const checkEmail = async (e) => {
@@ -77,12 +79,6 @@ export const SignUpEditContent = ({ state, setState }) => {
}
}
const checkPw = (e) => {
const pw = formData.passwort
const pw2 = e.target.value
setPwError(pw !== pw2)
}
const handleSave = async (e) => {
e.preventDefault()
try {
@@ -147,8 +143,8 @@ export const SignUpEditContent = ({ state, setState }) => {
fullWidth
value={formData.passwort || ''}
onChange={handleChange}
helperText={pwError ? t('signUp.pwNoMatch') : '8 - 30 ' + t('signUp.sign')}
inputProps={{ minLength: 8, maxLength: 30 }}
helperText={pwError ? t('signUp.pwNoMatch') : t('signUp.sign') + ' (8-30)'}
/>
<TextField
id="pw2"
@@ -162,9 +158,8 @@ export const SignUpEditContent = ({ state, setState }) => {
fullWidth
value={formData.passwort2 || ''}
onChange={handleChange}
onBlur={checkPw}
helperText={pwError ? t('signUp.pwNoMatch') : '8 - 30 ' + t('signUp.sign')}
inputProps={{ minLength: 8, maxLength: 30 }}
helperText={pwError ? t('signUp.pwNoMatch') : t('signUp.sign') + ' (8-30)'}
/>
<TextField margin="dense" name="telefon" variant="standard" label={t('phone')} type="text" fullWidth value={formData.telefon} onChange={handleChange} />
</DialogContent>

19
test/e2e/example.spec.js Normal file
View File

@@ -0,0 +1,19 @@
// @ts-check
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

View File

@@ -0,0 +1,449 @@
// @ts-check
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
});
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
];
test.describe('New Todo', () => {
test('should allow me to add todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Make sure the list only has one todo item.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0]
]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
// Make sure the list now has two todo items.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1]
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test('should clear text input field when an item is added', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test('should append new items to the bottom of the list', async ({ page }) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
// Check test using different methods.
await expect(page.getByText('3 items left')).toBeVisible();
await expect(todoCount).toHaveText('3 items left');
await expect(todoCount).toContainText('3');
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe('Mark all as completed', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should allow me to mark all items as completed', async ({ page }) => {
// Complete all todos.
await page.getByLabel('Mark all as complete').check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test('should allow me to clear the complete state of all items', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
});
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe('Item', () => {
test('should allow me to mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
// Check first item.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').check();
await expect(firstTodo).toHaveClass('completed');
// Check second item.
const secondTodo = page.getByTestId('todo-item').nth(1);
await expect(secondTodo).not.toHaveClass('completed');
await secondTodo.getByRole('checkbox').check();
// Assert completed class.
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).toHaveClass('completed');
});
test('should allow me to un-mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const firstTodo = page.getByTestId('todo-item').nth(0);
const secondTodo = page.getByTestId('todo-item').nth(1);
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test('should allow me to edit an item', async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId('todo-item');
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2]
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
});
test.describe('Editing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should hide other controls when editing', async ({ page }) => {
const todoItem = page.getByTestId('todo-item').nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
await expect(todoItem.locator('label', {
hasText: TODO_ITEMS[1],
})).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should save edits on blur', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should trim entered text', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should remove the item if an empty text string was entered', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[2],
]);
});
test('should cancel edits on escape', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe('Counter', () => {
test('should display the current number of todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('1');
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('2');
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe('Clear completed button', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test('should display the correct text', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
});
test('should remove completed items when clicked', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).getByRole('checkbox').check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should be hidden when there are no items that are completed', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
});
});
test.describe('Persistence', () => {
test('should persist its data', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const todoItems = page.getByTestId('todo-item');
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
});
});
test.describe('Routing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test('should allow me to display active items', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should respect the back button', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step('Showing all items', async () => {
await page.getByRole('link', { name: 'All' }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step('Showing active items', async () => {
await page.getByRole('link', { name: 'Active' }).click();
});
await test.step('Showing completed items', async () => {
await page.getByRole('link', { name: 'Completed' }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test('should allow me to display completed items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(1);
});
test('should allow me to display all items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await page.getByRole('link', { name: 'Completed' }).click();
await page.getByRole('link', { name: 'All' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(3);
});
test('should highlight the currently applied filter', async ({ page }) => {
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
//create locators for active and completed links
const activeLink = page.getByRole('link', { name: 'Active' });
const completedLink = page.getByRole('link', { name: 'Completed' });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass('selected');
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass('selected');
});
});
async function createDefaultTodos(page) {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
}
/**
* @param {import('@playwright/test').Page} page
* @param {number} expected
*/
async function checkNumberOfTodosInLocalStorage(page, expected) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).length === e;
}, expected);
}
/**
* @param {import('@playwright/test').Page} page
* @param {number} expected
*/
async function checkNumberOfCompletedTodosInLocalStorage(page, expected) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e;
}, expected);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} title
*/
async function checkTodosInLocalStorage(page, title) {
return await page.waitForFunction(t => {
return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t);
}, title);
}