login - register
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kt-vite",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"private": true,
|
||||
"homepage": "https://karateturniere.de",
|
||||
"type": "module",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
|
||||
@@ -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
19
test/e2e/example.spec.js
Normal 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();
|
||||
});
|
||||
449
tests-examples/demo-todo-app.spec.js
Normal file
449
tests-examples/demo-todo-app.spec.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user